N

Nanyx

Documentation

Contexts

Contexts in Nanyx provide a way to handle effects explicitly without threading parameters through every function. They're Nanyx's take on algebraic effect handlers, designed to be simple yet powerful.

Why Contexts?

In functional programming, we want to separate pure code from code that has side effects. Contexts make this separation explicit while keeping your code clean and composable.

Defining a Context

A context is defined similarly to a type, but represents ambient capabilities:

context Console = (
  println: string -> ()
  readLine: () -> string
)

This defines what capabilities are available in a Console context, without specifying how they're implemented.

Using Contexts in Functions

Functions can declare that they need a context:

def greet: <Console> string -> () = { name ->
  println("Hello, {name}!")
  println("How are you?")
  def response = readLine()
  println("You said: {response}")
}

The <Console> annotation means this function requires a Console context to run.

Providing Context Implementations

When calling a function that requires a context, you provide the implementation:

def runGreeting = {
  use Console(
    println = { s -> -- actual console output }
    readLine = { -- actual console input }
  )
  
  greet("Alice")
}

State Effect Example

Contexts can manage state:

context State(a) = (
  get: () -> a
  set: a -> ()
)

def counter: <State(int)> () -> int = {
  def current = get()
  set(current + 1)
  current
}

def runCounter = {
  state(10) {  -- Initialize state with 10
    counter()  -- Returns 10, sets state to 11
    counter()  -- Returns 11, sets state to 12
  }
}

Exception Handling via Contexts

Contexts can model exception handling:

context Raise = (
  ctx raise: string -> a
)

def safeDivide: <Raise> (int, int) -> int = { x, y ->
  if y == 0 -> raise("Division by zero")
  else -> x / y
}

def handleDivision = {
  use Raise(
    ctx raise = { msg -> 
      dbg("Error: {msg}")
      resume 0  -- resume with default value
    }
  )
  
  safeDivide(10, 0)  -- Returns 0 instead of crashing
}

Composing Contexts

Functions can require multiple contexts:

def processUser: <Console, Database, Logger> User -> () = { user ->
  log("Processing user {user.id}")
  def data = queryDatabase(user.id)
  println("Found user: {data.name}")
}

Context Benefits

Explicit Effects

Contexts make it clear when a function has side effects:

-- Pure function - no effects
def double: int -> int = { x -> x * 2 }

-- Effectful function - requires Console
def interactive: <Console> () -> () = { 
  println("Hello!") 
}

Testability

By providing different implementations, you can easily test effectful code:

-- Production implementation
use Console(
  println = { s -> -- real console output }
)

-- Test implementation
use Console(
  println = { s -> testBuffer.append(s) }
)

Separation of Concerns

Contexts let you separate what effects you need from how they're implemented. Your business logic can focus on what it does, while the implementation details are handled elsewhere.

Advanced: Custom Handlers

You can create sophisticated effect handlers with contexts:

context Async(a) = (
  await: Promise(a) -> a
  async: (() -> a) -> Promise(a)
)

def fetchUser: <Async> UserId -> User = { id ->
  def promise = Http.get("/users/{id}")
  await(promise)
}

Contexts vs Monads

If you're familiar with Haskell or other functional languages, contexts are similar to monad transformers but with a cleaner syntax and easier composition. They provide the power of algebraic effects without the complexity.