Nanyx

Contexts as effects

A Caesar cipher cracker, then reader and state effects expressed as contexts.

module contextsAsEffects

import nanyx/math

---
Part one: a Caesar cipher and a frequency-analysis cracker, written with
ordinary pure functions and pipes.
---

def letters = 'a'..'z'

def rotateChar: (string, int) -> string = { c, shift ->
  if letters \contains(c) then
    def base = c.codePoint - 'a'.codePoint
    String.fromCodePoint('a'.codePoint + (base + shift) % 26)
  else c
}

def rotate: (string, int) -> string = { s, shift ->
  s \chars \map { c -> rotateChar(c, shift) } \String.join("")
}

-- The letter frequency table for English
def english = [
  8.2, 1.5, 2.8, 4.3, 12.7, 2.2, 2.0, 6.1, 7.0, 0.2, 0.8, 4.0, 2.4,
  6.7, 7.5, 1.9, 0.1, 6.0, 6.3, 9.1, 2.8, 1.0, 2.4, 0.2, 2.0, 0.1
]

def percent: (int, int) -> float = { n, m -> 100.0 * n.toFloat / m.toFloat }

def rotateList: (list(a), int) -> list(a) = { xs, n -> xs \drop(n) ++ xs \take(n) }

-- A frequency table for a string
def freqs: string -> list(float) = { s ->
  def occurs = letters \map { c -> s \chars \count { == c } }
  def total = occurs \sum
  occurs \map { n -> percent(n, total) }
}

-- How well two frequency tables match, according to the chi-square statistic
def chisqr: (list(float), list(float)) -> float = { xs, ys ->
  xs \zip(ys) \map { x, y -> (x - y) ** 2.0 / y } \sum
}

-- Crack a Caesar-encoded string
def crack: string -> string = { s ->
  def table = freqs(s)
  def chitab = (0..25) \map { n -> chisqr(rotateList(table, n), english) }
  def #some(shift) = chitab \indexOf(chitab \min) except #notFound -> #some(0)
  rotate(s, 26 - shift)
}

---
Part two: contexts as effects. A context is an implicit parameter, so a
function that declares `[Ask(int)]` can call `ask()` without being passed it,
and the caller decides what `ask` does with `use`.
---

context Ask(a) = (ask: () -> a)

def addTwice: [Ask(int)] () -> int = { ask() + ask() }

def askConst: () -> int = {
  use Ask(int)(ask = { 21 }) in addTwice()   -- 42
}

def askRandom: [Random] () -> int = {
  use Ask(int)(ask = { randomInt() }) in addTwice()
}

-- State is just another context; the mutation lives in a `memory` scope that
-- never escapes `runState`.
context State(a) = (
  get: () -> a
  put: a -> ()
)

def sumdown: [State(int)] int -> int = { sum ->
  def i = get()
  if i <= 0 then sum
  else
    put(i - 1)
    sumdown(sum + i)
}

def runState: [c] (a, [State(a) & c] () -> b) -> b = { init, action ->
  memory {
    def cell = mut init
    use State(a)(
      get = { cell }
      put = { v -> cell := v }
    ) in action()
  }
}

@entry
def main = {
  println(crack(rotate("nanyx is a fun language", 3)))
  println("{askConst()}")
  println("{runState(10, { sumdown(0) })}")   -- 55
}

This program lives in the repository as examples/Contexts-as-effects.nyx.