This tutorial covers the built-in functional programming (FP) traits. These traits form a hierarchy of abstractions for composable data transformations.
Note: FP traits are built-in and always available. No import required!
Semigroup
↓
Monoid
Functor
↓
Applicative
↓
Monad
Each level builds upon the previous, adding more capabilities.
A Semigroup provides an associative binary operation <> that combines two values of the same type.
The built-in trait is defined as:
trait Semigroup<A> {
operator (<>)(a: A, b: A) -> A
}
The <> operation must be associative:
(a <> b) <> c == a <> (b <> c)
List: Concatenation
[1, 2, 3] <> [4, 5, 6] // [1, 2, 3, 4, 5, 6]Option: First non-None wins
Some(10) <> Some(20) // Some(10)
Some(10) <> None // Some(10)
None <> Some(20) // Some(20)
None <> None // Nonetype Text = MkText String
instance Semigroup Text {
operator (<>)(a: Text, b: Text) -> Text {
match (a, b) {
(MkText x, MkText y) -> MkText(x ++ y)
}
}
}
t1 = MkText("Hello")
t2 = MkText(" ")
t3 = MkText("World")
result = t1 <> t2 <> t3
// Note: We need a helper to extract the string because
// we can't pattern match on Text directly in print()
fun getText(t) { match t { MkText s -> s } }
print(getText(result)) // Hello WorldA Monoid is a Semigroup with an identity element mempty.
trait Monoid<A> : Semigroup<A> {
fun mempty() -> A
}
- Left identity:
mempty <> x == x - Right identity:
x <> mempty == x
List: Empty list is identity
mempty: List<Int> // []
[] <> [1, 2, 3] // [1, 2, 3]
[1, 2, 3] <> [] // [1, 2, 3]Option: None is identity
mempty: Option<Int> // None
None <> Some(42) // Some(42)type Text = MkText String
fun getText(t) { match t { MkText s -> s } }
instance Semigroup Text {
operator (<>)(a: Text, b: Text) -> Text {
match (a, b) {
(MkText x, MkText y) -> MkText(x ++ y)
}
}
}
instance Monoid Text {
fun mempty() -> Text { MkText("") }
}
// Type annotation needed for dispatch
t: Text = mempty()
print(getText(t)) // ""A Functor is a type constructor that can be mapped over with a function.
trait Functor<F> {
fun fmap<A, B>(f: (A) -> B, fa: F<A>) -> F<B>
}
- Identity:
fmap(id, x) == x - Composition:
fmap(f ,, g, x) == fmap(f, fmap(g, x))
List: Map over each element
fmap(fun(x) -> x * 2, [1, 2, 3, 4, 5]) // [2, 4, 6, 8, 10]Option: Map over Some, None stays None
fmap(fun(x) -> x * 2, Some(10)) // Some(20)
fmap(fun(x) -> x * 2, None) // NoneResult: Map over Ok, Fail stays Fail
fmap(fun(x) -> x + 100, Ok(5)) // Ok(105)
fmap(fun(x) -> x + 100, Fail("error")) // Fail("error")// Identity law
idFn = fun(x) -> x
print(fmap(idFn, [1, 2, 3]) == [1, 2, 3]) // true
// Composition law
inc = fun(x) -> x + 1
dbl = fun(x) -> x * 2
composed = dbl ,, inc // dbl(inc(x))
opt = Some(5)
print(fmap(composed, opt) == fmap(dbl, fmap(inc, opt))) // trueAn Applicative functor adds the ability to lift values and apply wrapped functions.
trait Applicative<F> : Functor<F> {
fun pure<A>(x: A) -> F<A>
operator (<*>)<A, B>(ff: F<(A) -> B>, fa: F<A>) -> F<B>
}
- Identity:
pure(id) <*> v == v - Homomorphism:
pure(f) <*> pure(x) == pure(f(x)) - Interchange:
u <*> pure(y) == pure(fun(f) -> f(y)) <*> u - Composition:
pure(,,) <*> u <*> v <*> w == u <*> (v <*> w)
List: Cartesian product application
fns = [fun(x) -> x + 1, fun(x) -> x * 2]
vals = [10, 20]
fns <*> vals // [11, 21, 20, 40] - applies each fn to each valOption: Apply if both are Some
Some(fun(x) -> x + 1) <*> Some(10) // Some(11)
Some(fun(x) -> x + 1) <*> None // None
None <*> Some(10) // NoneResult: Apply if both are Ok
Ok(fun(x) -> x * 2) <*> Ok(5) // Ok(10)
Ok(fun(x) -> x * 2) <*> Fail("e") // Fail("e")
Fail("e1") <*> Fail("e2") // Fail("e1") - first errorThe pure function lifts a value into an applicative context. To use it, you need to specify the target type:
opt: Option<Int> = pure(42) // Some(42)
lst: List<Int> = pure(42) // [42]
res: Result<String, Int> = pure(42) // Ok(42)A Monad extends Applicative with the ability to chain computations that depend on previous results.
trait Monad<M> : Applicative<M> {
operator (>>=)<A, B>(ma: M<A>, f: (A) -> M<B>) -> M<B>
}
- Left identity:
pure(a) >>= f == f(a) - Right identity:
m >>= pure == m - Associativity:
(m >>= f) >>= g == m >>= (fun(x) -> f(x) >>= g)
List: FlatMap (concatMap)
[1, 2, 3] >>= fun(x) -> [x, x * 10]
// [1, 10, 2, 20, 3, 30]Option: Chain computations that may fail
Some(10) >>= fun(x) -> Some(x + 1) // Some(11)
Some(10) >>= fun(_) -> None // None
None >>= fun(x) -> Some(x + 1) // NoneResult: Chain computations with errors
Ok(10) >>= fun(x) -> Ok(x * 2) // Ok(20)
Ok(10) >>= fun(_) -> Fail("error") // Fail("error")
Fail("e") >>= fun(x) -> Ok(x * 2) // Fail("e")// Safe division chain
fun safeDiv(x: Int, y: Int) -> Option<Int> {
if y == 0 { None } else { Some(x / y) }
}
// 100 / 2 / 5 / 2 (chained on single line)
result = Some(100) >>= fun(x) -> safeDiv(x, 2) >>= fun(x) -> safeDiv(x, 5) >>= fun(x) -> safeDiv(x, 2)
print(result) // Some(5)
// Division by zero — returns None immediately
result2 = Some(100) >>= fun(x) -> safeDiv(x, 0) >>= fun(x) -> safeDiv(x, 5)
print(result2) // NoneIn addition to container types (List, Option, Result), the language provides several standard monads for common side-effects.
The simplest monad, just wraps a value. Useful when a monad is expected but you need no extra effects.
// Type: Identity<A>
idVal = identity(42)
res = idVal >>= \x -> identity(x * 2)
print(runIdentity(res)) // 84Encapsulates computation with access to a shared environment.
// Type: Reader<E, A> (where E is environment type)
// Computes value using env (Int)
op = reader(\env -> env * 2)
// Chain: gets env, adds 10
op2 = op >>= \x -> reader(\env -> x + env + 10)
// Run with environment 5
// op: 5 * 2 = 10
// op2: 10 + 5 + 10 = 25
result = runReader(op2, 5)
print(result) // 25Accumulates a log (using Semigroup <>) while computing a value.
// Type: Writer<W, A> (where W is log type, must implement Semigroup)
// writer(value, log) -> Writer<W, A>
// pure(value) -> Writer<W, A> (requires W to implement Monoid for empty log)
// Using explicit Writer constructor
w = writer(10, ["started"])
// Chain: logs message and modifies value
op = w >>= \x ->
// wTell is not built-in, but easy to simulate:
writer((), ["processing " ++ show(x)]) >>= \_ ->
writer(x * 2, ["done"])
// Run
// Result is a Tuple (value, log)
res = runWriter(op)
val = res[0] // 20
log = res[1] // ["started", "processing 10", "done"]
// Using pure with Writer
// Requires explicit type annotation so runtime knows the Log type (W)
// to call the correct mempty() (empty log)
type alias MyLog = List<String>
wPure: Writer<MyLog, Int> = pure(42) // Writer(42, [])Encapsulates stateful computations S -> (A, S).
// Type: State<S, A>
// state(fn) where fn: S -> (A, S)
pop = state(\s -> match s {
[h, ...t] -> (h, t),
[] -> (0, [])
})
push = \x -> state(\s -> ((), [x, ...s]))
// Stack operation: pop, multiply by 10, push back
stackOp = pop >>= \x -> push(x * 10)
// Run with initial stack [1, 2]
// pop 1 -> stack becomes [2]
// push 10 -> stack becomes [10, 2]
res = runState(stackOp, [1, 2])
finalStack = res[1] // [10, 2]Helpers sGet (read state) and sPut (write state) are also available via state primitives.
You can implement FP traits for your own types:
type Box<t> = MkBox t
// Functor instance
instance Functor Box {
fun fmap<a, b>(f: (a) -> b, fa: Box<a>) -> Box<b> {
match fa {
MkBox(x) -> MkBox(f(x))
}
}
}
// Now you can use fmap with Box
fmap(fun(x) -> x * 2, MkBox(21)) // MkBox(42)Using Applicative for parallel validation:
import "lib/list" (length)
type ValidationError = VErr String
// Validation functions returning Option
fun validateName(name: String) -> Option<String> {
if length(name) > 0 { Some(name) } else { None }
}
fun validateAge(age: Int) -> Option<Int> {
if age >= 0 && age < 150 { Some(age) } else { None }
}
// Combine validations
type Person = MkPerson((String, Int))
// Using Applicative to combine (curried function)
fun makePerson(name: String) -> (Int) -> Person {
fun(age: Int) -> MkPerson((name, age))
}
// If both validations pass, we get Some(Person)
validatedPerson = fmap(makePerson, validateName("Alice")) <*> validateAge(30)
print(validatedPerson) // Some(MkPerson("Alice", 30))
// If any fails, we get None
invalidPerson = fmap(makePerson, validateName("")) <*> validateAge(30)
print(invalidPerson) // None| Operator | Trait | Type Signature | Description |
|---|---|---|---|
<> |
Semigroup | A -> A -> A |
Combine two values |
<*> |
Applicative | F<(A -> B)> -> F<A> -> F<B> |
Apply wrapped fn |
>>= |
Monad | M<A> -> (A -> M<B>) -> M<B> |
Bind/flatMap |
| Function | Trait | Type Signature | Description |
|---|---|---|---|
mempty |
Monoid | () -> A |
Identity element |
fmap |
Functor | (A -> B) -> F<A> -> F<B> |
Map over container |
pure |
Applicative | A -> F<A> |
Lift value into F |
Due to Higher-Kinded Types (HKT), some operations require explicit type annotations:
// Type annotation needed for pure
opt: Option<Int> = pure(42)
// fmap and >>= can often infer types from context
fmap(fun(x) -> x + 1, Some(10)) // Works without annotation
Some(10) >>= fun(x) -> Some(x + 1) // Works without annotation- Traits - How traits work in general
- Generics - Generic types and type parameters
- Error Handling - Using Option and Result