SagaSaga
Guide

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.