TIL: Importing Constructors in Gleam
Another Exercism exercise. This time round, I was working with the Option type, the equivalent of Maybe
in Elm.
With the following piece of code:
import gleam/bool
import gleam/option.{type Option}
pub type Player {
Player(name: Option(String), level: Int, health: Int, mana: Option(Int))
}
pub fn revive(player: Player) -> Option(Player) {
case player.health {
0 -> Some(Player(..player, health: 100))
|> option.map(fn(p) {
use <- bool.guard(when: p.level >= 10, return: Player(..p, mana: 100))
p
})
_ -> None
}
}
I was getting the following compilation error:
error: Unknown variable
┌─ /src/main.gleam:10:10
│
10 │ 0 -> Some(Player(..player, health: 100))
│ ^^^^
The custom type variant constructor `Some` is not in scope here.
error: Unknown type for record access
┌─ /src/main.gleam:12:33
│
12 │ use <- bool.guard(when: p.level >= 10, return: Player(..p, mana: 100))
│ ^ I don't know what type this is
In order to access a record field we need to know what type it is, but I
can't tell the type here. Try adding type annotations to your function and
try again.
error: Type mismatch
┌─ /src/main.gleam:12:68
│
12 │ use <- bool.guard(when: p.level >= 10, return: Player(..p, mana: 100))
│ ^^^^^^^^^
Expected type:
Option(Int)
Found type:
Int
error: Unknown variable
┌─ /src/main.gleam:15:10
│
15 │ _ -> None
│ ^^^^
The custom type variant constructor `None` is not in scope here.
What happened was that, even if I imported Option
in the second line, I did not import the constructors:
-import gleam/option.{type Option}
+import gleam/option.{type Option, Some, None}
Then, afterward, I will need to fix the mana
to use Some
:
- use <- bool.guard(when: p.level >= 10, return: Player(..p, mana: 100))
+ use <- bool.guard(when: p.level >= 10, return: Player(..p, mana: Some(100)))
Re-reading Type imports, I quote the following:
Unlike functions, Gleam types are commonly imported in an unqualified way.
Since the Some
and None
are constructor functions of the Option
type, I needed to import it since these are not native types of the language.
As for the Result
type, it seems to be native type. Therefore, I did not need to import Ok
or Error
when working with the Result
type.
I refactored my solution for the revive
function to something like:
pub fn revive(player: Player) -> Option(Player) {
case player.health {
0 -> Some(Player(..player, health: 100))
|> option.map(revive_with_full_mana)
_ -> None
}
}
fn revive_with_full_mana(player: Player) -> Player {
use <- bool.guard(when: player.level < 10, return: player)
Player(..player, mana: Some(100))
}
At least if we needed to write tests for revive_with_full_mana
, we can do so instead of chucking it into an inline function in the original implementation. Plus, early return concept from Go, too.
I am still not very sure if the use <- bool.guard
will be readable over time as compared to case
statements (more verbose, more lines of code).