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

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 return keyword
  • 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:

  1. Same input → Same output (always)
  2. 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

  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.

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!

  • foldl goes left-to-right
  • foldr goes 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:

  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
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:

  1. Get only the odd numbers
  2. Square each number
  3. 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

  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

-- 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 →