This tutorial includes: - 7 progressive lessons covering Elm fundamentals - Exercises with starter code - Solutions for exercises - Final project: Task Manager app with localStorage persistence Topics covered: - Basic syntax and types - Functions and functional programming concepts - The Elm Architecture (Model-View-Update) - Lists and Maybe types - HTTP requests and JSON decoding - Ports for JavaScript interop
499 lines
9.3 KiB
Markdown
499 lines
9.3 KiB
Markdown
# Lesson 3: Functions
|
|
|
|
## Learning Goals
|
|
|
|
By the end of this lesson, you will:
|
|
- Write pure functions
|
|
- Understand currying and partial application
|
|
- Use higher-order functions (map, filter, reduce)
|
|
- Use the pipe operator for readable code
|
|
- Understand function composition
|
|
|
|
## Defining Functions
|
|
|
|
### Basic Syntax
|
|
|
|
```elm
|
|
-- JavaScript
|
|
// const greet = (name) => "Hello, " + name;
|
|
|
|
-- Elm
|
|
greet : String -> String
|
|
greet name =
|
|
"Hello, " ++ name
|
|
```
|
|
|
|
Key differences:
|
|
- No parentheses around parameters
|
|
- No `return` keyword
|
|
- No curly braces
|
|
- Last expression is automatically returned
|
|
|
|
### Multiple Parameters
|
|
|
|
```elm
|
|
-- JavaScript
|
|
// const add = (a, b) => a + b;
|
|
|
|
-- Elm
|
|
add : Int -> Int -> Int
|
|
add a b =
|
|
a + b
|
|
|
|
-- Using it
|
|
add 5 3 -- 8 (no parentheses or commas!)
|
|
```
|
|
|
|
### If-Then-Else
|
|
|
|
```elm
|
|
-- JavaScript
|
|
// const abs = x => x < 0 ? -x : x;
|
|
|
|
-- Elm
|
|
abs : Int -> Int
|
|
abs x =
|
|
if x < 0 then
|
|
-x
|
|
else
|
|
x
|
|
```
|
|
|
|
**Important:** In Elm, `if` is an **expression** that returns a value, not a statement. Both branches must return the same type.
|
|
|
|
```elm
|
|
-- This won't compile:
|
|
invalid x =
|
|
if x > 0 then
|
|
"positive"
|
|
-- Missing else! What would it return?
|
|
```
|
|
|
|
## Pure Functions
|
|
|
|
In Elm, ALL functions are **pure**:
|
|
|
|
1. **Same input → Same output** (always)
|
|
2. **No side effects** (no I/O, no mutation, no randomness)
|
|
|
|
```javascript
|
|
// JavaScript - Impure function
|
|
let counter = 0;
|
|
function increment() {
|
|
counter += 1; // Side effect: modifies external state
|
|
return counter;
|
|
}
|
|
increment(); // 1
|
|
increment(); // 2 - Different result!
|
|
```
|
|
|
|
```elm
|
|
-- Elm - Pure function
|
|
increment : Int -> Int
|
|
increment counter =
|
|
counter + 1
|
|
|
|
increment 0 -- 1
|
|
increment 0 -- 1 (always!)
|
|
```
|
|
|
|
### Benefits of Pure Functions
|
|
|
|
1. **Testable** - No mocks needed, just input/output
|
|
2. **Cacheable** - Same input = same output, so cache results
|
|
3. **Predictable** - No hidden state changes
|
|
4. **Parallelizable** - Safe to run simultaneously
|
|
|
|
## Currying and Partial Application
|
|
|
|
### What is Currying?
|
|
|
|
In Elm, every function takes exactly one argument. A function with "multiple arguments" is actually a chain of functions.
|
|
|
|
```elm
|
|
add : Int -> Int -> Int
|
|
add a b = a + b
|
|
|
|
-- This is actually:
|
|
add : Int -> (Int -> Int)
|
|
-- A function that takes an Int and returns a function that takes an Int
|
|
```
|
|
|
|
### Partial Application
|
|
|
|
You can apply some arguments now and the rest later:
|
|
|
|
```elm
|
|
add : Int -> Int -> Int
|
|
add a b = a + b
|
|
|
|
addFive : Int -> Int
|
|
addFive = add 5 -- Partially applied!
|
|
|
|
addFive 3 -- 8
|
|
addFive 10 -- 15
|
|
```
|
|
|
|
**JavaScript equivalent:**
|
|
```javascript
|
|
const add = a => b => a + b;
|
|
const addFive = add(5);
|
|
addFive(3); // 8
|
|
```
|
|
|
|
### Practical Example
|
|
|
|
```elm
|
|
-- A formatting function
|
|
format : String -> String -> String
|
|
format prefix text =
|
|
prefix ++ ": " ++ text
|
|
|
|
-- Create specialized formatters
|
|
formatError : String -> String
|
|
formatError = format "ERROR"
|
|
|
|
formatWarning : String -> String
|
|
formatWarning = format "WARNING"
|
|
|
|
formatError "File not found" -- "ERROR: File not found"
|
|
formatWarning "Low memory" -- "WARNING: Low memory"
|
|
```
|
|
|
|
## Anonymous Functions (Lambdas)
|
|
|
|
```elm
|
|
-- JavaScript
|
|
// const double = x => x * 2;
|
|
// numbers.map(x => x * 2);
|
|
|
|
-- Elm
|
|
double = \x -> x * 2
|
|
List.map (\x -> x * 2) numbers
|
|
```
|
|
|
|
The `\` represents the Greek letter lambda (λ).
|
|
|
|
### Multiple Parameters
|
|
|
|
```elm
|
|
-- JavaScript
|
|
// const add = (a, b) => a + b;
|
|
|
|
-- Elm
|
|
add = \a b -> a + b
|
|
-- Or equivalently
|
|
add = \a -> \b -> a + b
|
|
```
|
|
|
|
## Higher-Order Functions
|
|
|
|
Higher-order functions take or return other functions.
|
|
|
|
### List.map (like Array.map)
|
|
|
|
```elm
|
|
-- JavaScript
|
|
// [1, 2, 3].map(x => x * 2)
|
|
|
|
-- Elm
|
|
List.map (\x -> x * 2) [1, 2, 3] -- [2, 4, 6]
|
|
|
|
-- Or with a named function
|
|
double x = x * 2
|
|
List.map double [1, 2, 3] -- [2, 4, 6]
|
|
```
|
|
|
|
### List.filter (like Array.filter)
|
|
|
|
```elm
|
|
-- JavaScript
|
|
// [1, 2, 3, 4, 5].filter(x => x > 3)
|
|
|
|
-- Elm
|
|
List.filter (\x -> x > 3) [1, 2, 3, 4, 5] -- [4, 5]
|
|
```
|
|
|
|
### List.foldl / List.foldr (like Array.reduce)
|
|
|
|
```elm
|
|
-- JavaScript
|
|
// [1, 2, 3].reduce((sum, x) => sum + x, 0)
|
|
|
|
-- Elm
|
|
List.foldl (\x sum -> sum + x) 0 [1, 2, 3] -- 6
|
|
|
|
-- Or using the (+) operator as a function
|
|
List.foldl (+) 0 [1, 2, 3] -- 6
|
|
```
|
|
|
|
Note: Arguments are in different order than JavaScript!
|
|
- `foldl` goes left-to-right
|
|
- `foldr` goes right-to-left
|
|
|
|
### Other Useful Functions
|
|
|
|
```elm
|
|
List.head [1, 2, 3] -- Just 1 (returns Maybe!)
|
|
List.tail [1, 2, 3] -- Just [2, 3]
|
|
List.take 2 [1, 2, 3, 4] -- [1, 2]
|
|
List.drop 2 [1, 2, 3, 4] -- [3, 4]
|
|
List.reverse [1, 2, 3] -- [3, 2, 1]
|
|
List.sort [3, 1, 2] -- [1, 2, 3]
|
|
List.member 2 [1, 2, 3] -- True
|
|
List.length [1, 2, 3] -- 3
|
|
```
|
|
|
|
## The Pipe Operator |>
|
|
|
|
The pipe operator makes chains of functions readable:
|
|
|
|
```elm
|
|
-- Without pipes (hard to read, inside-out)
|
|
String.toUpper (String.trim (String.reverse " hello "))
|
|
|
|
-- With pipes (clear data flow)
|
|
" hello "
|
|
|> String.reverse
|
|
|> String.trim
|
|
|> String.toUpper
|
|
-- Result: "OLLEH"
|
|
```
|
|
|
|
**How it works:** `x |> f` is the same as `f x`
|
|
|
|
### JavaScript Comparison
|
|
|
|
```javascript
|
|
// JavaScript method chaining (only works with methods)
|
|
" hello ".split("").reverse().join("").trim().toUpperCase()
|
|
|
|
// JavaScript pipe (proposed, not standard)
|
|
" hello " |> reverse |> trim |> toUpperCase
|
|
```
|
|
|
|
### Complex Example
|
|
|
|
```elm
|
|
-- Get the names of users over 21, sorted
|
|
users
|
|
|> List.filter (\user -> user.age > 21)
|
|
|> List.map .name
|
|
|> List.sort
|
|
```
|
|
|
|
## Function Composition
|
|
|
|
Combine functions into new functions:
|
|
|
|
```elm
|
|
-- The >> operator (left to right)
|
|
addOne = (+) 1
|
|
double = (*) 2
|
|
|
|
addOneThenDouble : Int -> Int
|
|
addOneThenDouble = addOne >> double
|
|
|
|
addOneThenDouble 5 -- 12 (5+1=6, 6*2=12)
|
|
|
|
-- The << operator (right to left)
|
|
doubleThenAddOne : Int -> Int
|
|
doubleThenAddOne = addOne << double
|
|
|
|
doubleThenAddOne 5 -- 11 (5*2=10, 10+1=11)
|
|
```
|
|
|
|
### Pipe vs Composition
|
|
|
|
```elm
|
|
-- Pipe: Apply to a specific value
|
|
5 |> addOne |> double -- 12
|
|
|
|
-- Composition: Create a new function
|
|
addOneThenDouble = addOne >> double
|
|
addOneThenDouble 5 -- 12
|
|
```
|
|
|
|
## Let Expressions
|
|
|
|
Use `let...in` for local bindings:
|
|
|
|
```elm
|
|
-- JavaScript
|
|
// function circleArea(radius) {
|
|
// const pi = 3.14159;
|
|
// const squared = radius * radius;
|
|
// return pi * squared;
|
|
// }
|
|
|
|
-- Elm
|
|
circleArea : Float -> Float
|
|
circleArea radius =
|
|
let
|
|
pi = 3.14159
|
|
squared = radius * radius
|
|
in
|
|
pi * squared
|
|
```
|
|
|
|
You can also define helper functions:
|
|
|
|
```elm
|
|
pythagoras : Float -> Float -> Float
|
|
pythagoras a b =
|
|
let
|
|
square x = x * x
|
|
in
|
|
sqrt (square a + square b)
|
|
```
|
|
|
|
## Exercise 3.1: Basic Functions
|
|
|
|
Write these functions:
|
|
|
|
1. `isEven : Int -> Bool` - Returns True if number is even
|
|
2. `exclaim : String -> String` - Adds "!" to the end of a string
|
|
3. `greet : String -> String -> String` - Takes a greeting and a name
|
|
|
|
<details>
|
|
<summary>Solution</summary>
|
|
|
|
```elm
|
|
isEven : Int -> Bool
|
|
isEven n =
|
|
modBy 2 n == 0
|
|
|
|
|
|
exclaim : String -> String
|
|
exclaim text =
|
|
text ++ "!"
|
|
|
|
|
|
greet : String -> String -> String
|
|
greet greeting name =
|
|
greeting ++ ", " ++ name ++ "!"
|
|
```
|
|
|
|
</details>
|
|
|
|
## Exercise 3.2: Higher-Order Functions
|
|
|
|
Given this list:
|
|
```elm
|
|
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
|
```
|
|
|
|
Use List functions to:
|
|
1. Get only the odd numbers
|
|
2. Square each number
|
|
3. Get the sum of all numbers
|
|
|
|
<details>
|
|
<summary>Solution</summary>
|
|
|
|
```elm
|
|
-- 1. Odd numbers
|
|
List.filter (\n -> modBy 2 n /= 0) numbers
|
|
-- [1, 3, 5, 7, 9]
|
|
|
|
-- 2. Squared
|
|
List.map (\n -> n * n) numbers
|
|
-- [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
|
|
|
|
-- 3. Sum
|
|
List.foldl (+) 0 numbers
|
|
-- Or: List.sum numbers
|
|
-- 55
|
|
```
|
|
|
|
</details>
|
|
|
|
## Exercise 3.3: Pipe Operator
|
|
|
|
Rewrite this using the pipe operator:
|
|
|
|
```elm
|
|
String.fromInt (List.length (List.filter isEven (List.range 1 100)))
|
|
```
|
|
|
|
<details>
|
|
<summary>Solution</summary>
|
|
|
|
```elm
|
|
List.range 1 100
|
|
|> List.filter isEven
|
|
|> List.length
|
|
|> String.fromInt
|
|
-- "50"
|
|
```
|
|
|
|
</details>
|
|
|
|
## Exercise 3.4: Partial Application
|
|
|
|
Create a `greetUser` function by partially applying this:
|
|
|
|
```elm
|
|
greet : String -> String -> String
|
|
greet greeting name =
|
|
greeting ++ ", " ++ name ++ "!"
|
|
```
|
|
|
|
So that `greetUser "Alice"` returns `"Hello, Alice!"`
|
|
|
|
<details>
|
|
<summary>Solution</summary>
|
|
|
|
```elm
|
|
greetUser : String -> String
|
|
greetUser = greet "Hello"
|
|
|
|
greetUser "Alice" -- "Hello, Alice!"
|
|
greetUser "Bob" -- "Hello, Bob!"
|
|
```
|
|
|
|
</details>
|
|
|
|
## Key Takeaways
|
|
|
|
1. **All Elm functions are pure** - Same input always gives same output
|
|
2. **All functions are curried** - Multi-arg functions are chains of single-arg functions
|
|
3. **Partial application** lets you create specialized functions
|
|
4. **The pipe operator `|>`** makes data transformation chains readable
|
|
5. **Higher-order functions** like `map`, `filter`, `foldl` are your primary tools
|
|
6. **`let...in`** provides local bindings within functions
|
|
|
|
## Common Patterns
|
|
|
|
```elm
|
|
-- Transform a list
|
|
items |> List.map transform
|
|
|
|
-- Filter and transform
|
|
items
|
|
|> List.filter condition
|
|
|> List.map transform
|
|
|
|
-- Find something
|
|
items
|
|
|> List.filter condition
|
|
|> List.head
|
|
|
|
-- Aggregate
|
|
items
|
|
|> List.foldl combiner initial
|
|
```
|
|
|
|
## What's Next?
|
|
|
|
In [Lesson 4](04-tea.md), we'll learn The Elm Architecture (TEA):
|
|
- Model: Your application state
|
|
- View: Turning state into HTML
|
|
- Update: Handling messages and updating state
|
|
|
|
This is the heart of Elm applications and inspired Redux!
|
|
|
|
---
|
|
|
|
[← Previous: Lesson 2](02-basics.md) | [Next: Lesson 4 - The Elm Architecture →](04-tea.md)
|