Control Flow
Beyond if/else and case, Saga has two more control flow tools that come up
constantly in practice.
do...else
When you have several sequential operations that can each fail, nesting case
expressions gets unwieldy fast:
fun lookup_grade : Int -> Result String String
lookup_grade id =
case (find_user id) {
Err e -> Err e
Ok name -> case (find_score name) {
Err e -> Err e
Ok score -> case (grade score) {
Err e -> Err e
Ok g -> Ok g
}
}
}do...else flattens this. Each line binds a pattern; if the pattern does not
match, control jumps to the else block.
Unlike monadic bind in other languages, do...else is purely
pattern matching and doesn't require a specific type like Result or Maybe.
Any pattern works, on any type, in any combination:
fun lookup_grade : Int -> Result String String
lookup_grade id = do {
Ok user <- find_user id
True <- is_active user
Ok score <- find_score user.name
g <- grade score
Ok g
} else {
Err e -> Err e
}The last line (without <-) is the final return expression if all previous arms were successful. The do and else blocks must return the same type, e.g. a Result or Maybe or other ADT.
The success expression does not have to be Ok. Here the happy path returns an
Int and the else arm returns 0:
fun score_or_zero : Int -> Int
score_or_zero id = do {
Ok name <- find_user id
Ok score <- find_score name
score
} else {
Err _ -> 0
}The else block must be exhaustive: it has to cover every value that could
arrive from a failed binding. The compiler checks this the same way it checks
case expressions.
When different steps can fail in different ways, list all the bail patterns
together in one else block:
fun first_active_grade : Unit -> Result String String
first_active_grade () = do {
Just users <- active_users ()
Just user <- List.head users
Ok score <- find_score user.id
Ok g <- grade score
Ok g
} else {
Nothing -> Err "nothing found"
Err e -> Err e
}List Comprehensions
List comprehensions build new lists from existing ones. The syntax is
[expr | qualifiers]:
# Map
[x * 2 | x <- xs]
# Filter
[x | x <- xs, x > 0]
# Both
[x * 2 | x <- xs, x > 3]Multiple generators produce a cartesian product:
[x + y | x <- [1, 2, 3], y <- [10, 20]]
# [11, 21, 12, 22, 13, 23]let bindings work inside comprehensions:
[y | x <- xs, let y = transform x, y > threshold]Comprehensions desugar in the parser to flat_map, if/else, and let. There
is no special runtime support -- they are syntax sugar for code you could write
by hand.
The effect system provides another form of control flow through the Fail
effect, covered in Effects & Handlers and
Error Handling.