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
8.8 KiB
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
-- 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 - just "number"
const age = 30;
const price = 19.99;
Strings
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:
const name = "Alice";
const poem = `
Roses are red...
`;
"Hello" + " World" // Uses + not ++
Booleans
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
-- 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:
-- 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
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.
-- 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
-- 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:
-- 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 - Mutation causes bugs
const user = { name: "Alice", score: 100 };
doSomething(user);
console.log(user.score); // Who knows? doSomething might have changed it!
-- 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:
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:
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:
type Color
= Red
| Green
| Blue
type Status
= Loading
| Success String
| Error String
Using Custom Types
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 - 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 - 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:
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.
Solution
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
Exercise 2.2: Update a Record
Given this record:
player =
{ name = "Hero"
, health = 100
, score = 0
}
Create a new record where:
- health is reduced to 80
- score is increased to 50
Solution
updatedPlayer =
{ player
| health = 80
, score = 50
}
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.
Solution
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
Exercise 2.4: Maybe Practice
Write a function that takes a Maybe Int and returns the value doubled, or 0 if Nothing:
doubleOrZero : Maybe Int -> Int
Solution
doubleOrZero : Maybe Int -> Int
doubleOrZero maybeNum =
case maybeNum of
Just n ->
n * 2
Nothing ->
0
Key Takeaways
- Elm has distinct Int and Float types - Unlike JavaScript's single "number"
- Type annotations are documentation - They make code self-explanatory
- Records are immutable - Use update syntax
{ record | field = value } - Type aliases create reusable type names AND constructor functions
- Custom types replace string constants and are type-safe
- 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, we'll explore:
- Pure functions
- Higher-order functions
- Currying and partial application
- The pipe operator