funtil
Types
This type is used to represent a value that can never happen. What does that mean exactly?
- A
Boolis a type that has two values:TrueandFalse. Nilis a type that has one value:Nil.Neveris a type that has zero values: it’s impossible to construct!
Why this type is useful is a bit of a mind-bender, but it’s a useful tool to
have in your toolbox. For example, if you have a function that returns a
Result(Int, Never) then you know that it will always return an Int and
it is safe to use let assert to unwrap the value.
pub opaque type Never
Values
pub fn fix(f: fn(fn(a) -> b, a) -> b) -> fn(a) -> b
Gleam’s type system does not support recursive let-bound functions, even
though they are theoretically possible. The fix combinator is a sneaky way
around this limitation by making the recursive function a parameter of
itself.
Sound a bit too magical? Let’s first take a look at what happens if we try
to write a recursive let-bound function in Gleam:
pub fn example() {
let factorial = fn(x) {
case x {
0 -> 1
x -> x * factorial(x - 1)
// ^^^^^^^^^ The name `factorial` is not in scope here.
}
}
assert fact(5) == 120
}
We get a compile error because the name factorial is not in scope inside
the function body. What does it look like if we try to use fix?
import funtil
pub fn example() {
let factorial =
funtil.fix(fn(factorial, x) {
case x {
0 -> 1
x -> x * factorial(x - 1)
}
})
assert fact(5) == 120
}
🚨 Gleam is designed with this limitation to encourage you to pull things out
of let bindings when they get too complex. If you find yourself reaching for
fix, consider if there’s a clearer way to solve your problem.
pub fn fix2(f: fn(fn(a, b) -> c, a, b) -> c) -> fn(a, b) -> c
A version of the fix util for functions that take two arguments.
🚨 Gleam is designed with this limitation to encourage you to pull things out
of let bindings when they get too complex. If you find yourself reaching for
fix2, consider if there’s a clearer way to solve your problem.
pub fn fix3(
f: fn(fn(a, b, c) -> d, a, b, c) -> d,
) -> fn(a, b, c) -> d
A version of the fix util for functions that take three arguments.
🚨 Gleam is designed with this limitation to encourage you to pull things out
of let bindings when they get too complex. If you find yourself reaching for
fix3, consider if there’s a clearer way to solve your problem.
pub fn never(val: Never) -> a
If you’ve got a Never somewhere in a type it can be a bit of a problem if
you need something else. Just like Never is a type that can never
be constructed, never is a function that can never be called.
To take our Result(Int, Never) example from above, what if we want to
pass that value into a function that expects a Result(Int, String)? As it
stands, the types don’t match up, but because we know that we can never have
an error, we can use never to pretend to convert it into a String:
import funtil.{Never, never}
import gleam/io
import gleam/result
fn log_error(result: Result(a, String)) -> Result(a, String) {
case result {
Ok(a) -> Nil
Error(message) -> io.println(message)
}
result
}
fn example() {
let val: Result(Int, Never) = Ok(42)
val
|> result.map_error(never)
|> log_error
}
pub fn then(
first: fn(a) -> b,
do second: fn(b) -> c,
) -> fn(a) -> c
Compose two functions together, where the output of the first function is passed as the input to the second function. This is known as composition and can be a way to write code in a “point-free” style where you don’t explicitly mention function arguments.
import funtil
import gleam/list
import gleam/string
fn example() {
let shouty_names =
list.map(
["yoshie", "danielle", "marniek"],
string.uppercase |> funtil.then(string.append(_, "!"))
)
assert shouty_names == ["YOSHIE!", "DANIELLE!", "MARNIEK!"]
}
🚨 Gleam intentionally doesn’t have an operator for function composition. In
other languages this is often represented as . or >>. Point-free programming
can be a useful tool but it can also make code harder to understand, hold with
care!