Fix logo
Reimplement send with less recursion
Move around runtime code
Yet another experimental lang about piping data around
Pipes includes as a foundational construct, pattern matching and rewriting.
Check out the playground here: https://stuff.kyleperik.com/pipes/
Note, there is a save button, which saves the program to a registry server so they can be shared, but only if you are authenticated. If you would like to be able to use this, contact me and I'll create an account for you. You can also spin up your own service from this repository to run your own registry.
Otherwise the programs are cached in the browser local storage.
Pipes statements define what it should select, and what to generate with it.
'marco': 'polo';
This basic snippet selects on any 'marco' input, and spits out 'polo' in response.
In reality, things are a bit more complicated than that, since you may have multiple sources of data incoming, not just strings.
The basic implementation of pipes includes devices to handle basic string input and output. To select a device, simply specify the type in your match/result statements.
['input', 'marco', state]: ['log', 'polo'];
Now this is a working program. Type 'marco' in the input textbox and enter, and you will see 'polo' logged below.
To passthrough values received from input, don't specify a value for a key to give assign it to a reference, which can be used in the result.
['input', value, state]: ['log', value];
Specifying 'input' and 'log' can get a bit verbose. You can simplify the core rules by abstracting out the extraction to and from devices by including multiple stages.
['input', value, state]: value
| 'marco': 'polo'
| value: ['log', value];
Each stage selects for criteria, and has it's own result which is passed to the following stage to process.
You can include multiple rules within each step using grouping.
['input', value, state]: value
| (
'marco': 'polo';
'dog': 'woof';
)
| value: ['log', value];
Definitions enable you to package a set of rules into a reusable pattern, making use more succinct.
def input
['input', value, state]: value;
def log
value: ['log', value];
input | 'marco': 'polo' | log;
When matching patterns, if you use the same variable name, this will automatically apply a conditional that those values are the same.
For example:
[a, a]: a;
Matches [1, 1] but not [1, 2].
Objects keys are matched similar to js, where the key can be written without quotes.
{key: value}
This interprets "key" as a static string match.
Pipes also supports self-named key matches
{key}
In this case, key will match, and be assigned to the name "key".
Pipes also supports variable keys, when wrapping in parens.
For example, this matches the variable key.
{(key): value}
In doing so, this will potentially generate additional matches for each potential.
For example, this program:
{(key): value}: [key, value];
will match like so:
{a: 1, b: 2, c: 3} -> ['a', 1], ['b', 2], ['c', 3]
Pipes supports a number of primitives to enable math and boolean logic, and more.
Objects and lists support pulling a variable number of elements, for both matches and the result, similar to js, using 3 dots.
This example matches a list with at least 3 elements, and generates a list containing any additional elements (may be an empty list)
[a, b, c, ...rest]: rest;
Similarly, this matches an object with keys a, b and c, and outputs a object containing all other keys.
{a, b, c, ...rest}: rest;
In both causes the operator only works at the end.
The spread operator also works in the result too.
This outputs the object, replacing the a key with the value 1:
obj: {...obj, a: 1};
This outputs a new list with 1 at the end.
list: [...list, 1];
Pipes is unique in that every pipe has the potential to generate from any given event, any number of resulting events.
This makes things like mapping and filtering simpler, but loses the ability to track over time which events were a result of some upstream event.
For example, it's easy to increment numbers embedded in a list.
| each | x: [x, 1] | add;
# [1, 2], [2, 3] -> 2, 3, 3, 4
But this loses the original structure, if you wanted this instead
[2, 3], [3, 4]
This is what the bundle operator @ is for.
| @( each | x: [x, 1] | add );
# [1, 2], [2, 3] -> [2, 3], [3, 4]
When a pipe is prepended with @, this cases any events which result from this pipe to go into a list. Even if a given event results in no events from the pipe, it will still result in one result which is an empty list.
These devices are changing as I'm experimenting and moving things around. This specification is not core to the language, but is just a baseline to enable some usefulness to the language for now.
In general, input devices take this format, as a 3 element tuple:
[type, data, state]
Input event matching the form:
['input', line, state]
Analogous to standard in.
Output event matching the form:
['log', message]
Output event matching the form:
['draw', {type, ...data}]
Draws an element to screen
To draw a circle
['draw', {type: 'circle', radius, pos: {x, y}]
To draw a circle
['draw', {type: 'line', start: {x, y}, end: {x, y}]
To clear the screen
['draw', {type: 'clear'}]
Output event matching the form
['store', value, ...path]
Stores values to a given key in a path.
['store', value]
will store value as the entire state.
['store', value, key]
stores a value at a key.
['store', value, ...path]
store a value at the path.
Input event matching the form
['mousemove', {x, y}, state]
['mouseup', nil, state]
['mousedown', nil, state]
Input event matching the form
['keypress', key, state]
['keyup', key, state]
['keydown', key, state]
Input matching the form
['tick', nil, state]