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
9.3 KiB
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
-- JavaScript
// const greet = (name) => "Hello, " + name;
-- Elm
greet : String -> String
greet name =
"Hello, " ++ name
Key differences:
- No parentheses around parameters
- No
returnkeyword - No curly braces
- Last expression is automatically returned
Multiple Parameters
-- 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
-- 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.
-- 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:
- Same input → Same output (always)
- No side effects (no I/O, no mutation, no randomness)
// JavaScript - Impure function
let counter = 0;
function increment() {
counter += 1; // Side effect: modifies external state
return counter;
}
increment(); // 1
increment(); // 2 - Different result!
-- Elm - Pure function
increment : Int -> Int
increment counter =
counter + 1
increment 0 -- 1
increment 0 -- 1 (always!)
Benefits of Pure Functions
- Testable - No mocks needed, just input/output
- Cacheable - Same input = same output, so cache results
- Predictable - No hidden state changes
- 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.
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:
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:
const add = a => b => a + b;
const addFive = add(5);
addFive(3); // 8
Practical Example
-- 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)
-- 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
-- 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)
-- 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)
-- 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)
-- 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!
foldlgoes left-to-rightfoldrgoes right-to-left
Other Useful Functions
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:
-- 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 method chaining (only works with methods)
" hello ".split("").reverse().join("").trim().toUpperCase()
// JavaScript pipe (proposed, not standard)
" hello " |> reverse |> trim |> toUpperCase
Complex Example
-- 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:
-- 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
-- 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:
-- 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:
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:
isEven : Int -> Bool- Returns True if number is evenexclaim : String -> String- Adds "!" to the end of a stringgreet : String -> String -> String- Takes a greeting and a name
Solution
isEven : Int -> Bool
isEven n =
modBy 2 n == 0
exclaim : String -> String
exclaim text =
text ++ "!"
greet : String -> String -> String
greet greeting name =
greeting ++ ", " ++ name ++ "!"
Exercise 3.2: Higher-Order Functions
Given this list:
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Use List functions to:
- Get only the odd numbers
- Square each number
- Get the sum of all numbers
Solution
-- 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
Exercise 3.3: Pipe Operator
Rewrite this using the pipe operator:
String.fromInt (List.length (List.filter isEven (List.range 1 100)))
Solution
List.range 1 100
|> List.filter isEven
|> List.length
|> String.fromInt
-- "50"
Exercise 3.4: Partial Application
Create a greetUser function by partially applying this:
greet : String -> String -> String
greet greeting name =
greeting ++ ", " ++ name ++ "!"
So that greetUser "Alice" returns "Hello, Alice!"
Solution
greetUser : String -> String
greetUser = greet "Hello"
greetUser "Alice" -- "Hello, Alice!"
greetUser "Bob" -- "Hello, Bob!"
Key Takeaways
- All Elm functions are pure - Same input always gives same output
- All functions are curried - Multi-arg functions are chains of single-arg functions
- Partial application lets you create specialized functions
- The pipe operator
|>makes data transformation chains readable - Higher-order functions like
map,filter,foldlare your primary tools let...inprovides local bindings within functions
Common Patterns
-- 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, 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 | Next: Lesson 4 - The Elm Architecture →