N

Nanyx

Documentation

Pattern Matching

Pattern matching is the core of Nanyx control flow. It's a powerful feature that lets you destructure data, test conditions, and handle different cases in a type-safe, exhaustive manner.

Match Expressions

The most common way to pattern match in Nanyx is with a match expression:

def oddOrEven = { n ->
  match n % 2
    | 0 -> "even"
    | _ -> "odd"
}

The _ pattern matches anything, acting as a catch-all.

Matching on Tag Unions

Pattern matching is especially powerful with tag unions:

def describe: Option(int) -> string = { opt ->
  match opt
    | #some(x) -> "Got value: {x}"
    | #none -> "No value"
}

-- Multiple tag variants
def area: Shape -> float = { shape ->
  match shape
    | #circle(r) -> 3.14159 * r * r
    | #rectangle(w, h) -> w * h
    | #triangle(b, h) -> 0.5 * b * h
}

Matching on Records

You can destructure records in pattern matches:

def greet: Person -> string = { person ->
  match person
    | (name = "Alice", age = age) -> "Hi Alice, age {age}!"
    | (name = name, age = a) if a >= 18 -> "Hello adult {name}"
    | (name = name, _) -> "Hi young {name}"
}

Matching on Literals

Match specific values:

def classify: int -> string = { x ->
  match x
    | 0 -> "zero"
    | 1 -> "one"
    | n if n < 0 -> "negative"
    | n if n > 100 -> "large"
    | _ -> "other"
}

Comparison Patterns

Use comparison operators directly in patterns:

def sign: int -> string = { x ->
  match x
    | < 0 -> "negative"
    | > 0 -> "positive"
    | _ -> "zero"
}

List Destructuring

Pattern match on lists with head/tail patterns:

rec sumList: list(int) -> int = {
  | [] -> 0
  | [head, ...tail] -> head + sumList(tail)
}

Guards

Add conditions to patterns with if:

def classifyNumber: int -> string = { 
  | 0 -> "zero"
  | 1..10 -> "small"
  | n if n > 10 -> "really big!"
  | _ -> "unknown"
}

Pattern Matching in Function Definitions

Functions can pattern match directly on their arguments:

rec sumList 
  : list(int) -> int 
  = { 
    | [] -> 0
    | [head, ...tail] -> head + sumList(tail) 
  }

