casserole

Source   Edit  

This library supports making sum types without needing a separate enum like with object variants which gives multiple benefits of

  • Being able to reuse field names
  • Safely unpacking fields
  • Pattern matching for quick comparisons

Getting Started

Types are created via the cased macro which supports both creating sum types from object declarations along with a shorthand for single field sumtypes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import casserole

type
  IntOrString {.cased.} = object
    case
    of Int:
      val: int
    of String:
      # Notice that field names can be shared
      val: string
  # Short hand syntax if you just need a single anonymous field
  IntOrStringShort {.cased.} = tuple
    Int: int
    String: string
You can then construct these objects using Object.Tag(fields...) syntax. (See `.()` for more details)
15
16
17
let
  isInt = IntOrString.Int(9)
  isString = IntOrString.String("Hello")

Pattern Matching

To safely use these values, you use pattern matching via ?=, ?==, and `case`. Patterns are in the form Tag(field, ...) where Tag is the discriminator and field forms a pattern e.g.

  • _: This matches anything, also doesn't put a variable into scope
  • foo: This matches anything, but also puts a variable foo into scope with that value
  • "someValue": This matches the exact value "someValue" (could be any expression), doesn't put anything into scope
18
19
20
21
22
23
24
25
26
27
28
Int(_) ?= isInt # Be very careful with `?=`, it throws a defect if you're wrong

# `?==` is safer since it only runs the if statement if it matched the pattern
if Int(val) ?== isString:
  echo "It was an integer and it was: ", $val # this will never run tho...

# Case statements are handy if you have lots of patterns
case isInt
of Int(1): echo "The value was 1"
of String(x): echo "It was ", x
of Int(_): echo "Was some other number" # You can have multiple patterns, placement gives precedence

Supporting existing types

A feature of this library is being able to support existing types. For example, you can use std/options with pattern matching

Example:

import casserole
import std/options

let val = some("Hello")

if Some(value) ?== val:
  echo value
The following standard library modules are supported

Examples

For example, we could construct an Result type like so (Though we already have this in the library results: Result)

Example:

import casserole
import casserole
import std/random

type
  # You must attach the `{.cased.}` pragma
  Result*[T, E] {.cased.} = object
    case
    of Ok:
      # Throw your fields into each branch
      value: T
    of Error:
      error: E

# Looks like any other type
proc possiblyFails(): Result[int, string] =
  if sample([true, false]):
    # Constructed by referencing the type and the branch
    Result[int, string].Ok(rand(100))
  else:
    Result[int, string].Error("Failed for some reason =(")

let res = possiblyFails()

# Can extract via pattern matching with `if`/`case` statements
if Ok(randVal) ?== res:
  echo "We got: " & $randVal

# Case statements check for exhaustiveness
case res
of Ok(randVal):
  echo "Was something"
of Error(error):
  echo "It failed"

Types

CasedObject[D; R] = concept
    proc currentBranch(val: Self): D ## This must return the current discriminator value for an object
    proc getBranch(val: Self; branch: static[D]): R ## This must return the value of a branch. Can raise a fieldDefect if in wrong state
This gets implemented for custom types to integrate them into cased pattern matching. Source   Edit  
CaseObject[D] = object of RootObj
Type class for accepting a case object. D is the discriminator for the fields Source   Edit  

Procs

proc `==`[T: CaseObject](left, right: T): bool
Compares two CaseObject and considers them equal if they have the same branch and values Source   Edit  

Macros

macro `.()`(obj: typedesc; tag: untyped; values: varargs[untyped]): untyped
This is used for constructor a cased object. Construction is done in the form Object.Tag(params...) Source   Edit  
macro `?=`(lhs: untyped; rhs: untyped): untyped
Unpacks a cased object into an expected type. Raises a field defect if its the wrong type

Example:

import std/options

let opt = some(1)
# Inserts `val` into scope
Some(val) ?= opt
assert val == opt.get()
Source   Edit  
macro `?==`(lhs: untyped; rhs: untyped): bool
Like ?= except doesn't raise an error. This is meant to be used inside if statements for unpacking a value. Variables will still be added to scope for wrong branch, but won't be initialised

Example:

import std/options
let opt = none(string)

# Won't error if wrong branch
if Some(value) ?== opt:
  # `value` can only be accessed inside if statement
  echo "Great, we have " & $value
Source   Edit  
macro `case`(n: CasedObject | CaseObject): untyped
Macro that adds support for pattern matching via case statement/expression. This supports the same syntax as ?=

Example:

import std/options

case some(9)
of Some(_): echo "Have something"
of None(): echo "Have nothing"
Source   Edit  
macro cased(inp: untyped): untyped
Generates a case class object based on RFC#559

Example:

type
  Optional[T] {.cased.} = object
    case
    of Some:
      value: T
    of None: discard
There is also a shorthand syntax using tuples if you don't need multiple fields e.g. This is equivilant notation

Example:

type
  Optional[T] {.cased.} = tuple[
    Some: T,
    None: nil
  ]
Source   Edit  
macro unrollEnum(body: ForLoopStmt): untyped
Macro that unrolls an enum and runs the loop body for each instance. This is unrolled at compile time, so each iteration gets a static version of the value. Be careful with large enums since the body is copied for each enum value Source   Edit  

Templates

template currentBranch[D; T: CaseObject[D]](c: T): D
Returns the current state that a CaseObject is in Source   Edit  
template getBranch[T: CaseObject](c: T; branch: untyped): tuple
Generic function that gets the branch value for any CaseObject Source   Edit