# Lesson 5: Lists & Maybe ## Learning Goals By the end of this lesson, you will: - Master List operations in Elm - Understand the Maybe type deeply - Handle missing values safely - Use pattern matching effectively - Work with Result for error handling ## Lists in Elm Lists in Elm are **linked lists** (not arrays like JavaScript). ```elm -- Creating lists numbers : List Int numbers = [1, 2, 3, 4, 5] strings : List String strings = ["apple", "banana", "cherry"] empty : List a empty = [] -- Using cons operator (::) to prepend moreNumbers = 0 :: numbers -- [0, 1, 2, 3, 4, 5] ``` ### JavaScript Comparison | JavaScript | Elm | |-----------|-----| | `[1, 2, 3]` | `[1, 2, 3]` | | `arr.push(4)` | Creates new list: `arr ++ [4]` | | `arr.unshift(0)` | `0 :: arr` (prepend) | | `arr[0]` | `List.head arr` (returns Maybe!) | | `arr.length` | `List.length arr` | ### Important: Lists Are Homogeneous ```elm -- This works numbers = [1, 2, 3] -- This does NOT compile mixed = [1, "hello", True] -- ERROR: all elements must be same type ``` JavaScript allows mixed arrays, Elm doesn't. This catches bugs early! ## Essential List Functions ### Creating Lists ```elm List.range 1 5 -- [1, 2, 3, 4, 5] List.repeat 3 "hi" -- ["hi", "hi", "hi"] List.singleton 42 -- [42] ``` ### Transforming Lists ```elm List.map (\x -> x * 2) [1, 2, 3] -- [2, 4, 6] List.map String.toUpper ["a", "b", "c"] -- ["A", "B", "C"] -- With index List.indexedMap (\i x -> String.fromInt i ++ ": " ++ x) ["a", "b"] -- ["0: a", "1: b"] ``` ### Filtering ```elm List.filter (\x -> x > 2) [1, 2, 3, 4, 5] -- [3, 4, 5] List.filter String.isEmpty ["", "hello", "", "world"] -- ["", ""] -- Partition: split into two lists List.partition (\x -> x > 2) [1, 2, 3, 4, 5] -- ([3, 4, 5], [1, 2]) ``` ### Combining Lists ```elm [1, 2] ++ [3, 4] -- [1, 2, 3, 4] List.concat [[1,2], [3,4]] -- [1, 2, 3, 4] List.intersperse 0 [1,2,3] -- [1, 0, 2, 0, 3] ``` ### Reducing (Folding) ```elm -- Sum List.foldl (+) 0 [1, 2, 3, 4] -- 10 -- Product List.foldl (*) 1 [1, 2, 3, 4] -- 24 -- Build a string List.foldl (\item acc -> acc ++ item) "" ["a", "b", "c"] -- "abc" -- Convenience functions List.sum [1, 2, 3, 4] -- 10 List.product [1, 2, 3, 4] -- 24 List.maximum [1, 5, 3] -- Just 5 (returns Maybe!) List.minimum [1, 5, 3] -- Just 1 ``` ### Accessing Elements ```elm List.head [1, 2, 3] -- Just 1 List.tail [1, 2, 3] -- Just [2, 3] List.head [] -- Nothing List.take 2 [1, 2, 3, 4] -- [1, 2] List.drop 2 [1, 2, 3, 4] -- [3, 4] ``` Notice `head` and `tail` return `Maybe`! This is Elm protecting you. ## The Maybe Type `Maybe` is Elm's solution to null/undefined. It forces you to handle missing values. ```elm type Maybe a = Just a | Nothing ``` ### Why Maybe? ```javascript // JavaScript - Runtime crash waiting to happen function getFirst(arr) { return arr[0]; // undefined if empty! } const first = getFirst([]); console.log(first.toUpperCase()); // CRASH! ``` ```elm -- Elm - Must handle both cases getFirst : List a -> Maybe a getFirst list = List.head list case getFirst [] of Just value -> String.toUpper value -- Safe! Nothing -> "No value" -- Must handle this case ``` ### Working with Maybe #### Pattern Matching (Most Common) ```elm showResult : Maybe Int -> String showResult maybeNum = case maybeNum of Just n -> "The number is " ++ String.fromInt n Nothing -> "No number found" ``` #### Maybe.withDefault ```elm -- Provide a default value Maybe.withDefault 0 (Just 5) -- 5 Maybe.withDefault 0 Nothing -- 0 -- Useful pattern name = Maybe.withDefault "Anonymous" maybeName ``` #### Maybe.map Transform the value inside if it exists: ```elm Maybe.map (\x -> x * 2) (Just 5) -- Just 10 Maybe.map (\x -> x * 2) Nothing -- Nothing -- Chain operations Just "hello" |> Maybe.map String.toUpper |> Maybe.map String.reverse -- Just "OLLEH" ``` #### Maybe.andThen Chain operations that might fail: ```elm -- String.toInt returns Maybe Int String.toInt "42" -- Just 42 String.toInt "hello" -- Nothing -- Chain fallible operations parseAndDouble : String -> Maybe Int parseAndDouble str = String.toInt str |> Maybe.andThen (\n -> Just (n * 2)) parseAndDouble "5" -- Just 10 parseAndDouble "hello" -- Nothing ``` ### Maybe in Practice ```elm type alias User = { name : String , email : Maybe String -- Email is optional , age : Maybe Int -- Age is optional } displayEmail : User -> String displayEmail user = case user.email of Just email -> email Nothing -> "No email provided" -- Or more concisely: displayEmail2 : User -> String displayEmail2 user = Maybe.withDefault "No email provided" user.email ``` ## Pattern Matching Pattern matching is a powerful way to destructure data. ### On Lists ```elm describeList : List a -> String describeList list = case list of [] -> "Empty list" [x] -> "Single element" [x, y] -> "Two elements" x :: rest -> "Starts with something, has more" ``` ### On Maybe ```elm case maybeName of Just name -> "Hello, " ++ name Nothing -> "Hello, stranger" ``` ### On Custom Types ```elm type Status = Loading | Success String | Error String Int showStatus : Status -> String showStatus status = case status of Loading -> "Loading..." Success message -> "Success: " ++ message Error message code -> "Error " ++ String.fromInt code ++ ": " ++ message ``` ### Wildcards Use `_` to ignore values: ```elm isLoading : Status -> Bool isLoading status = case status of Loading -> True _ -> False -- Matches Success and Error ``` ## The Result Type `Result` is like `Maybe` but carries error information: ```elm type Result error value = Ok value | Err error ``` ### Example: Parsing Numbers ```elm parseAge : String -> Result String Int parseAge input = case String.toInt input of Just age -> if age < 0 then Err "Age cannot be negative" else if age > 150 then Err "Age seems unrealistic" else Ok age Nothing -> Err "Please enter a valid number" -- Using it case parseAge userInput of Ok age -> "Your age is " ++ String.fromInt age Err errorMessage -> "Invalid: " ++ errorMessage ``` ### Result Functions ```elm Result.withDefault 0 (Ok 5) -- 5 Result.withDefault 0 (Err "oops") -- 0 Result.map (\x -> x * 2) (Ok 5) -- Ok 10 Result.map (\x -> x * 2) (Err "x") -- Err "x" ``` ## Exercise 5.1: List Operations Given this list of numbers: ```elm numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ``` Write expressions to: 1. Get all even numbers 2. Square each number 3. Get the sum of squares of even numbers
Solution ```elm -- 1. Even numbers evens = List.filter (\n -> modBy 2 n == 0) numbers -- [2, 4, 6, 8, 10] -- 2. Square each squares = List.map (\n -> n * n) numbers -- [1, 4, 9, 16, 25, 36, 49, 64, 81, 100] -- 3. Sum of squares of evens sumOfSquaresOfEvens = numbers |> List.filter (\n -> modBy 2 n == 0) |> List.map (\n -> n * n) |> List.sum -- 220 ```
## Exercise 5.2: Safe Division Write a function `safeDivide : Int -> Int -> Maybe Int` that: - Returns `Nothing` if dividing by zero - Returns `Just result` otherwise
Solution ```elm safeDivide : Int -> Int -> Maybe Int safeDivide numerator denominator = if denominator == 0 then Nothing else Just (numerator // denominator) safeDivide 10 2 -- Just 5 safeDivide 10 0 -- Nothing ```
## Exercise 5.3: User Lookup Given this data structure: ```elm type alias User = { id : Int , name : String } users : List User users = [ { id = 1, name = "Alice" } , { id = 2, name = "Bob" } , { id = 3, name = "Charlie" } ] ``` Write `findUser : Int -> Maybe User` that finds a user by ID.
Solution ```elm findUser : Int -> Maybe User findUser id = users |> List.filter (\user -> user.id == id) |> List.head -- Or using List.find (from List.Extra package) -- findUser id = List.Extra.find (\user -> user.id == id) users findUser 2 -- Just { id = 2, name = "Bob" } findUser 99 -- Nothing ```
## Exercise 5.4: Chain Maybe Operations Write a function that: 1. Takes a list of strings 2. Gets the first element 3. Converts it to an integer 4. Doubles it ```elm processFirst : List String -> Maybe Int ```
Solution ```elm processFirst : List String -> Maybe Int processFirst strings = strings |> List.head |> Maybe.andThen String.toInt |> Maybe.map (\n -> n * 2) processFirst ["42", "hello"] -- Just 84 processFirst ["hello", "42"] -- Nothing (can't parse) processFirst [] -- Nothing (empty list) ```
## Key Takeaways 1. **Lists are homogeneous** - All elements must be the same type 2. **Lists are immutable** - Operations return new lists 3. **Maybe replaces null** - Forces explicit handling of missing values 4. **Result carries error info** - Better than just Nothing 5. **Pattern matching** is powerful for destructuring data 6. **Use `|>` with Maybe.map and Maybe.andThen** for clean chains ## Common Patterns ```elm -- Safely get and transform List.head items |> Maybe.map transform |> Maybe.withDefault defaultValue -- Chain fallible operations input |> step1 |> Maybe.andThen step2 |> Maybe.andThen step3 -- Filter and transform in one go items |> List.filterMap toMaybe ``` ## What's Next? In [Lesson 6](06-http-json.md), we'll learn about: - HTTP requests - JSON decoding - Commands and subscriptions - Side effects in Elm --- [← Previous: Lesson 4](04-tea.md) | [Next: Lesson 6 - HTTP & JSON →](06-http-json.md)