Catenary
Catenary is a concatenative programming library for Javascript. Like functional programming, concatenative programming can be hard to get your head around at first, but used appropriately it can make your programs cleaner and more elegant.
Catenary is an attempt to unite the fun and elegance of the concatenative style with the ease and popularity of Javascript. It puts pragmatism over purity, and is designed to interoperate cleanly with regular JS functions so you can mix and match to do whatever suits your code best.
In addition, Catenary is designed to be a utility library with all your old favourites like .extend
and .shuffle
.
You can install it with npm install catenary
Concatenative programming
Concatenative programming is kind of the bizarro world version of regular imperative or functional programming.
From functional programming you might be used to applying functions like then_do_that(do_this(take_this))
, but concatenative programming turns all that on its head! Functions chain (or concatenate) together instead of applying, so you would write take_this do_this then_do_that
. Each function receives the arguments from the previous function.
And from imperative programming you might be used to passing variables around, like x = leftFoot(); y = rightFoot(); z = shakeAllAbout(x, y);
. But concatenative programming doesn't believe in variables! Instead you would write something like leftFoot rightFoot shakeAllAbout
. Each function knows how many arguments it needs and how many it returns, so the variables can be implicit.
The way this is usually implemented is with a stack, which holds those implicit variables. Each function then pulls as many arguments as it likes from the stack and puts as many as it likes back on. Because all the data is implicit, you can think of it less like executing instructions and more like building a pipeline, or making a chain of operations that each connect to the next.
Want an example? Here's an example!
coffee> cat5 Function_stack: 5 coffee> cat5dup Function_stack: 55 coffee> cat5duptimes Function_stack: 25 coffee> catdefine 'square'catduptimes Function_stack: coffee> catsquare2 Function_stack: 4 coffee> catsquaresquare2 Function_stack: 16
Or how about our old friend fibonacci?
# [a, b] --swap--> [b, a] --over--> [b, a, b], --plus--> [b, a+b] catdefine 'nextfib'catswapoverplus cat0110catdupprintnextfibrepeat# 1 # 1 # 2 # 3 # 5 # 8 # 13 # 21 # 34 # 55
Want a more complicated example? Okay!
# Add 'n bottle/bottles' to the stack catdefine 'bottles' cat1dupdequal cat' bottle'' bottles'q dupdplus catdefine 'beer' cat bottlescat' of beer on the wall,'plusprint bottlescat' of beer!'plusprint cat'Take one down, pass it around,'print dec bottlescat' of beer on the wall.'plusprint cat''print cat99catbeerloop #99 bottles of beer on the wall, #99 bottles of beer! #Take one down, pass it around, #98 bottles of beer on the wall. # [etc...]
How Catenary works
You can create stacks of items with cat(...)
:
coffee> cat123 Function_stack: 123
And concatenate more values to the stack with cat(...).cat(...)
:
coffee> cat123cat456 Function_stack: 123456
When you access a property on a cat, that adds a function to the stack:
coffee> cat123plus Function_stack: 123Function
Chained properties just add more functions:
coffee> cat123plusplus Function_stack: 12FunctionFunction
Then when you execute the cat, all functions are called in order from left to right:
coffee> cat123plusplus Function_stack: 5
That means you can actually just add plain functions to the stack and they will be executed:
coffee> cat123 a * b Function_stack: 16
The arity of the function is inspected via its .length
property to figure out how many items to take off the stack. Any value it returns other than undefined will be put back on the stack:
coffee> cat123 7 Function_stack: 7
And you can chain multiple plain functions together too:
coffee> cat1 x + 1 x * 5 Function_stack: 10
You can also pass arguments when you execute the cat - they are added to the start of the stack:
coffee> cat3plus5 Function_stack: 8 coffee> catplus35 Function_stack: 8
Executing a cat with no functions in it will just return a copy:
coffee> cat123 Function_stack: 123 coffee> cat123 Function_stack: 123
So actually the main export is just a cat with an empty stack, which is fine because cats are immutable:
coffee> cat = require 'catenary' Function_stack:
Which means you can also write in regular function style, like this:
coffee> catplus23 Function_stack: 5 coffee> cattimestimes345 Function_stack: 60
To get values out, you can use $
($
is the show-me-the-money operator)
coffee> cat12$2cat12stack$12
But I am generally of the opinion that it's better to just pass whatever function you were going to return to into Catenary instead.
coffee> consolelog cat12plus$ #boo 3 coffee> catdefine 'print' consolelog x Function_stack: coffee> cat12plusprint #yay! 3
Higher order programming
To do loops and if statements and other fun things, we need to be able to use functions without executing them, what some concatenative languages call quotation
. We do this by wrapping them in a cat()
. A cat inside another cat won't be executed, and can be passed around like a value:
coffee> cat1 x + 1 Function_stack: 2 coffee> cat1cat x + 1 Function_stack: 1 Function_stack: Object
We can execute a cat from inside another cat by using .exec
:
coffee> cat1cat x + 1exec Function_stack: 2
Or do other things with it...
coffee> cat1500cat x + 1repeat Function_stack: 501
You can also include values inside the cat:
coffee> cat1cat5 x + yexec Function_stack: 6
Or to put it another way:
coffee> cat1cat5plusexec Function_stack: 6
You can also use .cat
to create higher-order cats:
coffee> cat1catplus5 Function_stack: 1 Function_stack: Object coffee> cat1catplus5exec Function_stack: 6
Which leads to a nifty self-contained imperative style, if you're into that kind of thing:
------> cat1...... catplus2exec...... catminus5exec...... cattimes7exec Function_stack: -14
Defining, importing and returning
You can define your own words:
coffee> catdefine 'add2' x + 2 Function_stack: coffee> catadd23 Function_stack: 5
And they don't have to be functions, they can be any value:
coffee> catdefine 'hello''hello!' Function_stack: coffee> cathellohellohello Function_stack: 'hello!''hello!''hello!'
You can also define namespaced words by using an object:
coffee> catdefine 'letters'a: 'apple'b: 'banana' Function_stack: coffee> catlettersalettersb Function_stack: 'apple''banana'
Or import a whole object at once:
coffee> catimporta: 'apple'b: 'banana' Function_stack: coffee> catabab Function_stack: 'apple''banana''apple''banana'
If you're concerned about collisions, you can use an instance:
coffee> cat2 = catinstance Function_proto: _stack: coffee> cat2define 'hello''world' Function_proto: _stack: coffee> cat2hello Function_proto: _stack: 'world' coffee> cathelloundefined
Standard library
Catenary can do a lot of stuff that is, for now, tragically undocumented. The aim is to eventually be a fully-featured utility library in the spirit of Underscore, lodash or Ramda. You can have a browse through the lib/
and test/lib
folders to see what is currently supported.
Limitations
Right now Catenary is fairly early stage. I have not done any performance or browser testing of any kind, so it is almost certainly unsuitable for your billion dollar web app. But there are tests for all the major functionality and I am committed to being one with the semver, so it should be safe enough to use.