Pipes

A pipe is a self-operator that transforms a value in place. The colon
notation receiver:name(args) looks up the function name in the pipe table
and calls it with the receiver injected as the first argument:

receiver:name(args)  ==  receiver = name(receiver, args)

Two important properties:

  • The result is assigned back to the receiver, so pipes read naturally as
    in-place transformations: s:trim():upper() trims then uppercases s.
  • The return type must match the receiver’s type. A pipe that returns a
    different type raises a TYPE_ERROR.

Pipes resolve through the pipe table and the global/std scope, never
through the receiver’s own members — a function stored in an object is called
with dot (obj.fn(...), where the receiver is not passed).

var s  = "  hello  "
s:trim():upper()        # s -> "HELLO"

var arr = [3, 1, 2]
arr:sort():reverse()    # arr -> [3, 2, 1]

var obj = {a: 1}
obj:merge({b: 2})       # obj -> {a: 1, b: 2}

print("abc":upper())    # error: literals cannot be receivers — bind to a name first

Conversion pipes

int, long, float, double, and to_string are pipes as well as
globals — the same function either way (x:int() is int(x)). They are the
deliberate exception to the same-type rule: a conversion rewrites the
receiver in place, changing its type:

var flt = 1.04
flt:int()               # flt -> 1 (int)

var s = "3.7"
var i = s:double():int()  # i -> 3; s itself became 3.7 along the way

var n = 42
n:to_string()           # n -> "42"

A conversion that cannot be performed ("abc":int()) throws
INVALID_CONVERSION and leaves the receiver untouched.

The pipe builtins

pipe(name, fn) registers a callable name in the pipe table. From then on,
receiver:name(args) calls fn(receiver, args).

pipe_remove(name) unregisters it and returns true if it was removed:

pipe("twice", fn(n) { return n * 2 })

var x = 21
x:twice()               # x -> 42

print(pipe_remove("twice"))    # true
# x:twice()            # error: unknown pipe 'twice'

Initially available pipes

The built-in pipe table is seeded with a curated set of standard library
functions:

Strings

upper  lower  trim  trim_left  trim_right
substr  replace  replace_first  repeat  pad_left  pad_right

Arrays (from std.sort and std.seq)

sort  sort_by  shuffle  unique  flatten  slice  concat  fill
map  filter  find_all  zip
take  drop  chunk  enumerate
set_union  set_intersect  set_difference

Objects

merge

Dispatch pipes — work on more than one receiver type:

insert   # strings, arrays, objects
erase    # strings, arrays, objects
reverse  # strings, arrays

Conversion pipes — change the receiver’s type by design (see above):

int  long  float  double  to_string
var s = "hello"
s:insert(1, "X")        # s -> "hXello"

var n = [1, 2, 3]
n:erase(1)              # n -> [1, 3]

var w = "abc"
w:reverse()             # w -> "cba"

Not every std.* function is available as a pipe — only the ones seeded above
(plus anything you register). To call any other library function use the dot
form, e.g. std.string.split(s, ",").