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

23
.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# Elm compilation output
elm-stuff/
elm.js
# Editor files
*.swp
*.swo
*~
.idea/
.vscode/
*.sublime-*
# OS files
.DS_Store
Thumbs.db
# Node (if using npm for tooling)
node_modules/
package-lock.json
# Build artifacts
/build/
/dist/

122
README.md Normal file
View File

@@ -0,0 +1,122 @@
# Elm Tutorial for JavaScript Developers
Welcome! This tutorial is designed for developers with JavaScript experience who want to learn Elm. We'll leverage your existing knowledge while introducing functional programming concepts gradually.
## What is Elm?
Elm is a functional programming language that compiles to JavaScript. It's known for:
- **No runtime exceptions** - The compiler catches errors before your code runs
- **Friendly error messages** - Best-in-class compiler messages that help you fix issues
- **Enforced semantic versioning** - Package updates never break your code unexpectedly
- **Small bundle sizes** - Elm produces highly optimized JavaScript
## Why Learn Elm?
As a JavaScript developer, Elm will teach you:
1. Pure functional programming patterns
2. Strong static typing (that actually helps rather than hinders)
3. Immutable data structures
4. A clean architecture pattern (The Elm Architecture inspired Redux!)
## Prerequisites
- Basic JavaScript knowledge
- Familiarity with HTML
- A code editor (VS Code recommended with the Elm extension)
## Setup
### 1. Install Elm
**macOS (Homebrew):**
```bash
brew install elm
```
**Linux/macOS (npm):**
```bash
npm install -g elm
```
**Windows:**
Download the installer from [elm-lang.org](https://guide.elm-lang.org/install/elm.html)
### 2. Verify Installation
```bash
elm --version
# Should output something like: 0.19.1
```
### 3. Install Helpful Tools
```bash
# Elm formatter (like Prettier for Elm)
npm install -g elm-format
# Elm test runner
npm install -g elm-test
# Development server with hot reloading
npm install -g elm-live
```
### 4. Editor Setup
For VS Code, install the "Elm" extension by Elm tooling. This gives you:
- Syntax highlighting
- Auto-formatting on save
- Inline error messages
- Go to definition
## Tutorial Structure
| Lesson | Topic | Key Concepts |
|--------|-------|--------------|
| 1 | [Introduction & First Program](lessons/01-introduction.md) | REPL, Hello World, elm.json |
| 2 | [Basic Syntax & Types](lessons/02-basics.md) | Values, Types, Type Annotations |
| 3 | [Functions](lessons/03-functions.md) | Pure Functions, Currying, Pipes |
| 4 | [The Elm Architecture](lessons/04-tea.md) | Model, View, Update |
| 5 | [Lists & Maybe](lessons/05-lists-maybe.md) | Collections, Null Safety |
| 6 | [HTTP & JSON](lessons/06-http-json.md) | Commands, Decoders |
| 7 | [Final Project](lessons/07-final-project.md) | Building a Complete App |
## How to Use This Tutorial
1. **Read each lesson** in order - concepts build on each other
2. **Type the code yourself** - don't just copy-paste
3. **Complete the exercises** - they reinforce learning
4. **Experiment** - break things, see what errors you get
## Quick Reference: JavaScript to Elm
Here's a preview of how familiar JavaScript concepts translate to Elm:
```javascript
// JavaScript
const name = "Alice";
const add = (a, b) => a + b;
const numbers = [1, 2, 3];
const doubled = numbers.map(n => n * 2);
```
```elm
-- Elm
name = "Alice"
add a b = a + b
numbers = [1, 2, 3]
doubled = List.map (\n -> n * 2) numbers
```
## Getting Help
- [Official Elm Guide](https://guide.elm-lang.org/)
- [Elm Slack](https://elmlang.herokuapp.com/)
- [Elm Discourse](https://discourse.elm-lang.org/)
- [Elm Packages](https://package.elm-lang.org/)
---
Ready? Let's start with [Lesson 1: Introduction](lessons/01-introduction.md)!

View File

@@ -0,0 +1,25 @@
module Main exposing (main)
{-| Exercise 1: Hello World
Complete the exercises below by modifying this file.
Exercise 1.1: Change the greeting to say "Hello, [Your Name]!"
Exercise 1.2: Make the greeting ALL CAPS using String.toUpper
Exercise 1.3: Add a second paragraph with your programming experience
Run with: elm reactor
Then open http://localhost:8000/src/Main.elm
-}
import Html exposing (Html, div, h1, p, text)
main : Html msg
main =
div []
[ h1 [] [ text "Hello, Elm!" ]
-- Add more elements here for Exercise 1.3
]

View File

@@ -0,0 +1,81 @@
module Main exposing (main)
{-| Exercise 2: Counter
This is a basic counter application. Your tasks:
Exercise 2.1: Add a "Reset" button that sets the count back to 0
Exercise 2.2: Add a "Double" button that doubles the current count
Exercise 2.3: Prevent the count from going below 0
Run with: elm reactor
Then open http://localhost:8000/src/Main.elm
-}
import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
-- MODEL
type alias Model =
{ count : Int
}
init : Model
init =
{ count = 0
}
-- UPDATE
type Msg
= Increment
| Decrement
-- Add more messages here for the exercises
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
{ model | count = model.count + 1 }
Decrement ->
{ model | count = model.count - 1 }
-- VIEW
view : Model -> Html Msg
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (String.fromInt model.count) ]
, button [ onClick Increment ] [ text "+" ]
-- Add more buttons here
]
-- MAIN
main : Program () Model Msg
main =
Browser.sandbox
{ init = init
, update = update
, view = view
}

View File

@@ -0,0 +1,100 @@
module Main exposing (main)
{-| Exercise 3: Temperature Converter
Build a temperature converter:
Exercise 3.1: Convert Celsius to Fahrenheit (F = C × 9/5 + 32)
Exercise 3.2: Add Fahrenheit to Celsius conversion
Exercise 3.3: Handle invalid input gracefully
Run with: elm reactor
Then open http://localhost:8000/src/Main.elm
-}
import Browser
import Html exposing (Html, div, input, text)
import Html.Attributes exposing (placeholder, value)
import Html.Events exposing (onInput)
-- MODEL
type alias Model =
{ celsius : String
}
init : Model
init =
{ celsius = ""
}
-- UPDATE
type Msg
= UpdateCelsius String
update : Msg -> Model -> Model
update msg model =
case msg of
UpdateCelsius value ->
{ model | celsius = value }
-- HELPER FUNCTIONS
celsiusToFahrenheit : Float -> Float
celsiusToFahrenheit c =
-- TODO: Implement the conversion formula
0
-- VIEW
view : Model -> Html Msg
view model =
let
fahrenheit =
case String.toFloat model.celsius of
Just c ->
-- TODO: Call celsiusToFahrenheit and format the result
"TODO"
Nothing ->
"Enter a valid number"
in
div []
[ input
[ placeholder "Enter Celsius"
, value model.celsius
, onInput UpdateCelsius
]
[]
, text " °C = "
, text fahrenheit
]
-- MAIN
main : Program () Model Msg
main =
Browser.sandbox
{ init = init
, update = update
, view = view
}

274
lessons/01-introduction.md Normal file
View File

@@ -0,0 +1,274 @@
# Lesson 1: Introduction & Your First Elm Program
## Learning Goals
By the end of this lesson, you will:
- Understand the Elm REPL
- Create your first Elm project
- Compile and run Elm code
- Understand the project structure
## The Elm REPL
Like Node.js, Elm has a REPL (Read-Eval-Print Loop) for experimenting with code.
Start it by running:
```bash
elm repl
```
Try these expressions:
```elm
> 1 + 1
2 : number
> "Hello" ++ " World"
"Hello World" : String
> String.length "Elm"
3 : Int
```
Notice something different from JavaScript? Elm tells you the **type** of each result!
### Key Differences from JavaScript Console
| JavaScript | Elm REPL |
|-----------|----------|
| `"a" + "b"` | `"a" ++ "b"` (string concat uses `++`) |
| `1 + "1"` = `"11"` | Error! Can't mix types |
| `console.log(x)` | Just type the expression |
Type `:exit` to leave the REPL.
## Your First Elm Project
Let's create a real project:
```bash
mkdir hello-elm
cd hello-elm
elm init
```
This creates:
```
hello-elm/
├── elm.json # Project configuration (like package.json)
└── src/ # Your Elm source files go here
```
### Understanding elm.json
```json
{
"type": "application",
"source-directories": ["src"],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"elm/browser": "1.0.2",
"elm/core": "1.0.5",
"elm/html": "1.0.0"
},
"indirect": { ... }
},
"test-dependencies": { ... }
}
```
**Comparison to JavaScript:**
- `source-directories` is like setting up your bundler's entry points
- `dependencies` works like `package.json` but with direct/indirect separation
- Unlike npm, Elm enforces **semantic versioning automatically**
## Hello World
Create `src/Main.elm`:
```elm
module Main exposing (main)
import Html exposing (text)
main =
text "Hello, Elm!"
```
Let's break this down:
### 1. Module Declaration
```elm
module Main exposing (main)
```
- Every Elm file is a **module**
- `exposing (main)` says "make `main` available to other modules"
- **JavaScript equivalent:** `export { main }`
### 2. Imports
```elm
import Html exposing (text)
```
- Import the `Html` module
- `exposing (text)` lets us use `text` directly instead of `Html.text`
- **JavaScript equivalent:** `import { text } from 'html'`
### 3. The main Function
```elm
main =
text "Hello, Elm!"
```
- `main` is the entry point (like `ReactDOM.render` or your app's root)
- `text` creates an HTML text node
- No `return` keyword needed - the last expression is the return value
## Running Your Code
### Option 1: Elm Reactor (Development Server)
```bash
elm reactor
```
Open http://localhost:8000 and click on `src/Main.elm`.
### Option 2: Compile to HTML
```bash
elm make src/Main.elm
```
This creates an `index.html` file you can open in a browser.
### Option 3: Elm Live (Hot Reloading)
```bash
elm-live src/Main.elm -- --open
```
This opens your browser and reloads on file changes (like webpack-dev-server).
## Exercise 1.1: Experiment in the REPL
Start `elm repl` and try:
1. Calculate `123 * 456`
2. Concatenate three strings: "I", " love", " Elm"
3. Find the length of "functional programming"
4. Try `1 + "hello"` - what happens?
<details>
<summary>Solutions</summary>
```elm
> 123 * 456
56088 : number
> "I" ++ " love" ++ " Elm"
"I love Elm" : String
> String.length "functional programming"
22 : Int
> 1 + "hello"
-- TYPE MISMATCH --
The (+) operator is expecting both arguments to be numbers...
```
</details>
## Exercise 1.2: Modify Hello World
1. Change the greeting to include your name
2. Try using `String.toUpper` to make it ALL CAPS
<details>
<summary>Solution</summary>
```elm
module Main exposing (main)
import Html exposing (text)
main =
text (String.toUpper "Hello, Your Name!")
```
</details>
## Exercise 1.3: Multiple Elements
We need to use `Html.div` to group multiple elements:
```elm
module Main exposing (main)
import Html exposing (div, h1, p, text)
main =
div []
[ h1 [] [ text "Welcome to Elm" ]
, p [] [ text "This is a paragraph" ]
]
```
The pattern is: `element attributes children`
- `div []` - a div with no attributes
- `[ h1 ..., p ... ]` - list of children
Create a page with:
1. A heading with your name
2. A paragraph about why you're learning Elm
3. Another paragraph about your programming background
<details>
<summary>Solution</summary>
```elm
module Main exposing (main)
import Html exposing (div, h1, p, text)
main =
div []
[ h1 [] [ text "Hi, I'm Learning Elm!" ]
, p [] [ text "I want to learn functional programming." ]
, p [] [ text "I have experience with JavaScript." ]
]
```
</details>
## Key Takeaways
1. **Elm is typed** - The compiler tells you types and catches errors early
2. **No semicolons or braces** - Whitespace and indentation matter
3. **Everything is an expression** - No statements, everything returns a value
4. **String concatenation uses `++`** - Not `+` like JavaScript
5. **HTML is just functions** - `div`, `h1`, `p` are all functions
## Common Mistakes from JavaScript Developers
| Mistake | Why It Happens | Elm Way |
|---------|---------------|---------|
| Using `+` for strings | JavaScript habit | Use `++` |
| Forgetting type annotations | JS doesn't have them | Optional but recommended |
| Using `return` | JS function habit | Last expression is returned |
| Using `{}` for blocks | JS syntax | Use indentation |
## What's Next?
In [Lesson 2](02-basics.md), we'll dive deeper into Elm's type system and learn about:
- Primitive types (Int, Float, String, Bool)
- Type annotations
- Records (like JavaScript objects, but better!)
- Custom types
---
[Next: Lesson 2 - Basic Syntax & Types →](02-basics.md)

485
lessons/02-basics.md Normal file
View File

@@ -0,0 +1,485 @@
# Lesson 2: Basic Syntax & Types
## Learning Goals
By the end of this lesson, you will:
- Understand Elm's primitive types
- Write type annotations
- Work with records (Elm's "objects")
- Create type aliases
- Understand union types (a powerful feature JS doesn't have!)
## Primitive Types
### Numbers
```elm
-- Integers
age : Int
age = 30
-- Floating point
price : Float
price = 19.99
-- The 'number' type works with both
double : number -> number
double n = n * 2
double 5 -- 10 : number
double 5.5 -- 11.0 : Float
```
**JavaScript comparison:**
```javascript
// JavaScript - just "number"
const age = 30;
const price = 19.99;
```
### Strings
```elm
name : String
name = "Alice"
-- Multi-line strings use triple quotes
poem : String
poem = """
Roses are red,
Violets are blue,
Elm has no nulls,
And neither should you!
"""
-- String operations
String.length "hello" -- 5
String.reverse "hello" -- "olleh"
String.toUpper "hello" -- "HELLO"
"Hello" ++ " World" -- "Hello World"
```
**JavaScript comparison:**
```javascript
const name = "Alice";
const poem = `
Roses are red...
`;
"Hello" + " World" // Uses + not ++
```
### Booleans
```elm
isActive : Bool
isActive = True
-- Note: True and False are capitalized!
-- Comparison operators
5 > 3 -- True
5 == 5 -- True (equality uses ==, same as JS)
5 /= 3 -- True (not equal uses /= not !==)
-- Boolean operators
True && False -- False
True || False -- True
not True -- False
```
**Key difference:** Not-equal is `/=` in Elm, not `!==` like JavaScript.
### Characters
```elm
-- Single characters use single quotes
letter : Char
letter = 'A'
-- Strings use double quotes
word : String
word = "ABC"
```
JavaScript doesn't have a separate character type.
## Type Annotations
Type annotations are **optional** but highly recommended:
```elm
-- Without annotation (Elm infers the type)
add a b = a + b
-- With annotation (clearer and self-documenting)
add : Int -> Int -> Int
add a b = a + b
```
### Reading Type Annotations
```elm
greet : String -> String
-- ^input ^output
greet name = "Hello, " ++ name
add : Int -> Int -> Int
-- ^first ^second ^result
add a b = a + b
```
Think of `->` as "then returns". So `Int -> Int -> Int` reads as:
"Takes an Int, then an Int, then returns an Int"
## Records: Elm's Objects
Records are like JavaScript objects, but **immutable** and **typed**.
```elm
-- Defining a record
person =
{ name = "Alice"
, age = 30
, email = "alice@example.com"
}
-- Accessing fields (dot notation, like JS)
person.name -- "Alice"
person.age -- 30
-- Or using accessor functions
.name person -- "Alice"
```
### Record Type Annotations
```elm
-- Inline type
alice : { name : String, age : Int }
alice = { name = "Alice", age = 30 }
-- With type alias (preferred for reuse)
type alias Person =
{ name : String
, age : Int
, email : String
}
bob : Person
bob =
{ name = "Bob"
, age = 25
, email = "bob@example.com"
}
```
### Updating Records
In Elm, you can't mutate records. Instead, you create a new one:
```elm
-- JavaScript way (mutates)
-- person.age = 31;
-- Elm way (creates new record)
olderPerson = { person | age = 31 }
-- Update multiple fields
updatedPerson =
{ person
| age = 31
, email = "newemail@example.com"
}
```
The original `person` is unchanged! This is **immutability**.
### Why Immutability Matters
```javascript
// JavaScript - Mutation causes bugs
const user = { name: "Alice", score: 100 };
doSomething(user);
console.log(user.score); // Who knows? doSomething might have changed it!
```
```elm
-- Elm - No surprises
user = { name = "Alice", score = 100 }
newUser = doSomething user
user.score -- Still 100, guaranteed!
newUser.score -- Whatever doSomething returned
```
## Type Aliases
Type aliases give names to types:
```elm
type alias Person =
{ name : String
, age : Int
}
type alias Point =
{ x : Float
, y : Float
}
-- Use them in annotations
distance : Point -> Point -> Float
distance p1 p2 =
sqrt ((p2.x - p1.x)^2 + (p2.y - p1.y)^2)
```
Type aliases also create **constructor functions**:
```elm
type alias Person =
{ name : String
, age : Int
}
-- This automatically creates:
-- Person : String -> Int -> Person
alice = Person "Alice" 30
-- Same as: { name = "Alice", age = 30 }
```
## Custom Types (Union Types)
This is where Elm gets really powerful. Custom types let you define your own types with specific variants:
```elm
type Color
= Red
| Green
| Blue
type Status
= Loading
| Success String
| Error String
```
### Using Custom Types
```elm
type Status
= Loading
| Success String
| Error String
showStatus : Status -> String
showStatus status =
case status of
Loading ->
"Loading..."
Success message ->
"Success: " ++ message
Error errorMsg ->
"Error: " ++ errorMsg
showStatus Loading -- "Loading..."
showStatus (Success "Done!") -- "Success: Done!"
showStatus (Error "Not found") -- "Error: Not found"
```
### JavaScript Comparison
```javascript
// JavaScript - using strings (error-prone)
const status = "loading";
if (status === "loading") { ... }
if (status === "laoding") { ... } // Typo! No error
// JavaScript - using objects
const status = { type: "success", message: "Done" };
```
```elm
-- Elm - compiler catches typos
case status of
Laoding -> ... -- COMPILE ERROR: Laoding is not defined
```
### Maybe: Handling Missing Values
Elm has no `null` or `undefined`. Instead, it uses the `Maybe` type:
```elm
type Maybe a
= Just a
| Nothing
-- Example: Safe dictionary lookup
Dict.get "name" myDict -- Returns Maybe String
-- You MUST handle both cases
case Dict.get "name" myDict of
Just value ->
"Found: " ++ value
Nothing ->
"Not found"
```
**This eliminates null pointer exceptions entirely!**
## Exercise 2.1: Define a Record Type
Create a `type alias` for a `Book` with:
- title (String)
- author (String)
- pages (Int)
- isRead (Bool)
Then create two book records.
<details>
<summary>Solution</summary>
```elm
type alias Book =
{ title : String
, author : String
, pages : Int
, isRead : Bool
}
book1 : Book
book1 =
{ title = "The Elm Guide"
, author = "Evan Czaplicki"
, pages = 150
, isRead = True
}
book2 : Book
book2 = Book "Learn You a Haskell" "Miran Lipovaca" 400 False
```
</details>
## Exercise 2.2: Update a Record
Given this record:
```elm
player =
{ name = "Hero"
, health = 100
, score = 0
}
```
Create a new record where:
1. health is reduced to 80
2. score is increased to 50
<details>
<summary>Solution</summary>
```elm
updatedPlayer =
{ player
| health = 80
, score = 50
}
```
</details>
## Exercise 2.3: Create a Custom Type
Create a `TrafficLight` type with Red, Yellow, and Green variants.
Write a function `canGo : TrafficLight -> Bool` that returns True only for Green.
<details>
<summary>Solution</summary>
```elm
type TrafficLight
= Red
| Yellow
| Green
canGo : TrafficLight -> Bool
canGo light =
case light of
Green ->
True
Yellow ->
False
Red ->
False
-- Or more concisely:
canGo2 : TrafficLight -> Bool
canGo2 light =
light == Green
```
</details>
## Exercise 2.4: Maybe Practice
Write a function that takes a `Maybe Int` and returns the value doubled, or 0 if Nothing:
```elm
doubleOrZero : Maybe Int -> Int
```
<details>
<summary>Solution</summary>
```elm
doubleOrZero : Maybe Int -> Int
doubleOrZero maybeNum =
case maybeNum of
Just n ->
n * 2
Nothing ->
0
```
</details>
## Key Takeaways
1. **Elm has distinct Int and Float types** - Unlike JavaScript's single "number"
2. **Type annotations are documentation** - They make code self-explanatory
3. **Records are immutable** - Use update syntax `{ record | field = value }`
4. **Type aliases** create reusable type names AND constructor functions
5. **Custom types** replace string constants and are type-safe
6. **Maybe replaces null** - Forces you to handle missing values explicitly
## JavaScript to Elm Cheatsheet
| JavaScript | Elm |
|-----------|-----|
| `{}` | Record `{ field = value }` |
| `obj.field` | Same: `record.field` |
| `{...obj, field: newVal}` | `{ record \| field = newVal }` |
| `null` / `undefined` | `Maybe` type |
| String constants | Custom types |
| `===` | `==` |
| `!==` | `/=` |
## What's Next?
In [Lesson 3](03-functions.md), we'll explore:
- Pure functions
- Higher-order functions
- Currying and partial application
- The pipe operator
---
[← Previous: Lesson 1](01-introduction.md) | [Next: Lesson 3 - Functions →](03-functions.md)

498
lessons/03-functions.md Normal file
View File

@@ -0,0 +1,498 @@
# 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
```elm
-- 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
```elm
-- 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
```elm
-- 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.
```elm
-- 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
// JavaScript - Impure function
let counter = 0;
function increment() {
counter += 1; // Side effect: modifies external state
return counter;
}
increment(); // 1
increment(); // 2 - Different result!
```
```elm
-- 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.
```elm
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:
```elm
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:**
```javascript
const add = a => b => a + b;
const addFive = add(5);
addFive(3); // 8
```
### Practical Example
```elm
-- 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)
```elm
-- 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
```elm
-- 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)
```elm
-- 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)
```elm
-- 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)
```elm
-- 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
```elm
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:
```elm
-- 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
// JavaScript method chaining (only works with methods)
" hello ".split("").reverse().join("").trim().toUpperCase()
// JavaScript pipe (proposed, not standard)
" hello " |> reverse |> trim |> toUpperCase
```
### Complex Example
```elm
-- 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:
```elm
-- 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
```elm
-- 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:
```elm
-- 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:
```elm
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
<details>
<summary>Solution</summary>
```elm
isEven : Int -> Bool
isEven n =
modBy 2 n == 0
exclaim : String -> String
exclaim text =
text ++ "!"
greet : String -> String -> String
greet greeting name =
greeting ++ ", " ++ name ++ "!"
```
</details>
## Exercise 3.2: Higher-Order Functions
Given this list:
```elm
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
<details>
<summary>Solution</summary>
```elm
-- 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
```
</details>
## Exercise 3.3: Pipe Operator
Rewrite this using the pipe operator:
```elm
String.fromInt (List.length (List.filter isEven (List.range 1 100)))
```
<details>
<summary>Solution</summary>
```elm
List.range 1 100
|> List.filter isEven
|> List.length
|> String.fromInt
-- "50"
```
</details>
## Exercise 3.4: Partial Application
Create a `greetUser` function by partially applying this:
```elm
greet : String -> String -> String
greet greeting name =
greeting ++ ", " ++ name ++ "!"
```
So that `greetUser "Alice"` returns `"Hello, Alice!"`
<details>
<summary>Solution</summary>
```elm
greetUser : String -> String
greetUser = greet "Hello"
greetUser "Alice" -- "Hello, Alice!"
greetUser "Bob" -- "Hello, Bob!"
```
</details>
## 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
```elm
-- 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](04-tea.md), 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](02-basics.md) | [Next: Lesson 4 - The Elm Architecture →](04-tea.md)

729
lessons/04-tea.md Normal file
View File

@@ -0,0 +1,729 @@
# Lesson 4: The Elm Architecture (TEA)
## Learning Goals
By the end of this lesson, you will:
- Understand the Model-View-Update pattern
- Handle user input events
- Build interactive applications
- Understand how Elm inspired Redux
## What is The Elm Architecture?
The Elm Architecture (TEA) is a pattern for building web applications. It has three parts:
1. **Model** - Your application's state (the data)
2. **View** - A function that turns the model into HTML
3. **Update** - A function that updates the model based on messages
```
┌──────────────────────────────────────────┐
│ │
│ User clicks button │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Message │ │
│ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Update │───▶│ Model │ │
│ └──────────────┘ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ View │ │
│ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ HTML │───┘
│ └──────────────┘
└──────────────────────────────────────────┘
```
### Redux Connection
If you've used Redux, you already know TEA!
| Elm | Redux |
|-----|-------|
| Model | State/Store |
| Msg | Action |
| Update | Reducer |
| View | React Component |
## Your First Interactive App: Counter
Let's build a counter that you can increment and decrement.
Create a new file `src/Counter.elm`:
```elm
module Counter exposing (main)
import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
-- MODEL
type alias Model =
{ count : Int
}
init : Model
init =
{ count = 0
}
-- UPDATE
type Msg
= Increment
| Decrement
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
{ model | count = model.count + 1 }
Decrement ->
{ model | count = model.count - 1 }
-- VIEW
view : Model -> Html Msg
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (String.fromInt model.count) ]
, button [ onClick Increment ] [ text "+" ]
]
-- MAIN
main =
Browser.sandbox
{ init = init
, update = update
, view = view
}
```
Run it with:
```bash
elm reactor
```
Then open http://localhost:8000/src/Counter.elm
### Breaking It Down
#### 1. The Model
```elm
type alias Model =
{ count : Int
}
init : Model
init =
{ count = 0
}
```
The model is just a record holding your state. `init` is the initial state.
**JavaScript/Redux equivalent:**
```javascript
const initialState = { count: 0 };
```
#### 2. Messages (Msg)
```elm
type Msg
= Increment
| Decrement
```
Messages describe things that can happen. They're like Redux action types, but type-safe.
**JavaScript/Redux equivalent:**
```javascript
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
```
#### 3. The Update Function
```elm
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
{ model | count = model.count + 1 }
Decrement ->
{ model | count = model.count - 1 }
```
Takes a message and the current model, returns a new model. This is a **pure function** - no side effects!
**JavaScript/Redux equivalent:**
```javascript
function reducer(state, action) {
switch (action.type) {
case INCREMENT:
return { ...state, count: state.count + 1 };
case DECREMENT:
return { ...state, count: state.count - 1 };
default:
return state;
}
}
```
#### 4. The View Function
```elm
view : Model -> Html Msg
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (String.fromInt model.count) ]
, button [ onClick Increment ] [ text "+" ]
]
```
A pure function that takes the model and returns HTML. The `Html Msg` type means "HTML that can produce Msg values."
**React equivalent:**
```jsx
function Counter({ count, dispatch }) {
return (
<div>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
<div>{count}</div>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
</div>
);
}
```
#### 5. Wiring It Together
```elm
main =
Browser.sandbox
{ init = init
, update = update
, view = view
}
```
`Browser.sandbox` connects everything. Elm handles:
- Calling `view` whenever the model changes
- Routing user events to `update`
- Re-rendering efficiently (virtual DOM)
## HTML in Elm
Every HTML element follows this pattern:
```elm
element attributes children
```
```elm
-- <div class="container">Hello</div>
div [ class "container" ] [ text "Hello" ]
-- <button id="submit" disabled>Submit</button>
button [ id "submit", disabled True ] [ text "Submit" ]
-- <input type="text" value="hello" />
input [ type_ "text", value "hello" ] []
```
Note: Some attributes like `type` use underscores (`type_`) because they conflict with Elm keywords.
### Common Attributes
```elm
import Html.Attributes exposing (..)
-- Styling
class "my-class"
style "color" "red"
id "my-id"
-- Form inputs
value "text"
placeholder "Enter text..."
disabled True
checked True
-- Links and images
href "https://elm-lang.org"
src "image.png"
alt "Description"
```
### Common Events
```elm
import Html.Events exposing (..)
onClick Msg -- Button clicks
onInput (String -> Msg) -- Text input
onSubmit Msg -- Form submission
onMouseEnter Msg -- Mouse enter
onMouseLeave Msg -- Mouse leave
```
## Example 2: Text Input
```elm
module TextInput exposing (main)
import Browser
import Html exposing (Html, div, input, text)
import Html.Attributes exposing (placeholder, value)
import Html.Events exposing (onInput)
-- MODEL
type alias Model =
{ content : String
}
init : Model
init =
{ content = ""
}
-- UPDATE
type Msg
= Change String
update : Msg -> Model -> Model
update msg model =
case msg of
Change newContent ->
{ model | content = newContent }
-- VIEW
view : Model -> Html Msg
view model =
div []
[ input
[ placeholder "Type something..."
, value model.content
, onInput Change
]
[]
, div [] [ text ("You typed: " ++ model.content) ]
, div [] [ text ("Length: " ++ String.fromInt (String.length model.content)) ]
]
main =
Browser.sandbox
{ init = init
, update = update
, view = view
}
```
### Key Point: Messages with Data
```elm
type Msg
= Change String -- This message carries a String!
-- onInput sends the input's value with the message
onInput Change -- When input changes, send: Change "new value"
-- In update, extract the data
Change newContent ->
{ model | content = newContent }
```
## Example 3: Todo List
A more complex example with a list of items:
```elm
module TodoList exposing (main)
import Browser
import Html exposing (Html, button, div, input, li, text, ul)
import Html.Attributes exposing (placeholder, value)
import Html.Events exposing (onClick, onInput)
-- MODEL
type alias Model =
{ todos : List String
, newTodo : String
}
init : Model
init =
{ todos = []
, newTodo = ""
}
-- UPDATE
type Msg
= UpdateNewTodo String
| AddTodo
| RemoveTodo Int
update : Msg -> Model -> Model
update msg model =
case msg of
UpdateNewTodo text ->
{ model | newTodo = text }
AddTodo ->
if String.isEmpty model.newTodo then
model
else
{ model
| todos = model.todos ++ [ model.newTodo ]
, newTodo = ""
}
RemoveTodo index ->
{ model
| todos = removeAt index model.todos
}
removeAt : Int -> List a -> List a
removeAt index list =
List.take index list ++ List.drop (index + 1) list
-- VIEW
view : Model -> Html Msg
view model =
div []
[ div []
[ input
[ placeholder "Add a todo..."
, value model.newTodo
, onInput UpdateNewTodo
]
[]
, button [ onClick AddTodo ] [ text "Add" ]
]
, ul [] (List.indexedMap viewTodo model.todos)
]
viewTodo : Int -> String -> Html Msg
viewTodo index todo =
li []
[ text todo
, button [ onClick (RemoveTodo index) ] [ text "x" ]
]
main =
Browser.sandbox
{ init = init
, update = update
, view = view
}
```
### Key Concepts
1. **List.indexedMap** - Like map, but gives you the index too
2. **Messages with multiple types of data** - `RemoveTodo Int` carries which item to remove
3. **Helper functions** - `removeAt` and `viewTodo` keep code organized
## Exercise 4.1: Add Features to Counter
Enhance the counter with:
1. A "Reset" button that sets count to 0
2. A "Double" button that doubles the count
<details>
<summary>Solution</summary>
```elm
type Msg
= Increment
| Decrement
| Reset
| Double
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
{ model | count = model.count + 1 }
Decrement ->
{ model | count = model.count - 1 }
Reset ->
{ model | count = 0 }
Double ->
{ model | count = model.count * 2 }
view : Model -> Html Msg
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (String.fromInt model.count) ]
, button [ onClick Increment ] [ text "+" ]
, button [ onClick Reset ] [ text "Reset" ]
, button [ onClick Double ] [ text "Double" ]
]
```
</details>
## Exercise 4.2: Temperature Converter
Build a Celsius to Fahrenheit converter:
- Input field for Celsius
- Display the Fahrenheit equivalent
- Formula: F = C × 9/5 + 32
<details>
<summary>Solution</summary>
```elm
module TempConverter exposing (main)
import Browser
import Html exposing (Html, div, input, text)
import Html.Attributes exposing (placeholder, value)
import Html.Events exposing (onInput)
type alias Model =
{ celsius : String
}
init : Model
init =
{ celsius = ""
}
type Msg
= UpdateCelsius String
update : Msg -> Model -> Model
update msg model =
case msg of
UpdateCelsius value ->
{ model | celsius = value }
celsiusToFahrenheit : Float -> Float
celsiusToFahrenheit c =
c * 9 / 5 + 32
view : Model -> Html Msg
view model =
let
fahrenheit =
case String.toFloat model.celsius of
Just c ->
String.fromFloat (celsiusToFahrenheit c) ++ "°F"
Nothing ->
"Enter a valid number"
in
div []
[ input
[ placeholder "Celsius"
, value model.celsius
, onInput UpdateCelsius
]
[]
, text " °C = "
, text fahrenheit
]
main =
Browser.sandbox
{ init = init
, update = update
, view = view
}
```
</details>
## Exercise 4.3: Signup Form
Build a signup form with:
- Name field
- Email field
- Password field
- "Sign Up" button
- Display "Welcome, {name}!" after clicking Sign Up
<details>
<summary>Solution</summary>
```elm
module Signup exposing (main)
import Browser
import Html exposing (Html, button, div, input, text)
import Html.Attributes exposing (placeholder, type_, value)
import Html.Events exposing (onClick, onInput)
type alias Model =
{ name : String
, email : String
, password : String
, submitted : Bool
}
init : Model
init =
{ name = ""
, email = ""
, password = ""
, submitted = False
}
type Msg
= UpdateName String
| UpdateEmail String
| UpdatePassword String
| Submit
update : Msg -> Model -> Model
update msg model =
case msg of
UpdateName value ->
{ model | name = value }
UpdateEmail value ->
{ model | email = value }
UpdatePassword value ->
{ model | password = value }
Submit ->
{ model | submitted = True }
view : Model -> Html Msg
view model =
if model.submitted then
div [] [ text ("Welcome, " ++ model.name ++ "!") ]
else
div []
[ div []
[ input
[ placeholder "Name"
, value model.name
, onInput UpdateName
]
[]
]
, div []
[ input
[ placeholder "Email"
, type_ "email"
, value model.email
, onInput UpdateEmail
]
[]
]
, div []
[ input
[ placeholder "Password"
, type_ "password"
, value model.password
, onInput UpdatePassword
]
[]
]
, button [ onClick Submit ] [ text "Sign Up" ]
]
main =
Browser.sandbox
{ init = init
, update = update
, view = view
}
```
</details>
## Key Takeaways
1. **TEA has three parts:** Model, View, Update
2. **Model** holds all your application state
3. **View** is a pure function: Model → HTML
4. **Update** is a pure function: Msg → Model → Model
5. **Messages** describe events and can carry data
6. **Browser.sandbox** wires everything together
7. **HTML is just functions** with attributes and children
## The Elm Architecture Benefits
1. **No unexpected state changes** - All updates go through update
2. **Time-travel debugging** - Elm debugger lets you replay messages
3. **Easy testing** - Pure functions with no side effects
4. **Predictable** - Given a model and message, you know the result
## What's Next?
In [Lesson 5](05-lists-maybe.md), we'll dive deeper into:
- Working with Lists
- The Maybe type for handling missing values
- Pattern matching techniques
- Common List operations
---
[← Previous: Lesson 3](03-functions.md) | [Next: Lesson 5 - Lists & Maybe →](05-lists-maybe.md)

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)

840
lessons/06-http-json.md Normal file
View File

@@ -0,0 +1,840 @@
# Lesson 6: HTTP & JSON
## Learning Goals
By the end of this lesson, you will:
- Understand Commands (Cmd) in Elm
- Make HTTP requests
- Decode JSON responses
- Handle loading states and errors
- Understand how Elm manages side effects
## Side Effects in Elm
Remember: Elm functions are **pure** - they can't do side effects directly. So how do we:
- Make HTTP requests?
- Generate random numbers?
- Get the current time?
- Write to local storage?
Answer: **Commands (Cmd)**
### The Command Pattern
Instead of performing side effects, you **describe** them:
```elm
-- Instead of actually making a request:
-- response = http.get("https://api.example.com") -- NOT how Elm works
-- You return a command describing what you want:
update msg model =
case msg of
FetchData ->
( model, Http.get { url = "...", expect = ... } )
-- Returns the model AND a command
```
Elm runtime executes the command and sends the result back as a message.
## Browser.element: Elm with Commands
We need to upgrade from `Browser.sandbox` to `Browser.element`:
```elm
main =
Browser.element
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}
```
### Key Differences
| Browser.sandbox | Browser.element |
|-----------------|-----------------|
| `init : Model` | `init : flags -> (Model, Cmd Msg)` |
| `update : Msg -> Model -> Model` | `update : Msg -> Model -> (Model, Cmd Msg)` |
| No side effects | Can perform side effects via Cmd |
## Your First HTTP Request
Let's fetch a random quote from an API.
### Step 1: Set Up the Project
```bash
mkdir http-example
cd http-example
elm init
elm install elm/http
elm install elm/json
```
### Step 2: The Complete Example
```elm
module Main exposing (main)
import Browser
import Html exposing (Html, button, div, p, text)
import Html.Events exposing (onClick)
import Http
-- MODEL
type Model
= Loading
| Success String
| Failure String
init : () -> ( Model, Cmd Msg )
init _ =
( Loading, fetchQuote )
-- UPDATE
type Msg
= GotQuote (Result Http.Error String)
| FetchNewQuote
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotQuote result ->
case result of
Ok quote ->
( Success quote, Cmd.none )
Err error ->
( Failure (errorToString error), Cmd.none )
FetchNewQuote ->
( Loading, fetchQuote )
errorToString : Http.Error -> String
errorToString error =
case error of
Http.BadUrl url ->
"Bad URL: " ++ url
Http.Timeout ->
"Request timed out"
Http.NetworkError ->
"Network error"
Http.BadStatus status ->
"Bad status: " ++ String.fromInt status
Http.BadBody message ->
"Bad body: " ++ message
-- HTTP
fetchQuote : Cmd Msg
fetchQuote =
Http.get
{ url = "https://api.quotable.io/random"
, expect = Http.expectJson GotQuote quoteDecoder
}
quoteDecoder : Json.Decode.Decoder String
quoteDecoder =
Json.Decode.field "content" Json.Decode.string
-- VIEW
view : Model -> Html Msg
view model =
div []
[ case model of
Loading ->
text "Loading..."
Success quote ->
div []
[ p [] [ text quote ]
, button [ onClick FetchNewQuote ] [ text "New Quote" ]
]
Failure error ->
div []
[ p [] [ text ("Error: " ++ error) ]
, button [ onClick FetchNewQuote ] [ text "Try Again" ]
]
]
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none
-- MAIN
main =
Browser.element
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}
```
### Breaking It Down
#### The Model as State Machine
```elm
type Model
= Loading
| Success String
| Failure String
```
Using a custom type for the model means we can only be in ONE state at a time. No more:
```javascript
// JavaScript
{ isLoading: false, hasError: true, data: null } // Confusing!
```
#### Init Returns a Command
```elm
init : () -> ( Model, Cmd Msg )
init _ =
( Loading, fetchQuote ) -- Start in Loading state, fetch immediately
```
The `()` is called "unit" - it means no flags are passed. We return a **tuple** of model and command.
#### Update Returns a Command
```elm
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotQuote result ->
case result of
Ok quote ->
( Success quote, Cmd.none ) -- No more commands
Err error ->
( Failure (errorToString error), Cmd.none )
```
`Cmd.none` means "no command to execute."
#### The HTTP Request
```elm
fetchQuote : Cmd Msg
fetchQuote =
Http.get
{ url = "https://api.quotable.io/random"
, expect = Http.expectJson GotQuote quoteDecoder
}
```
- `Http.get` creates a GET request command
- `expect` tells Elm what to do with the response
- `GotQuote` is the message constructor that will wrap the result
- `quoteDecoder` parses the JSON
## JSON Decoding
Elm doesn't just convert JSON to any type automatically. You must define a **decoder** that describes the JSON structure.
### Why Decoders?
JavaScript:
```javascript
const data = JSON.parse(response);
console.log(data.user.name); // Might crash if structure is wrong!
```
Elm: The decoder either succeeds with the exact type you expect, or fails gracefully.
### Basic Decoders
```elm
import Json.Decode exposing (Decoder, string, int, float, bool)
-- Primitive decoders
string : Decoder String -- Decodes "hello" to "hello"
int : Decoder Int -- Decodes 42 to 42
float : Decoder Float -- Decodes 3.14 to 3.14
bool : Decoder Bool -- Decodes true to True
```
### Decoding Objects
```elm
import Json.Decode exposing (Decoder, field, string, int)
-- JSON: { "name": "Alice" }
nameDecoder : Decoder String
nameDecoder =
field "name" string
-- JSON: { "user": { "name": "Alice" } }
nestedNameDecoder : Decoder String
nestedNameDecoder =
field "user" (field "name" string)
```
### Decoding Multiple Fields
```elm
import Json.Decode exposing (Decoder, map2, field, string, int)
type alias User =
{ name : String
, age : Int
}
-- JSON: { "name": "Alice", "age": 30 }
userDecoder : Decoder User
userDecoder =
map2 User
(field "name" string)
(field "age" int)
```
`map2` takes:
1. A constructor function (User)
2. Decoder for first field
3. Decoder for second field
For more fields, use `map3`, `map4`, etc., or the `elm/json-decode-pipeline` package.
### Using Pipeline Style (Recommended)
Install:
```bash
elm install NoRedInk/elm-json-decode-pipeline
```
```elm
import Json.Decode exposing (Decoder, string, int)
import Json.Decode.Pipeline exposing (required, optional)
type alias User =
{ name : String
, age : Int
, email : Maybe String
}
userDecoder : Decoder User
userDecoder =
Json.Decode.succeed User
|> required "name" string
|> required "age" int
|> optional "email" (Json.Decode.map Just string) Nothing
```
### Decoding Lists
```elm
import Json.Decode exposing (Decoder, list, string)
-- JSON: ["apple", "banana", "cherry"]
fruitsDecoder : Decoder (List String)
fruitsDecoder =
list string
-- JSON: [{ "name": "Alice" }, { "name": "Bob" }]
usersDecoder : Decoder (List User)
usersDecoder =
list userDecoder
```
### Decoding with Maybe
```elm
-- JSON: { "name": "Alice", "nickname": null }
-- Or: { "name": "Alice" } (nickname missing)
type alias Person =
{ name : String
, nickname : Maybe String
}
personDecoder : Decoder Person
personDecoder =
map2 Person
(field "name" string)
(Json.Decode.maybe (field "nickname" string))
```
### Testing Decoders
```elm
import Json.Decode exposing (decodeString)
result = decodeString userDecoder """{"name": "Alice", "age": 30}"""
-- Ok { name = "Alice", age = 30 }
badResult = decodeString userDecoder """{"name": "Alice"}"""
-- Err ... (missing field "age")
```
## POST Requests with JSON Body
```elm
import Http
import Json.Encode as Encode
createUser : String -> Int -> Cmd Msg
createUser name age =
Http.post
{ url = "https://api.example.com/users"
, body = Http.jsonBody (encodeUser name age)
, expect = Http.expectJson GotNewUser userDecoder
}
encodeUser : String -> Int -> Encode.Value
encodeUser name age =
Encode.object
[ ( "name", Encode.string name )
, ( "age", Encode.int age )
]
```
### JSON Encoding
```elm
import Json.Encode as Encode
-- Primitives
Encode.string "hello" -- "hello"
Encode.int 42 -- 42
Encode.float 3.14 -- 3.14
Encode.bool True -- true
Encode.null -- null
-- Objects
Encode.object
[ ( "name", Encode.string "Alice" )
, ( "age", Encode.int 30 )
]
-- { "name": "Alice", "age": 30 }
-- Lists
Encode.list Encode.int [1, 2, 3]
-- [1, 2, 3]
```
## Complete Example: GitHub User Search
```elm
module GitHubSearch exposing (main)
import Browser
import Html exposing (Html, button, div, img, input, li, text, ul)
import Html.Attributes exposing (placeholder, src, value, width)
import Html.Events exposing (onClick, onInput)
import Http
import Json.Decode exposing (Decoder, field, int, list, string)
-- MODEL
type alias User =
{ login : String
, avatarUrl : String
, id : Int
}
type Model
= Initial
| Loading String
| Success String (List User)
| Failure String String
init : () -> ( Model, Cmd Msg )
init _ =
( Initial, Cmd.none )
-- UPDATE
type Msg
= UpdateSearch String
| Search
| GotUsers (Result Http.Error (List User))
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
UpdateSearch query ->
case model of
Initial ->
( Initial, Cmd.none )
Loading q ->
( Loading query, Cmd.none )
Success _ users ->
( Success query users, Cmd.none )
Failure _ error ->
( Failure query error, Cmd.none )
Search ->
let
query =
getQuery model
in
if String.isEmpty query then
( model, Cmd.none )
else
( Loading query, searchUsers query )
GotUsers result ->
let
query =
getQuery model
in
case result of
Ok users ->
( Success query users, Cmd.none )
Err error ->
( Failure query (errorToString error), Cmd.none )
getQuery : Model -> String
getQuery model =
case model of
Initial ->
""
Loading q ->
q
Success q _ ->
q
Failure q _ ->
q
errorToString : Http.Error -> String
errorToString error =
case error of
Http.BadUrl url ->
"Bad URL: " ++ url
Http.Timeout ->
"Request timed out"
Http.NetworkError ->
"Network error"
Http.BadStatus status ->
"Bad status: " ++ String.fromInt status
Http.BadBody message ->
"Bad body: " ++ message
-- HTTP
searchUsers : String -> Cmd Msg
searchUsers query =
Http.get
{ url = "https://api.github.com/search/users?q=" ++ query
, expect = Http.expectJson GotUsers usersDecoder
}
usersDecoder : Decoder (List User)
usersDecoder =
field "items" (list userDecoder)
userDecoder : Decoder User
userDecoder =
Json.Decode.map3 User
(field "login" string)
(field "avatar_url" string)
(field "id" int)
-- VIEW
view : Model -> Html Msg
view model =
div []
[ div []
[ input
[ placeholder "Search GitHub users..."
, value (getQuery model)
, onInput UpdateSearch
]
[]
, button [ onClick Search ] [ text "Search" ]
]
, viewResults model
]
viewResults : Model -> Html Msg
viewResults model =
case model of
Initial ->
div [] [ text "Enter a search term" ]
Loading _ ->
div [] [ text "Searching..." ]
Success _ users ->
if List.isEmpty users then
div [] [ text "No users found" ]
else
ul [] (List.map viewUser users)
Failure _ error ->
div [] [ text ("Error: " ++ error) ]
viewUser : User -> Html Msg
viewUser user =
li []
[ img [ src user.avatarUrl, width 50 ] []
, text (" " ++ user.login)
]
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none
-- MAIN
main =
Browser.element
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}
```
## Exercise 6.1: Basic HTTP Request
Create an app that:
1. Fetches a random joke from `https://official-joke-api.appspot.com/random_joke`
2. Displays the setup and punchline
3. Has a "New Joke" button
The API returns:
```json
{
"type": "general",
"setup": "Why did the chicken...",
"punchline": "To get to the other side!",
"id": 123
}
```
<details>
<summary>Solution</summary>
```elm
module JokeApp exposing (main)
import Browser
import Html exposing (Html, button, div, p, text)
import Html.Events exposing (onClick)
import Http
import Json.Decode exposing (Decoder, field, string)
type alias Joke =
{ setup : String
, punchline : String
}
type Model
= Loading
| Success Joke
| Failure String
init : () -> ( Model, Cmd Msg )
init _ =
( Loading, fetchJoke )
type Msg
= GotJoke (Result Http.Error Joke)
| FetchNewJoke
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotJoke result ->
case result of
Ok joke ->
( Success joke, Cmd.none )
Err _ ->
( Failure "Failed to fetch joke", Cmd.none )
FetchNewJoke ->
( Loading, fetchJoke )
fetchJoke : Cmd Msg
fetchJoke =
Http.get
{ url = "https://official-joke-api.appspot.com/random_joke"
, expect = Http.expectJson GotJoke jokeDecoder
}
jokeDecoder : Decoder Joke
jokeDecoder =
Json.Decode.map2 Joke
(field "setup" string)
(field "punchline" string)
view : Model -> Html Msg
view model =
div []
[ case model of
Loading ->
text "Loading..."
Success joke ->
div []
[ p [] [ text joke.setup ]
, p [] [ text joke.punchline ]
, button [ onClick FetchNewJoke ] [ text "New Joke" ]
]
Failure error ->
div []
[ text error
, button [ onClick FetchNewJoke ] [ text "Try Again" ]
]
]
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none
main =
Browser.element
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}
```
</details>
## Exercise 6.2: Nested JSON Decoding
Write a decoder for this JSON:
```json
{
"user": {
"profile": {
"name": "Alice",
"bio": "Elm enthusiast"
},
"stats": {
"followers": 100,
"following": 50
}
}
}
```
Into this type:
```elm
type alias UserInfo =
{ name : String
, bio : String
, followers : Int
, following : Int
}
```
<details>
<summary>Solution</summary>
```elm
import Json.Decode exposing (Decoder, field, int, map4, string)
userInfoDecoder : Decoder UserInfo
userInfoDecoder =
map4 UserInfo
(field "user" (field "profile" (field "name" string)))
(field "user" (field "profile" (field "bio" string)))
(field "user" (field "stats" (field "followers" int)))
(field "user" (field "stats" (field "following" int)))
-- Or using Json.Decode.at for cleaner nested access:
userInfoDecoder2 : Decoder UserInfo
userInfoDecoder2 =
map4 UserInfo
(Json.Decode.at [ "user", "profile", "name" ] string)
(Json.Decode.at [ "user", "profile", "bio" ] string)
(Json.Decode.at [ "user", "stats", "followers" ] int)
(Json.Decode.at [ "user", "stats", "following" ] int)
```
</details>
## Key Takeaways
1. **Commands describe side effects** - Elm runtime performs them
2. **HTTP requests return Results** - Success or failure
3. **JSON decoders are type-safe** - Compiler ensures correct parsing
4. **Model states** - Use custom types to represent Loading/Success/Failure
5. **Update returns (Model, Cmd)** - New state plus commands to run
## What's Next?
In [Lesson 7](07-final-project.md), we'll build a complete application:
- A todo list with persistence
- Multiple pages
- All the concepts combined!
---
[← Previous: Lesson 5](05-lists-maybe.md) | [Next: Lesson 7 - Final Project →](07-final-project.md)

856
lessons/07-final-project.md Normal file
View File

@@ -0,0 +1,856 @@
# Lesson 7: Final Project - Building a Complete App
## Learning Goals
In this final lesson, you will:
- Apply everything you've learned
- Build a complete, functional application
- Understand how to structure larger Elm projects
- Learn about ports for JavaScript interop
## Project: Task Manager
We'll build a task manager with:
- Add, complete, and delete tasks
- Filter tasks (all, active, completed)
- Persist tasks to localStorage (using ports)
- Clean, modular code structure
## Setting Up
```bash
mkdir task-manager
cd task-manager
elm init
```
## The Complete Application
Create `src/Main.elm`:
```elm
port module Main exposing (main)
import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Json.Decode as Decode exposing (Decoder)
import Json.Encode as Encode
-- PORTS
port saveTasks : Encode.Value -> Cmd msg
port loadTasks : (Decode.Value -> msg) -> Sub msg
-- MODEL
type alias Task =
{ id : Int
, description : String
, completed : Bool
}
type Filter
= All
| Active
| Completed
type alias Model =
{ tasks : List Task
, newTask : String
, nextId : Int
, filter : Filter
}
init : Decode.Value -> ( Model, Cmd Msg )
init flags =
let
loadedTasks =
case Decode.decodeValue tasksDecoder flags of
Ok tasks ->
tasks
Err _ ->
[]
nextId =
case List.maximum (List.map .id loadedTasks) of
Just maxId ->
maxId + 1
Nothing ->
1
in
( { tasks = loadedTasks
, newTask = ""
, nextId = nextId
, filter = All
}
, Cmd.none
)
-- DECODERS & ENCODERS
taskDecoder : Decoder Task
taskDecoder =
Decode.map3 Task
(Decode.field "id" Decode.int)
(Decode.field "description" Decode.string)
(Decode.field "completed" Decode.bool)
tasksDecoder : Decoder (List Task)
tasksDecoder =
Decode.list taskDecoder
encodeTask : Task -> Encode.Value
encodeTask task =
Encode.object
[ ( "id", Encode.int task.id )
, ( "description", Encode.string task.description )
, ( "completed", Encode.bool task.completed )
]
encodeTasks : List Task -> Encode.Value
encodeTasks tasks =
Encode.list encodeTask tasks
-- UPDATE
type Msg
= UpdateNewTask String
| AddTask
| ToggleTask Int
| DeleteTask Int
| SetFilter Filter
| ClearCompleted
| LoadedTasks Decode.Value
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
UpdateNewTask text ->
( { model | newTask = text }, Cmd.none )
AddTask ->
if String.isEmpty (String.trim model.newTask) then
( model, Cmd.none )
else
let
newTask =
{ id = model.nextId
, description = String.trim model.newTask
, completed = False
}
newTasks =
model.tasks ++ [ newTask ]
in
( { model
| tasks = newTasks
, newTask = ""
, nextId = model.nextId + 1
}
, saveTasks (encodeTasks newTasks)
)
ToggleTask id ->
let
toggleTask task =
if task.id == id then
{ task | completed = not task.completed }
else
task
newTasks =
List.map toggleTask model.tasks
in
( { model | tasks = newTasks }
, saveTasks (encodeTasks newTasks)
)
DeleteTask id ->
let
newTasks =
List.filter (\task -> task.id /= id) model.tasks
in
( { model | tasks = newTasks }
, saveTasks (encodeTasks newTasks)
)
SetFilter filter ->
( { model | filter = filter }, Cmd.none )
ClearCompleted ->
let
newTasks =
List.filter (\task -> not task.completed) model.tasks
in
( { model | tasks = newTasks }
, saveTasks (encodeTasks newTasks)
)
LoadedTasks value ->
case Decode.decodeValue tasksDecoder value of
Ok tasks ->
( { model | tasks = tasks }, Cmd.none )
Err _ ->
( model, Cmd.none )
-- VIEW
view : Model -> Html Msg
view model =
div [ class "container" ]
[ h1 [] [ text "Task Manager" ]
, viewInput model.newTask
, viewFilters model.filter
, viewTasks model
, viewFooter model
]
viewInput : String -> Html Msg
viewInput newTask =
div [ class "input-section" ]
[ input
[ placeholder "What needs to be done?"
, value newTask
, onInput UpdateNewTask
, onEnter AddTask
]
[]
, button [ onClick AddTask ] [ text "Add" ]
]
onEnter : Msg -> Attribute Msg
onEnter msg =
let
isEnter code =
if code == 13 then
Decode.succeed msg
else
Decode.fail "not ENTER"
in
on "keydown" (Decode.andThen isEnter (Decode.field "keyCode" Decode.int))
viewFilters : Filter -> Html Msg
viewFilters currentFilter =
div [ class "filters" ]
[ filterButton All currentFilter "All"
, filterButton Active currentFilter "Active"
, filterButton Completed currentFilter "Completed"
]
filterButton : Filter -> Filter -> String -> Html Msg
filterButton filter currentFilter label =
button
[ classList [ ( "active", filter == currentFilter ) ]
, onClick (SetFilter filter)
]
[ text label ]
viewTasks : Model -> Html Msg
viewTasks model =
let
filteredTasks =
filterTasks model.filter model.tasks
in
if List.isEmpty model.tasks then
p [ class "empty-message" ] [ text "No tasks yet. Add one above!" ]
else if List.isEmpty filteredTasks then
p [ class "empty-message" ] [ text "No tasks match this filter." ]
else
ul [ class "task-list" ]
(List.map viewTask filteredTasks)
filterTasks : Filter -> List Task -> List Task
filterTasks filter tasks =
case filter of
All ->
tasks
Active ->
List.filter (\t -> not t.completed) tasks
Completed ->
List.filter .completed tasks
viewTask : Task -> Html Msg
viewTask task =
li [ classList [ ( "completed", task.completed ) ] ]
[ input
[ type_ "checkbox"
, checked task.completed
, onClick (ToggleTask task.id)
]
[]
, span [ class "task-description" ] [ text task.description ]
, button
[ class "delete-btn"
, onClick (DeleteTask task.id)
]
[ text "x" ]
]
viewFooter : Model -> Html Msg
viewFooter model =
let
activeCount =
List.length (List.filter (\t -> not t.completed) model.tasks)
completedCount =
List.length (List.filter .completed model.tasks)
itemWord =
if activeCount == 1 then
"item"
else
"items"
in
if List.isEmpty model.tasks then
text ""
else
div [ class "footer" ]
[ span []
[ text (String.fromInt activeCount ++ " " ++ itemWord ++ " left")
]
, if completedCount > 0 then
button [ onClick ClearCompleted ]
[ text "Clear completed" ]
else
text ""
]
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions _ =
loadTasks LoadedTasks
-- MAIN
main : Program Decode.Value Model Msg
main =
Browser.element
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}
```
## The HTML File with JavaScript
Create `index.html`:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Task Manager</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 40px 20px;
}
.container {
max-width: 500px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
padding: 30px;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
font-size: 2rem;
}
.input-section {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.input-section input {
flex: 1;
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.2s;
}
.input-section input:focus {
outline: none;
border-color: #667eea;
}
.input-section button {
padding: 12px 24px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: background 0.2s;
}
.input-section button:hover {
background: #5a67d8;
}
.filters {
display: flex;
gap: 10px;
margin-bottom: 20px;
justify-content: center;
}
.filters button {
padding: 8px 16px;
border: 2px solid #e0e0e0;
background: white;
border-radius: 20px;
cursor: pointer;
transition: all 0.2s;
}
.filters button:hover {
border-color: #667eea;
}
.filters button.active {
background: #667eea;
border-color: #667eea;
color: white;
}
.task-list {
list-style: none;
}
.task-list li {
display: flex;
align-items: center;
padding: 15px;
border-bottom: 1px solid #f0f0f0;
transition: background 0.2s;
}
.task-list li:hover {
background: #f9f9f9;
}
.task-list li.completed .task-description {
text-decoration: line-through;
color: #999;
}
.task-list input[type="checkbox"] {
width: 20px;
height: 20px;
margin-right: 15px;
cursor: pointer;
}
.task-description {
flex: 1;
font-size: 16px;
}
.delete-btn {
background: none;
border: none;
color: #ff6b6b;
font-size: 18px;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s;
}
.task-list li:hover .delete-btn {
opacity: 1;
}
.empty-message {
text-align: center;
color: #999;
padding: 40px;
font-style: italic;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #f0f0f0;
color: #666;
font-size: 14px;
}
.footer button {
background: none;
border: none;
color: #999;
cursor: pointer;
text-decoration: underline;
}
.footer button:hover {
color: #333;
}
</style>
</head>
<body>
<div id="app"></div>
<script src="elm.js"></script>
<script>
// Load saved tasks from localStorage
const savedTasks = localStorage.getItem('elm-tasks');
const initialTasks = savedTasks ? JSON.parse(savedTasks) : [];
// Initialize Elm app with saved tasks
const app = Elm.Main.init({
node: document.getElementById('app'),
flags: initialTasks
});
// Listen for save commands from Elm
app.ports.saveTasks.subscribe(function(tasks) {
localStorage.setItem('elm-tasks', JSON.stringify(tasks));
});
</script>
</body>
</html>
```
## Building and Running
```bash
# Compile Elm to JavaScript
elm make src/Main.elm --output=elm.js
# Open index.html in your browser
# On macOS: open index.html
# On Linux: xdg-open index.html
# On Windows: start index.html
# Or use a local server
python -m http.server 8000
# Then open http://localhost:8000
```
## Understanding Ports
Ports allow Elm to communicate with JavaScript for side effects Elm can't handle natively.
### Defining Ports
```elm
port module Main exposing (main)
-- Outgoing: Elm -> JavaScript
port saveTasks : Encode.Value -> Cmd msg
-- Incoming: JavaScript -> Elm
port loadTasks : (Decode.Value -> msg) -> Sub msg
```
### JavaScript Side
```javascript
// Subscribe to Elm's outgoing port
app.ports.saveTasks.subscribe(function(tasks) {
localStorage.setItem('elm-tasks', JSON.stringify(tasks));
});
// Send data to Elm's incoming port
app.ports.loadTasks.send(someData);
```
### Flags: Initial Data
```elm
-- Elm receives flags in init
init : Decode.Value -> ( Model, Cmd Msg )
init flags =
-- Decode flags (initial tasks from localStorage)
```
```javascript
// JavaScript passes flags at startup
Elm.Main.init({
node: document.getElementById('app'),
flags: initialTasks
});
```
## Code Organization Tips
### For Larger Apps
```
src/
├── Main.elm # Entry point
├── Model.elm # Model type and init
├── Update.elm # Update function and messages
├── View.elm # View functions
├── Types.elm # Shared types
├── Api.elm # HTTP requests
└── Ports.elm # Port declarations
```
### Splitting the Model
```elm
-- Types.elm
module Types exposing (..)
type alias Task =
{ id : Int
, description : String
, completed : Bool
}
type Filter
= All
| Active
| Completed
```
### Helper Functions
Extract common patterns:
```elm
-- Helpers.elm
module Helpers exposing (..)
updateTask : Int -> (Task -> Task) -> List Task -> List Task
updateTask targetId transform tasks =
List.map
(\task ->
if task.id == targetId then
transform task
else
task
)
tasks
```
## Testing Your Elm Code
### Install elm-test
```bash
npm install -g elm-test
elm-test init
```
### Writing Tests
```elm
-- tests/TaskTests.elm
module TaskTests exposing (..)
import Expect
import Test exposing (..)
import Main exposing (filterTasks, Filter(..))
suite : Test
suite =
describe "Task filtering"
[ test "All filter shows all tasks" <|
\_ ->
let
tasks =
[ { id = 1, description = "Task 1", completed = False }
, { id = 2, description = "Task 2", completed = True }
]
in
filterTasks All tasks
|> List.length
|> Expect.equal 2
, test "Active filter shows only incomplete" <|
\_ ->
let
tasks =
[ { id = 1, description = "Task 1", completed = False }
, { id = 2, description = "Task 2", completed = True }
]
in
filterTasks Active tasks
|> List.length
|> Expect.equal 1
]
```
### Running Tests
```bash
elm-test
```
## Exercises
### Exercise 7.1: Add Edit Functionality
Add the ability to edit task descriptions:
1. Double-click a task to edit it
2. Press Enter to save
3. Press Escape to cancel
<details>
<summary>Hint</summary>
Add a new field to the model:
```elm
type alias Model =
{ tasks : List Task
, newTask : String
, nextId : Int
, filter : Filter
, editing : Maybe Int -- ID of task being edited
, editText : String
}
```
Add new messages:
```elm
type Msg
= ...
| StartEditing Int String
| UpdateEditText String
| SaveEdit
| CancelEdit
```
</details>
### Exercise 7.2: Add Due Dates
Extend tasks to have optional due dates:
1. Add a date input when creating tasks
2. Display due dates with tasks
3. Highlight overdue tasks
### Exercise 7.3: Add Categories
Add task categories:
1. Tasks can be tagged with a category
2. Filter by category
3. Add color coding
## Key Takeaways from This Tutorial
1. **Elm is purely functional** - All functions are pure, side effects are managed
2. **Types are your friend** - They catch bugs at compile time
3. **The Elm Architecture** - Model, View, Update provides clean structure
4. **Maybe eliminates nulls** - No more "undefined is not a function"
5. **JSON decoders** ensure type-safe API integration
6. **Ports** bridge Elm and JavaScript for browser APIs
## Where to Go From Here
### Resources
- [Official Elm Guide](https://guide.elm-lang.org/) - The canonical resource
- [Elm in Action](https://www.manning.com/books/elm-in-action) - Excellent book
- [Elm Packages](https://package.elm-lang.org/) - Browse available libraries
- [Elm Slack](https://elmlang.herokuapp.com/) - Friendly community
### Practice Projects
1. **Weather App** - Fetch and display weather data
2. **Markdown Preview** - Live markdown editor with preview
3. **Kanban Board** - Drag-and-drop task management
4. **Chat Application** - WebSocket integration with ports
### Advanced Topics to Explore
- **elm-ui** - Layout without CSS
- **elm-spa** - Single Page Applications
- **GraphQL with elm-graphql**
- **Web Components interop**
## Congratulations!
You've completed this Elm tutorial! You now have:
- A solid understanding of functional programming concepts
- Knowledge of Elm's type system
- Experience with The Elm Architecture
- Skills to build real-world applications
The best way to learn more is to build something. Pick a project and start coding!
---
[← Previous: Lesson 6](06-http-json.md) | [Back to README](../README.md)

View File

@@ -0,0 +1,197 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Task Manager - Elm Tutorial Final Project</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 40px 20px;
}
.container {
max-width: 500px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
padding: 30px;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
font-size: 2rem;
}
.input-section {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.input-section input {
flex: 1;
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.2s;
}
.input-section input:focus {
outline: none;
border-color: #667eea;
}
.input-section button {
padding: 12px 24px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: background 0.2s;
}
.input-section button:hover {
background: #5a67d8;
}
.filters {
display: flex;
gap: 10px;
margin-bottom: 20px;
justify-content: center;
}
.filters button {
padding: 8px 16px;
border: 2px solid #e0e0e0;
background: white;
border-radius: 20px;
cursor: pointer;
transition: all 0.2s;
}
.filters button:hover {
border-color: #667eea;
}
.filters button.active {
background: #667eea;
border-color: #667eea;
color: white;
}
.task-list {
list-style: none;
}
.task-list li {
display: flex;
align-items: center;
padding: 15px;
border-bottom: 1px solid #f0f0f0;
transition: background 0.2s;
}
.task-list li:hover {
background: #f9f9f9;
}
.task-list li.completed .task-description {
text-decoration: line-through;
color: #999;
}
.task-list input[type="checkbox"] {
width: 20px;
height: 20px;
margin-right: 15px;
cursor: pointer;
}
.task-description {
flex: 1;
font-size: 16px;
}
.delete-btn {
background: none;
border: none;
color: #ff6b6b;
font-size: 18px;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s;
}
.task-list li:hover .delete-btn {
opacity: 1;
}
.empty-message {
text-align: center;
color: #999;
padding: 40px;
font-style: italic;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #f0f0f0;
color: #666;
font-size: 14px;
}
.footer button {
background: none;
border: none;
color: #999;
cursor: pointer;
text-decoration: underline;
}
.footer button:hover {
color: #333;
}
</style>
</head>
<body>
<div id="app"></div>
<script src="elm.js"></script>
<script>
// Load saved tasks from localStorage
const savedTasks = localStorage.getItem('elm-tasks');
const initialTasks = savedTasks ? JSON.parse(savedTasks) : [];
// Initialize Elm app with saved tasks
const app = Elm.Main.init({
node: document.getElementById('app'),
flags: initialTasks
});
// Listen for save commands from Elm
app.ports.saveTasks.subscribe(function(tasks) {
localStorage.setItem('elm-tasks', JSON.stringify(tasks));
});
</script>
</body>
</html>

View File

@@ -0,0 +1,370 @@
port module Main exposing (main)
{-| Task Manager - Final Project
A complete task manager application demonstrating:
- The Elm Architecture
- Custom types for state management
- Ports for localStorage persistence
- List operations
- JSON encoding/decoding
To run:
1. elm make src/Main.elm --output=elm.js
2. Open index.html in a browser
-}
import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Json.Decode as Decode exposing (Decoder)
import Json.Encode as Encode
-- PORTS
port saveTasks : Encode.Value -> Cmd msg
port loadTasks : (Decode.Value -> msg) -> Sub msg
-- MODEL
type alias Task =
{ id : Int
, description : String
, completed : Bool
}
type Filter
= All
| Active
| Completed
type alias Model =
{ tasks : List Task
, newTask : String
, nextId : Int
, filter : Filter
}
init : Decode.Value -> ( Model, Cmd Msg )
init flags =
let
loadedTasks =
case Decode.decodeValue tasksDecoder flags of
Ok tasks ->
tasks
Err _ ->
[]
nextId =
case List.maximum (List.map .id loadedTasks) of
Just maxId ->
maxId + 1
Nothing ->
1
in
( { tasks = loadedTasks
, newTask = ""
, nextId = nextId
, filter = All
}
, Cmd.none
)
-- DECODERS & ENCODERS
taskDecoder : Decoder Task
taskDecoder =
Decode.map3 Task
(Decode.field "id" Decode.int)
(Decode.field "description" Decode.string)
(Decode.field "completed" Decode.bool)
tasksDecoder : Decoder (List Task)
tasksDecoder =
Decode.list taskDecoder
encodeTask : Task -> Encode.Value
encodeTask task =
Encode.object
[ ( "id", Encode.int task.id )
, ( "description", Encode.string task.description )
, ( "completed", Encode.bool task.completed )
]
encodeTasks : List Task -> Encode.Value
encodeTasks tasks =
Encode.list encodeTask tasks
-- UPDATE
type Msg
= UpdateNewTask String
| AddTask
| ToggleTask Int
| DeleteTask Int
| SetFilter Filter
| ClearCompleted
| LoadedTasks Decode.Value
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
UpdateNewTask text ->
( { model | newTask = text }, Cmd.none )
AddTask ->
if String.isEmpty (String.trim model.newTask) then
( model, Cmd.none )
else
let
newTask =
{ id = model.nextId
, description = String.trim model.newTask
, completed = False
}
newTasks =
model.tasks ++ [ newTask ]
in
( { model
| tasks = newTasks
, newTask = ""
, nextId = model.nextId + 1
}
, saveTasks (encodeTasks newTasks)
)
ToggleTask id ->
let
toggleTask task =
if task.id == id then
{ task | completed = not task.completed }
else
task
newTasks =
List.map toggleTask model.tasks
in
( { model | tasks = newTasks }
, saveTasks (encodeTasks newTasks)
)
DeleteTask id ->
let
newTasks =
List.filter (\task -> task.id /= id) model.tasks
in
( { model | tasks = newTasks }
, saveTasks (encodeTasks newTasks)
)
SetFilter filter ->
( { model | filter = filter }, Cmd.none )
ClearCompleted ->
let
newTasks =
List.filter (\task -> not task.completed) model.tasks
in
( { model | tasks = newTasks }
, saveTasks (encodeTasks newTasks)
)
LoadedTasks value ->
case Decode.decodeValue tasksDecoder value of
Ok tasks ->
( { model | tasks = tasks }, Cmd.none )
Err _ ->
( model, Cmd.none )
-- VIEW
view : Model -> Html Msg
view model =
div [ class "container" ]
[ h1 [] [ text "Task Manager" ]
, viewInput model.newTask
, viewFilters model.filter
, viewTasks model
, viewFooter model
]
viewInput : String -> Html Msg
viewInput newTask =
div [ class "input-section" ]
[ input
[ placeholder "What needs to be done?"
, value newTask
, onInput UpdateNewTask
, onEnter AddTask
]
[]
, button [ onClick AddTask ] [ text "Add" ]
]
onEnter : Msg -> Attribute Msg
onEnter msg =
let
isEnter code =
if code == 13 then
Decode.succeed msg
else
Decode.fail "not ENTER"
in
on "keydown" (Decode.andThen isEnter (Decode.field "keyCode" Decode.int))
viewFilters : Filter -> Html Msg
viewFilters currentFilter =
div [ class "filters" ]
[ filterButton All currentFilter "All"
, filterButton Active currentFilter "Active"
, filterButton Completed currentFilter "Completed"
]
filterButton : Filter -> Filter -> String -> Html Msg
filterButton filter currentFilter label =
button
[ classList [ ( "active", filter == currentFilter ) ]
, onClick (SetFilter filter)
]
[ text label ]
viewTasks : Model -> Html Msg
viewTasks model =
let
filteredTasks =
filterTasks model.filter model.tasks
in
if List.isEmpty model.tasks then
p [ class "empty-message" ] [ text "No tasks yet. Add one above!" ]
else if List.isEmpty filteredTasks then
p [ class "empty-message" ] [ text "No tasks match this filter." ]
else
ul [ class "task-list" ]
(List.map viewTask filteredTasks)
filterTasks : Filter -> List Task -> List Task
filterTasks filter tasks =
case filter of
All ->
tasks
Active ->
List.filter (\t -> not t.completed) tasks
Completed ->
List.filter .completed tasks
viewTask : Task -> Html Msg
viewTask task =
li [ classList [ ( "completed", task.completed ) ] ]
[ input
[ type_ "checkbox"
, checked task.completed
, onClick (ToggleTask task.id)
]
[]
, span [ class "task-description" ] [ text task.description ]
, button
[ class "delete-btn"
, onClick (DeleteTask task.id)
]
[ text "x" ]
]
viewFooter : Model -> Html Msg
viewFooter model =
let
activeCount =
List.length (List.filter (\t -> not t.completed) model.tasks)
completedCount =
List.length (List.filter .completed model.tasks)
itemWord =
if activeCount == 1 then
"item"
else
"items"
in
if List.isEmpty model.tasks then
text ""
else
div [ class "footer" ]
[ span []
[ text (String.fromInt activeCount ++ " " ++ itemWord ++ " left")
]
, if completedCount > 0 then
button [ onClick ClearCompleted ]
[ text "Clear completed" ]
else
text ""
]
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions _ =
loadTasks LoadedTasks
-- MAIN
main : Program Decode.Value Model Msg
main =
Browser.element
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}

View File

@@ -0,0 +1,85 @@
module Main exposing (main)
{-| Counter Solution
This is the solution to Exercise 2 with:
- Reset button
- Double button
- Prevents count from going below 0
-}
import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
-- MODEL
type alias Model =
{ count : Int
}
init : Model
init =
{ count = 0
}
-- UPDATE
type Msg
= Increment
| Decrement
| Reset
| Double
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
{ model | count = model.count + 1 }
Decrement ->
-- Prevent going below 0
{ model | count = max 0 (model.count - 1) }
Reset ->
{ model | count = 0 }
Double ->
{ model | count = model.count * 2 }
-- VIEW
view : Model -> Html Msg
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (String.fromInt model.count) ]
, button [ onClick Increment ] [ text "+" ]
, button [ onClick Reset ] [ text "Reset" ]
, button [ onClick Double ] [ text "Double" ]
]
-- MAIN
main : Program () Model Msg
main =
Browser.sandbox
{ init = init
, update = update
, view = view
}

View File

@@ -0,0 +1,124 @@
module Main exposing (main)
{-| Temperature Converter Solution
Converts between Celsius and Fahrenheit
-}
import Browser
import Html exposing (Html, div, input, text)
import Html.Attributes exposing (placeholder, value)
import Html.Events exposing (onInput)
-- MODEL
type alias Model =
{ celsius : String
, fahrenheit : String
}
init : Model
init =
{ celsius = ""
, fahrenheit = ""
}
-- UPDATE
type Msg
= UpdateCelsius String
| UpdateFahrenheit String
update : Msg -> Model -> Model
update msg model =
case msg of
UpdateCelsius value ->
{ model
| celsius = value
, fahrenheit =
case String.toFloat value of
Just c ->
String.fromFloat (celsiusToFahrenheit c)
Nothing ->
""
}
UpdateFahrenheit value ->
{ model
| fahrenheit = value
, celsius =
case String.toFloat value of
Just f ->
String.fromFloat (fahrenheitToCelsius f)
Nothing ->
""
}
-- HELPER FUNCTIONS
celsiusToFahrenheit : Float -> Float
celsiusToFahrenheit c =
c * 9 / 5 + 32
fahrenheitToCelsius : Float -> Float
fahrenheitToCelsius f =
(f - 32) * 5 / 9
-- VIEW
view : Model -> Html Msg
view model =
div []
[ div []
[ input
[ placeholder "Celsius"
, value model.celsius
, onInput UpdateCelsius
]
[]
, text " °C"
]
, div []
[ text " = "
]
, div []
[ input
[ placeholder "Fahrenheit"
, value model.fahrenheit
, onInput UpdateFahrenheit
]
[]
, text " °F"
]
]
-- MAIN
main : Program () Model Msg
main =
Browser.sandbox
{ init = init
, update = update
, view = view
}