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.