Unions in Gera are used to represent one of multiple different types. Imagine them as enums, but every enum variant holds a single value, and the types of the values may also differ.
Let's consider std::io::read_dir, which is a procedure that takes a path string and attempts to read the contents of the directory at that path. It's return type is #ok [str] | #err str, which means it's a union of:
- the variant ok, having a value of type [str] (array of string)
- the variant err, having a value of type str (string)
Just like case can be used to execute a specific branch based on some value, it can also be used to execute a specific branch based on the variant of some union value. For example, we can use it to handle both possible variants of the return value of std::io::read_dir:
use std::io::println
proc main() {
case std::io::read_dir("test") {
#err error -> {
"Unable to read directory: "
|> concat(error)
|> println()
}
#ok contents -> {
"Directory has "
|> concat(as_str(length(contents)))
|> concat(" items")
|> println()
}
}
}
use std::io::println
// allows any union variant
// type of 'pet' would be
// '(#cat { hunger = float, ... } | #dog { volume = float, ... } | ...)'
// ('...' means that the variant / object may have more members / variants)
proc feed(pet) {
case pet {
#cat c -> c.hunger = 0.0
#dog d -> d.volume = 1.0
// you can also not specify a variable for the value to ignore it
#dodo -> println("wtf how do you have that")
} else {
// else nothing to do
}
}
Unions can be created by simply instantiating variants using the #name value syntax. The union type is then the sum of all the possible union variants.
proc weekday(n) {
case n {
0 -> return #some "Monday"
1 -> return #some "Tuesday"
2 -> return #some "Wednesday"
3 -> return #some "Thursday"
4 -> return #some "Friday"
5 -> return #some "Saturday"
6 -> return #some "Sunday"
} else return #none unit
}
The question mark operator can be used to make the assumption that a union value has some specific variant. If it is not that variant, the union value will be returned immediately. If it is that variant, the expression results in the value of that variant. For example, let's say we want to read a file, parse the contents as a float, compute the square root and return it:
proc file_sqrt(path) = path
// return type: '#ok str | #err str'
|> std::io::read_file()
// return type: '#some str | #none unit'
|> std::res::get_ok()
// get the value or return early
?some
// return type: '#some float | #none unit'
|> std::str::parse_flt()
// get the value or return early
?some
|> std::math::sqrt()
Using == with union values will first check if they are the same variant and if they are compare their values using ==. To only check if the variants match (without checking the value) you may use the built-in tag_eq-procedure.
use std::io::println
proc main() {
val a = #cat { name = "Snowball", hunger = 0.2 }
val b = #cat { name = "Cookie", hunger = 0.6 }
val c = #dog { name = "Rex", volume = 3.0 }
val d = #cat { name = "Snowball", hunger = 0.2 }
println(a == b) // prints 'false'
println(a == c) // prints 'false'
println(a == d) // prints 'true'
println(tag_eq(a, b)) // prints 'true'
println(tag_eq(a, c)) // prints 'false'
println(tag_eq(a, d)) // prints 'true'
}
Just like demonstrated above, one can use a union to represent optional values. By convention an optional value is simply of type #some T | #none unit (where T is the actual type of the value). The std::opt-module provides general procedures for working with these.
Result values are values that can either be an actual result value or an error value. By convention a result value is simply of type #ok V | #err E (where V is the actual value type and E the error type). The std::res-module provides general procedures for working with these.