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
This commit is contained in:
533
lessons/05-lists-maybe.md
Normal file
533
lessons/05-lists-maybe.md
Normal file
@@ -0,0 +1,533 @@
|
||||
# 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
|
||||
|
||||
<details>
|
||||
<summary>Solution</summary>
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Exercise 5.2: Safe Division
|
||||
|
||||
Write a function `safeDivide : Int -> Int -> Maybe Int` that:
|
||||
- Returns `Nothing` if dividing by zero
|
||||
- Returns `Just result` otherwise
|
||||
|
||||
<details>
|
||||
<summary>Solution</summary>
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 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.
|
||||
|
||||
<details>
|
||||
<summary>Solution</summary>
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Solution</summary>
|
||||
|
||||
```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)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 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)
|
||||
Reference in New Issue
Block a user