Tailspin works with streams of values in pipelines. You may often feel that your program is the machine and that the input data is the program.
While Tailspin is unlikely to become mainstream, or even production-ready, it will change the way you think about programming in a good way.
1// Comment to end of line
2
3// Process data in a pipeline with steps separated by ->
4// String literals are delimited by single quotes
5// A bang (!) indicates a sink, or end of the pipe
6// OUT is the standard output object, ::write is the message to write output
7'Hello, World!' -> !OUT::write
8
9// Output a newline by just entering it in the string (multiline strings)
10'
11' -> !OUT::write
12// Or output the decimal unicode value for newline (10) between $# and ;
13'$#10;' -> !OUT::write
14
15// Define an immutable named value. Value syntax is very literal.
16def names: ['Adam', 'George', 'Jenny', 'Lucy'];
17
18// Stream the list to process each name. Note the use of $ to get the value.
19// The current value in the pipeline is always just $
20// String interpolation starts with a $ and ends with ;
21$names... -> 'Hello $;!
22' -> !OUT::write
23
24// You can also stream in the interpolation and nest interpolations
25// Note the list indexing with parentheses and the slice extraction
26// Note the use of ~ to signify an exclusive bound to the range
27// Outputs 'Hello Adam, George, Jenny and Lucy!'
28'Hello $names(first);$names(first~..~last)... -> ', $;'; and $names(last);!
29' -> !OUT::write
30
31// Conditionally say different things to different people
32// Matchers (conditional expressions) are delimited by angle brackets
33// A set of matchers, evaluated top down, must be in templates (a function)
34// Here it is an inline templates delimited by \( to \)
35// Note the doubled '' and $$ to get a literal ' and $
36$names... -> \(
37 when <='Adam'> do 'What''s up $;?' !
38 when <='George'> do 'George, where are the $$10 you owe me?' !
39 otherwise 'Hello $;!' !
40\) -> '$;$#10;' -> !OUT::write
41
42// You can also define templates (functions)
43// A lone ! emits the value into the calling pipeline without returning control
44// The # sends the value to be matched by the matchers
45// Note that templates always take one input value and emit 0 or more outputs
46templates collatz-sequence
47 when <..0> do 'The start seed must be a positive integer' !
48 when <=1> do $!
49// The ?( to ) allows matching a computed value. Can be concatenated as "and"
50 when <?($ mod 2 <=1>)> do
51 $ !
52 3 * $ + 1 -> #
53 otherwise
54 $ !
55 $ ~/ 2 -> #
56end collatz-sequence
57
58// Collatz sequence from random start on one line separated by spaces
591000 -> SYS::randomInt -> $ + 1 -> collatz-sequence -> '$; ' -> !OUT::write
60'
61' -> !OUT::write
62
63// Collatz sequence formatted ten per line by an indexed list template
64// Note the square brackets creates a list of the enclosed pipeline results
65// The \[i]( to \) defines a templates to apply to each value of a list,
66// the i (or whatever identifier you choose) holds the index
67[1000 -> SYS::randomInt -> $ + 1 -> collatz-sequence]
68-> \[i](
69 when <=1|?($i mod 10 <=0>)> do '$;$#10;' !
70 otherwise '$; ' !
71\)... -> !OUT::write
72
73// A range can have an optional stride
74def odd-numbers: [1..100:2];
75
76// Use mutable state locally. One variable per templates, always called @
77templates product
78 @: $(first);
79 $(first~..last)... -> @: $@ * $;
80 $@ !
81end product
82
83$odd-numbers(6..8) -> product -> !OUT::write
84'
85' -> !OUT::write
86
87// Use processor objects to hold mutable state.
88// Note that the outer @ must be referred to by name in inner contexts
89// A sink templates gives no output and is called prefixed by !
90// A source templates takes no input and is called prefixed by $
91processor Product
92 @: 1;
93 sink accumulate
94 @Product: $@Product * $;
95 end accumulate
96 source result
97 $@Product !
98 end result
99end Product
100
101// The processor is a constructor templates. This one called with $ (no input)
102def multiplier: $Product;
103
104// Call object templates by sending messages with ::
1051..7 -> !multiplier::accumulate
106-1 -> !multiplier::accumulate
107$multiplier::result -> 'The product is $;
108' -> !OUT::write
109
110// Syntax sugar for a processor implementing the collector interface
1111..7 -> ..=Product -> 'The collected product is $;$#10;' -> !OUT::write
112
113// Symbol sets (essentially enums) can be defined for finite sets of values
114data colour #{green, red, blue, yellow}
115
116// Use processor typestates to model state cleanly.
117// The last named mutable state value set determines the typestate
118processor Lamp
119 def colours: $;
120 @Off: 0;
121 state Off
122 source switchOn
123 @On: $@Off mod $colours::length + 1;
124 'Shining a $colours($@On); light$#10;' !
125 end switchOn
126 end Off
127 state On
128 source turnOff
129 @Off: $@On;
130 'Lamp is off$#10;' !
131 end turnOff
132 end On
133end Lamp
134
135def myLamp: [colour#green, colour#blue] -> Lamp;
136
137$myLamp::switchOn -> !OUT::write // Shining a green light
138$myLamp::turnOff -> !OUT::write // Lamp is off
139$myLamp::switchOn -> !OUT::write // Shining a blue light
140$myLamp::turnOff -> !OUT::write // Lamp is off
141$myLamp::switchOn -> !OUT::write // Shining a green light
142
143// Use regular expressions to test strings
144['banana', 'apple', 'pear', 'cherry']... -> \(
145 when <'.*a.*'> do '$; contains an ''a''' !
146 otherwise '$; has no ''a''' !
147\) -> '$;
148' -> !OUT::write
149
150// Use composers with regular expressions and defined rules to parse strings
151composer parse-stock-line
152 {inventory-id: <INT> (<WS>), name: <'\w+'> (<WS>), currency: <'.{3}'>,
153 unit-price: <INT> (<WS>?) <parts>?}
154 rule parts: associated-parts: [<part>+]
155 rule part: <'[A-Z]\d+'> (<=','>?)
156end parse-stock-line
157
158'705 gizmo EUR5 A67,G456,B32' -> parse-stock-line -> !OUT::write
159// {associated-parts: [A67, G456, B32], currency: EUR,
160// inventory-id: 705, name: gizmo, unit-price: 5}
161'
162' -> !OUT::write
163
164// Stream a string to split it into glyphs.
165// A list can be indexed/sliced by an array of indexes
166// Outputs ['h','e','l','l','o'], indexing arrays/lists starts at 1
167['abcdefghijklmnopqrstuvwxyz'...] -> $([8,5,12,12,15]) -> !OUT::write
168'
169' -> !OUT::write
170
171// We have used only raw strings above.
172// Strings can have different types as determined by a tag.
173// Comparing different types is an error, unless a wider type bound is set
174// Type bound is given in ´´ and '' means any string value, tagged or raw
175templates get-string-type
176 when <´''´ '.*'> do '$; is a raw string' !
177 when <´''´ id´'\d+'> do '$; is a numeric id string' !
178 when <´''´ =id´'foo'> do 'id foo found' !
179 when <´''´ id´'.*'> do '$; is an id' !
180 when <´''´ name´'.+'> do '$; is a name' !
181 otherwise '$; is not a name or id, nor a raw string' !
182end get-string-type
183
184[name´'Anna', 'foo', id´'789', city´'London', id´'xzgh', id´'foo']...
185-> get-string-type -> '$;
186' -> !OUT::write
187
188// Numbers can be raw, tagged or have a unit of measure
189// Type .. is any numeric value, tagged, measure or raw
190templates get-number-type
191 when <´..´ =inventory-id´86> do 'inventory-id 86 found' !
192 when <´..´ inventory-id´100..> do '$; is an inventory-id >= 100' !
193 when <´..´ inventory-id´0..|..inventory-id´0> do '$; is an inventory-id' !
194 when <´..´ 0"m"..> do '$; is an m-measure >= 0"m"' !
195 when <´..´ ..0|0..> do '$; is a raw number' !
196 otherwise '$; is not a positive m-measure nor an inventory-id, nor raw' !
197end get-number-type
198
199[inventory-id´86, inventory-id´6, 78"m", 5"s", 99, inventory-id´654]...
200-> get-number-type -> '$;
201' -> !OUT::write
202
203// Measures can be used in arithmetic, "1" is the scalar unit
204// When mixing measures you have to cast to the result measure
2054"m" + 6"m" * 3"1" -> ($ ~/ 2"s")"m/s" -> '$;
206' -> !OUT::write
207
208// Tagged identifiers must be made into raw numbers when used in arithmetic
209// Then you can cast the result back to a tagged identifier if you like
210inventory-id´300 -> inventory-id´($::raw + 1) -> get-number-type -> '$;
211' -> !OUT::write
212
213// Fields get auto-typed, tagging raw strings or numbers by default
214// You cannot assign the wrong type to a field
215def item: { inventory-id: 23, name: 'thingy', length: 12"m" };
216
217'Field inventory-id $item.inventory-id -> get-number-type;
218' -> !OUT::write
219'Field name $item.name -> get-string-type;
220' -> !OUT::write
221'Field length $item.length -> get-number-type;
222' -> !OUT::write
223
224// You can define types and use as type-tests. This also defines a field.
225// It would be an error to assign a non-standard plate to a standard-plate field
226data standard-plate <'[A-Z]{3}[0-9]{3}'>
227
228[['Audi', 'XYZ345'], ['BMW', 'I O U']]... -> \(
229 when <?($(2) <standard-plate>)> do {make: $(1), standard-plate: $(2)}!
230 otherwise {make: $(1), vanity-plate: $(2)}!
231\) -> '$;
232' -> !OUT::write
233
234// You can define union types
235data age <"years"|"months">
236
237[ {name: 'Cesar', age: 20"years"},
238 {name: 'Francesca', age: 19"years"},
239 {name: 'Bobby', age: 11"months"}]...
240-> \(
241// Conditional tests on structures look a lot like literals, with field tests
242 when <{age: <13"years"..19"years">}> do '$.name; is a teenager'!
243 when <{age: <"months">}> do '$.name; is a baby'!
244// You don't need to handle all cases, 'Cesar' will just be ignored
245\) -> '$;
246' -> !OUT::write
247
248// Array/list indexes start at 1 by default, but you can choose
249// Slices return whatever overlaps with the actual array
250[1..5] -> $(-2..2) -> '$;
251' -> !OUT::write // Outputs [1,2]
2520:[1..5] -> $(-2..2) -> '$;
253' -> !OUT::write // Outputs [1,2,3]
254-2:[1..5] -> $(-2..2) -> '$;
255' -> !OUT::write // Outputs [1,2,3,4,5]
256
257// Arrays can have indexes of measures or tagged identifiers
258def game-map: 0"y":[
259 1..5 -> 0"x":[
260 1..5 -> level´1:[
261 1..3 -> {
262 level: $,
263 terrain-id: 6 -> SYS::randomInt,
264 altitude: (10 -> SYS::randomInt)"m"
265 }
266 ]
267 ]
268];
269
270// Projections (indexing) can span several dimensions
271$game-map(3"y"; 1"x"..3"x"; level´1; altitude:) -> '$;
272' -> !OUT::write // Gives a list of three altitude values
273
274// Flatten and do a grouping projection to get stats
275// Count and Max are built-in collector processors
276[$game-map... ... ...] -> $(collect {
277 occurences: Count,
278 highest-on-level: Max&{by: :(altitude:), select: :(level:)}
279 } by $({terrain-id:}))
280-> !OUT::write
281'
282' -> !OUT::write
283
284// Relations are sets of structures/records.
285// Here we get all unique {level:, terrain-id:, altitude:} combinations
286def location-types: {|$game-map... ... ...|};
287
288// Projections can re-map structures. Note § is the relative accessor
289$location-types({terrain-id:, foo: §.level::raw * §.altitude})
290-> '$;
291' -> !OUT::write
292
293// Relational algebra operators can be used on relations
294($location-types join {| {altitude: 3"m"} |})
295-> !OUT::write
296'
297' -> !OUT::write
298
299// Define your own operators for binary operations
300operator (left dot right)
301 $left -> \[i]($ * $right($i)!\)... -> ..=Sum&{of: :()} !
302end dot
303
304([1,2,3] dot [2,5,8]) -> 'dot product: $;
305' -> !OUT::write
306
307// Supply parameters to vary templates behaviour
308templates die-rolls&{sides:}
309 1..$ -> $sides::raw -> SYS::randomInt -> $ + 1 !
310end die-rolls
311
312[5 -> die-rolls&{sides:4}] -> '$;
313' -> !OUT::write
314
315// Pass templates as parameters, maybe with some parameters pre-filled
316source damage-roll&{first:, second:, third:}
317 (1 -> first) + (1 -> second) + (1 -> third) !
318end damage-roll
319
320$damage-roll&{first: die-rolls&{sides:4},
321 second: die-rolls&{sides:6}, third: die-rolls&{sides:20}}
322-> 'Damage done is $;
323' -> !OUT::write
324
325// Write tests inline. Run by --test flag on command line
326// Note the ~ in the matcher means "not",
327// and the array content matcher matches elements < 1 and > 4
328test 'die-rolls'
329 assert [100 -> die-rolls&{sides: 4}] <~[<..~1|4~..>]> 'all rolls 1..4'
330end 'die-rolls'
331
332// Provide modified modules to tests (aka test doubles or mocks)
333// IN is the standard input object and ::lines gets all lines
334source read-numbers
335 $IN::lines -> #
336 when <'\d+'> do $!
337end read-numbers
338
339test 'read numbers from input'
340 use shadowed core-system/
341 processor MockIn
342 source lines
343 [
344 '12a',
345 '65',
346 'abc'
347 ]... !
348 end lines
349 end MockIn
350 def IN: $MockIn;
351 end core-system/
352 assert $read-numbers <=65> 'Only 65 is read'
353end 'read numbers from input'
354
355// You can work with byte arrays
356composer hexToBytes
357 <HEX>
358end hexToBytes
359
360'1a5c678d' -> hexToBytes -> ($ and [x 07 x]) -> $(last-1..last) -> '$;
361' -> !OUT::write // Outputs 0005