Files
elm/lessons/03-functions.md
Mark Gerrard 7d7986f3ab Initial commit: Elm tutorial for JavaScript developers
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
2026-03-11 11:07:15 +00:00

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)