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:
485
lessons/02-basics.md
Normal file
485
lessons/02-basics.md
Normal 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)
|
||||
Reference in New Issue
Block a user