Nanyx

Option chaining

Implement a short-circuiting workflow for option-like values

Let’s build our understanding of workflows by implementing a simple option-chaining workflow. We want a workflow that allows us to chain together computations that may return #some(a) | #none, short-circuiting if any step returns #none.

Setup

nanyx new option_chaining
cd option_chaining

The code

A workflow is a record of keyword methods. We start by declaring the shape of that record as a context, so that functions can require it:

context OptionChain = (
  try: (#some(a) | #none, a -> #some(b) | #none) -> #some(b) | #none
  return: a -> #some(a) | #none
  zero: #some(a) | #none
)

Then we provide the implementation:

@workflow -- This attribute will give you a helper function for free
def optionChain: OptionChain = (
  -- This is how we get the short-circuiting behavior for options.
  -- Notice how we don't use the continuation for the `#none` case, so
  -- the workflow ignores the rest of the block
  try = {
    | #some(value), cont -> cont(value)
    | #none, _ -> #none
  }

  -- `return` wraps the block's final value
  return = { value -> #some(value) }

  -- `zero` is the value of a block that produces nothing: an `if` with no
  -- `else`, or a block that falls off the end without a `return`
  zero = #none
)

The @workflow attribute generates a helper function named after the builder. If we didn’t want to use the attribute, we could implement the helper ourselves. Here typeof optionChain is the type of the value optionChain (see typeof), so the lambda is required to run with optionChain loaded as a context:

def maybe: ([typeof optionChain] () -> #some(a) | #none) -> #some(a) | #none = { f ->
  use optionChain in f()
}

Either way, the workflow is used like this:

maybe {
  def a = try someComputation1()
  def b = try someComputation2()

  return a + b
}

If someComputation1() returns #none, the try keyword never calls its continuation, so someComputation2() is never run and the whole block evaluates to #none. Otherwise both values are bound and return produces #some(a + b).

For the general model behind this, see Workflows.