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
486 lines
8.8 KiB
Markdown
486 lines
8.8 KiB
Markdown
# 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)
|