Unions

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:

Note that this means it will return one of the two, not both.

Variant-Based Branching

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:

mod example

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()
}
}
}
Should a union be handled like this, it is enforced that it may only have handled variants. In other words - unhandled union variants will always result in an error. Adding an else-branch will handle all other union variants:
mod example

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
}
}

Creating Union Values

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.

mod example

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
}
In the above code, the procedure can either return the variant some with a string value or the variant none with the unit value. This means that the procedure returns the union #some str | #none unit.

Variant Unwrap Operator

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:

mod example

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()

Equality

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.

mod example

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'
}

Optional Values

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

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.