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:
2026-03-11 11:07:15 +00:00
commit 7d7986f3ab
16 changed files with 5342 additions and 0 deletions

533
lessons/05-lists-maybe.md Normal file
View 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)