-- Multiple arguments
def divide
  : int, int -> Result(int, #divideByZero)
  = { 
    | _, 0 -> #error(#divideByZero)
    | x, y -> #ok(x / y)
  }

Exhaustiveness

The compiler checks that your patterns are exhaustive - they must cover all possible cases:

-- Compiler error: missing pattern for #none
def bad: Option(int) -> int = { opt ->
  match opt
    | #some(x) -> x
    -- Missing: | #none -> ...
}

Nested Patterns

Patterns can be arbitrarily nested:

def process: list(Option(int)) -> int = { data ->
  match data
    | [#some(x), #some(y), ...] -> x + y
    | [#some(x), #none, ...] -> x
    | _ -> 0
}

Labeled Patterns

Use labels when destructuring to keep the original value:

def [[x, ...xs] list1, [y, ...ys] list2] = [[1,2,3], [4,5,6]]
x      -- => 1
xs     -- => [2,3]
list1  -- => [1,2,3]

Total vs Partial Patterns

Some patterns always match (total), others might fail (partial):

-- Total pattern - always matches
def (x, y) = (4, 5)

-- Partial pattern - compiler error if used in definition
-- def (4, y) = record  -- Error! Use match instead

-- Partial patterns must use match or if
if record is (4, y) ->
  println(y)

Negated patterns

But having to repeat the type name Customer is a bit anti-DRY.

We can instead match against anything except #nil using the not modifier to the pattern:

match getCustomer()
  | (not #nil) customer -> println("Hello, {customer.name}")
  | #nil -> println("No customer found")

But this feels a bit laborious. Nanyx has the concept of custom patterns, which we can use to introduce a some pattern that matches anything except #nil:

pattern Some = not #nil

This feels much better, and the standard library of Nanyx includes such a pattern

Note that the above is a simple pattern alias; it could have been written as

pattern Some = {
  | (not #nil) s -> (true, s)
  | _ -> false
}

It can then be used like this:

match someFunction()
  | #some data -> data
  | #nil -> "no data"

Custom patterns with outputs

Some patterns can have both inputs and outputs; e.g. this regex pattern:

pattern Regex = { r, s ->
  if Regex(r).match(s) ->
    s
  else ->
    #nil
}
def fizzbuzz { range ->
  range \map { i ->
    match i % 3, i % 5
    | 0, 0 -> "fizzbuzz"
    | 0, _ -> "fizz"
    | _, 0 -> "buzz"
    | else   -> string(i)
  }
}

Using a custom pattern, we can reduce the Fizzbuzz example above to:

pattern DivisibleBy | _ = { divisor, x ->
  if x \mod divisor == 0 -> true
  else -> false
}
def fizzbuzz { range ->
  range \map {
    | DivisibleBy(3) & DivisibleBy(5) -> "fizzbuzz"
    | DivisibleBy(3) -> "fizz"
    | DivisibleBy(5) -> "buzz"
    | else -> "{i}"
  }
}

There is also an except keyword, which works like match except that it does not necessarily need to match all cases; any unmatched values are simply returned

type AD = #a | #b | #c | #d

def f: AD -> string = { x ->
  x except 
    | #a -> "a"
    | #b -> "b" -- Since we've only matched #a and #b, #c and #d will be returned as-is
                -- The whole function now has a return value of `string | #c | #d`
}

When using except, if the result has been narrowed down to a single tag from a tag union then it is automatically unwrapped:

def f: (#a(string) | #b) -> string = { x ->
  def s =
    x except 
      | #b -> "No string" 

  -- s is of type string
  -- Notice how the #a has disappeared because it was the only tag remaining
}

The except keyword is especially useful for handling errors. For example, given the function

def getCustomer
  : (id: string) -> #some(Customer) | #notFound 
  = { id -> 
    if ... -> #notFound
    else -> #some(Customer(...))
  }

we can use except to handle the error case in a more functional way:

def f = {
  def customer = getCustomer("some_id") except
    | #notFound -> return #customerDoesNotExist

  ... -- Do something with `customer`
}

Pattern matching is a powerful tool for working with data. It allows you to both test an expression to see if it contains certain data and, at the same, to extract other pieces of data from the expression.

The syntax for pattern matching is to pipe an expression to a series of case arms, like so:

def f = {
  | 1 -> "one"
  | 2 -> "two"
  | 3 -> "three"
  | _ -> "lots" 
}

Active patterns are functions that can be used in a | arm. They are written as a normal function, but with the name encased in brackets

The simplest active pattern is a function that returns a boolean. If the function returns true, the match arm is executed. Patterns can be partial or total

pattern Even | _ = { x -> x % 2 == 0 }

pattern Even | Odd = { x -> 
  if x % 2 == 0 -> Even 
  else -> Odd
}

def f: int -> string = { x -> match x
  | Even -> "even"
  | Odd  -> "odd" -- This pattern is known to be complete, so we don't need a catch-all arm
}

Active patterns can also return a value. This value is then bound to the name of the active pattern in the match arm. This pattern is also partial (indicated by the ?), meaning that it might not match

pattern Regex | _ = { r, x ->
  if Text.regexMatch(r, x) is [y, ...] ->
    Regex(y)
}
def f = { 
  | Regex("^[0-9]+$", d) -> "a number -> {d}"
  | Regex("^[a-z]+$", s) -> "a string -> {s}"
  | _                    -> "something else" 
}

It's also possible to match using an arbitrary function:

match someInt()
| 0               -> "zero"
| { < 5 }         -> "small"
| { in 5..10 }    -> "medium"
| { x -> x > 10 } -> "big"

It's possible in Nanyx to pattern match on a partial tag union. This is what allows many of the standard library functions to act on both an optional value and a result. There's a convention that any tag union that includes a #some(t) variant is considered an "optional T" value and there are many functions that operate on these:

def maybeString: (#some(string) | _) -> string = {
  | #some(s) -> s
  | _ -> "Nothing was found"
}

In the above function the "rest" parameter that represents the rest of the union is unused so we can use the discard syntax _.

We can give it a proper type parameter if we'd like to use it elsewhere in the function signature. For example, in this optional map function:

def mapOptional: (#some(a) | r), (a -> b) -> (#some(b) | r) = {
  | #some(v), f -> #some(f(v))
  | other -> other
}

def x: #some(string) | #nil = ???
def x' = x \mapOptional { s -> s.length } -- x': #some(int) | #nil

def y: #some(int) | #noValue | #infinity = ???
def y' = y \mapOptional { \float } -- y': #some(float) | #noValue | #infinity

def z: #some(Customer) | #err(string) = ???
def z' = z \mapOptional { .id } -- z': #some(CustomerId) | #err(string)

Notice how we have specified that the union of the first argument should include #some(a) but we have placed no limits on what the other tags in the union can be. That allows us to map one of the tags whilst leaving all others as-is.