@dashkite/katana
v0.4.18
Published
Stack-based composition combinators in JavaScript
Readme
Katana
Combinators for stack-based composition in JavaScript.
import { pipe } from "@dashkite/joy/function"
import { push, pop, poke } from "@dashkite/katana"
pipe [
push -> 1
push -> 2
poke ( x, y ) -> x + y
pop ( z ) -> assert.equal 3, z
( stack ) -> assert.equal stack.length, 0
]Table Of Contents
Installation
Install Katana as @dashkite/katana using your favorite package manager. To use Katana in a browser, import it directly or use your favorite bundler.
Motivation
Functional programming is often promoted as a way to build more reliable software. In particular, function composition promises to make it possible to combine modular and well-tested functions to build more complex systems without sacrificing reliability or ease of maintenance.
In practice, one of the challenges is that composition can be difficult. Function signatures don’t always line up neatly, forcing awkward compromises that may be difficult to understand or reason about. In contrast, imperative code with intermediate state, though perhaps less elegant, may ultimately be more expressive.
Stack-based composition is an attempt to address this challenge by making it easier to compose binary (or non-unary) functions in a point-free style. For example, consider a binary function, encrypt, taking a key and message (the plaintext). We might write this imperatively like this:
encryptWithKeyName = ( message, name ) ->
key = await getKey name
encrypt message, keyIf we wanted to use composition here, we’d need a way to compose encrypt with getKey. However, because encrypt is a binary function, it’s unclear how we might do this.
Stack-based composition solves this problem by composing over a stack, so that both the key and message are available (the top of the stack is the rightmost value, at the end of the array):
encryptWithKeyName = pipe [
pair # [ message, key-name ]
poke getKey # [ message, key ]
swap # [ key, message ]
poke encrypt # [ encrypted-message ]
first # => encrypted-message
]For such a simple function, we might well prefer to use the imperative style. However, for more complex cases, being able to rely strictly on composition makes it easier realize the advantages of functional programming.
API
The core stack combinators—push, pop, peek, and poke—apply a function, using the arity of the function to determine how many elements from the stack to pass into it, and, in the case of pop and poke, to remove from the stack. Applying a unary function will result in passing the top of the stack into the function. Applying a binary function will result in passing the first two elements from the stack into the function, and so on. Other stack operators, such as drop and copy, simply operate on the stack directly.
You can compose function with any composition function from your favorite functional programming library. Stack combinators that take asynchronous functions will return a promise, so your composition function should handle promises if you’re using asynchronous functions.
Reference
push
push f, stack → stack
Call f with k items from the stack, where k is the arity of f. Push the return value onto the stack.
pop
pop f, stack → stack
Call f with k items from the stack, where k is the arity of f. Pops the items from the stack.
poke, replace
poke f, stack → stack
Call f with k items from the stack, where k is the arity of f. Pops the items from the stack. Push the return value onto the stack. Alias: replace.
peek
peek f, stack → stack
Call f with k items from the stack, where k is the arity of f. Leaves the stack unchanged.
drop, discard
drop stack → stack
Pops the stack. Equivalent to pop ( x ) ->
up
up stack → stack
Rotates the items on the stack, pushing items up, while the top of the stack goes to the bottom.
do pipe [
-> [ 1..5 ] # [ 1, 2, 3, 4, 5 ]
up # [ 5, 1, 2, 3, 4 ]
]down
down stack → stack
Rotates the items on the stack, pushing items down, while the bottom of the stack goes to the top.
do pipe [
-> [ 1..5 ] # [ 1, 2, 3, 4, 5 ]
down # [ 2, 3, 4, 5, 1 ]
]swap
swap stack → stack
Swaps the first two items on the stack, so the first becomes the second and vice-versa.
copy, duplicate
copy stack → stack
Copies the top item on the stack.
flatten
flatten stack → stack
If the first item of the stack is an iterable, push each item it produces onto the stack. Items are added in the reverse order from when they’re produced, so that the first item produced will be at the top of the stack.
If it’s not an iterable, do nothing.
do pipe [
-> [] # []
push [ 1..5 ] # [[ 1..5 ]]
flatten # [ 5, 4, 3, 2, 1 ]
][!Note]
The reason we need this operator—rather than just relying on array functions—is because there’s no simple way to do this otherwise. For example, using
Array::flatwould just add back the array.
stack
stack stack → stack
Place the stack on the stack as an array, removing all the other items.
