summaryrefslogtreecommitdiffhomepage
path: root/tailspin.html.markdown
blob: 88bf07c678c7655215542657776d84d34a050ee7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
---
language: tailspin
filename: learntailspin.tt
contributors:
    - ["Torbjörn Gannholm", "https://github.com/tobega/"]

---

**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.

```c
// Comment to end of line

// Process data in a pipeline with steps separated by ->
// String literals are delimited by single quotes
// A bang (!) indicates a sink, or end of the pipe
// OUT is the standard output object, ::write is the message to write output
'Hello, World!' -> !OUT::write

// Output a newline by just entering it in the string (multiline strings)
'
' -> !OUT::write
// Or output the decimal unicode value for newline (10) between $# and ;
'$#10;' -> !OUT::write

// Define an immutable named value. Value syntax is very literal.
def names: ['Adam', 'George', 'Jenny', 'Lucy'];

// Stream the list to process each name. Note the use of $ to get the value.
// The current value in the pipeline is always just $
// String interpolation starts with a $ and ends with ;
$names... -> 'Hello $;!
' -> !OUT::write

// You can also stream in the interpolation and nest interpolations
// Note the list indexing with parentheses and the slice extraction
// Note the use of ~ to signify an exclusive bound to the range
// Outputs 'Hello Adam, George, Jenny and Lucy!'
'Hello $names(first);$names(first~..~last)... -> ', $;'; and $names(last);!
' -> !OUT::write

// Conditionally say different things to different people
// Matchers (conditional expressions) are delimited by angle brackets
// A set of matchers, evaluated top down, must be in templates (a function)
// Here it is an inline templates delimited by \( to \)
// Note the doubled '' and $$ to get a literal ' and $
$names... -> \(
  when <='Adam'> do 'What''s up $;?' !
  when <='George'> do 'George, where are the $$10 you owe me?' !
  otherwise 'Hello $;!' !
\) -> '$;$#10;' -> !OUT::write

// You can also define templates (functions)
// A lone ! emits the value into the calling pipeline without returning control
// The # sends the value to be matched by the matchers 
// Note that templates always take one input value and emit 0 or more outputs
templates collatz-sequence
  when <..0> do 'The start seed must be a positive integer' !
  when <=1> do $!
// The ?( to ) allows matching a computed value. Can be concatenated as "and"
  when <?($ mod 2 <=1>)> do
    $ !
    3 * $ + 1 -> #
  otherwise
    $ !
    $ ~/ 2 -> #
end collatz-sequence

// Collatz sequence from random start on one line separated by spaces
1000 -> SYS::randomInt -> $ + 1 -> collatz-sequence -> '$; ' -> !OUT::write
'
' -> !OUT::write

// Collatz sequence formatted ten per line by an indexed list template
// Note the square brackets creates a list of the enclosed pipeline results
// The \[i]( to \) defines a templates to apply to each value of a list,
// the i (or whatever identifier you choose) holds the index
[1000 -> SYS::randomInt -> $ + 1 -> collatz-sequence]
-> \[i](
  when <=1|?($i mod 10 <=0>)> do '$;$#10;' !
  otherwise '$; ' !
\)... -> !OUT::write

// A range can have an optional stride
def odd-numbers: [1..100:2];

// Use mutable state locally. One variable per templates, always called @
templates product
  @: $(first);
  $(first~..last)... -> @: $@ * $;
  $@ !
end product

$odd-numbers(6..8) -> product -> !OUT::write
'
' -> !OUT::write

// Use processor objects to hold mutable state.
// Note that the outer @ must be referred to by name in inner contexts
// A sink templates gives no output and is called prefixed by !
// A source templates takes no input and is called prefixed by $
processor Product
  @: 1;
  sink accumulate
    @Product: $@Product * $;
  end accumulate
  source result
    $@Product !
  end result
end Product

// The processor is a constructor templates. This one called with $ (no input)
def multiplier: $Product;

// Call object templates by sending messages with ::
1..7 -> !multiplier::accumulate
-1 -> !multiplier::accumulate
$multiplier::result -> 'The product is $;
' -> !OUT::write

// Syntax sugar for a processor implementing the collector interface
1..7 -> ..=Product -> 'The collected product is $;$#10;' -> !OUT::write

// Symbol sets (essentially enums) can be defined for finite sets of values
data colour #{green, red, blue, yellow}

// Use processor typestates to model state cleanly.
// The last named mutable state value set determines the typestate
processor Lamp
  def colours: $;
  @Off: 0;
  state Off
    source switchOn
      @On: $@Off mod $colours::length + 1;
      'Shining a $colours($@On); light$#10;' !
    end switchOn
  end Off
  state On
    source turnOff
      @Off: $@On;
      'Lamp is off$#10;' !
    end turnOff
  end On
end Lamp

def myLamp: [colour#green, colour#blue] -> Lamp;

$myLamp::switchOn -> !OUT::write // Shining a green light
$myLamp::turnOff -> !OUT::write  // Lamp is off
$myLamp::switchOn -> !OUT::write // Shining a blue light
$myLamp::turnOff -> !OUT::write  // Lamp is off
$myLamp::switchOn -> !OUT::write // Shining a green light

// Use regular expressions to test strings
['banana', 'apple', 'pear', 'cherry']... -> \(
  when <'.*a.*'> do '$; contains an ''a''' !
  otherwise '$; has no ''a''' !
\) -> '$;
' -> !OUT::write

// Use composers with regular expressions and defined rules to parse strings
composer parse-stock-line
  {inventory-id: <INT> (<WS>), name: <'\w+'> (<WS>), currency: <'.{3}'>,
    unit-price: <INT> (<WS>?) <parts>?}
  rule parts: associated-parts: [<part>+]
  rule part: <'[A-Z]\d+'> (<=','>?)
end parse-stock-line

'705 gizmo EUR5 A67,G456,B32' -> parse-stock-line -> !OUT::write
// {associated-parts: [A67, G456, B32], currency: EUR,
//     inventory-id: 705, name: gizmo, unit-price: 5}
'
' -> !OUT::write

// Stream a string to split it into glyphs.
// A list can be indexed/sliced by an array of indexes
// Outputs ['h','e','l','l','o'], indexing arrays/lists starts at 1
['abcdefghijklmnopqrstuvwxyz'...] -> $([8,5,12,12,15]) -> !OUT::write
'
' -> !OUT::write

// We have used only raw strings above.
// Strings can have different types as determined by a tag.
// Comparing different types is an error, unless a wider type bound is set
// Type bound is given in ´´ and '' means any string value, tagged or raw
templates get-string-type
  when <´''´ '.*'> do '$; is a raw string' !
  when <´''´ id´'\d+'> do '$; is a numeric id string' !
  when <´''´ =id´'foo'> do 'id foo found' !
  when <´''´ id´'.*'> do '$; is an id' !
  when <´''´ name´'.+'> do '$; is a name' !
  otherwise '$; is not a name or id, nor a raw string' !
end get-string-type

[name´'Anna', 'foo', id´'789', city´'London', id´'xzgh', id´'foo']...
-> get-string-type -> '$;
' -> !OUT::write

// Numbers can be raw, tagged or have a unit of measure
// Type .. is any numeric value, tagged, measure or raw
templates get-number-type
  when <´..´ =inventory-id´86> do 'inventory-id 86 found' !
  when <´..´ inventory-id´100..> do '$; is an inventory-id >= 100' !
  when <´..´ inventory-id´0..|..inventory-id´0> do '$; is an inventory-id' !
  when <´..´ 0"m"..> do '$; is an m-measure >= 0"m"' !
  when <´..´ ..0|0..> do '$; is a raw number' !
  otherwise '$; is not a positive m-measure nor an inventory-id, nor raw' !
end get-number-type

[inventory-id´86, inventory-id´6, 78"m", 5"s", 99, inventory-id´654]...
-> get-number-type -> '$;
' -> !OUT::write

// Measures can be used in arithmetic, "1" is the scalar unit
// When mixing measures you have to cast to the result measure
4"m" + 6"m" * 3"1" -> ($ ~/ 2"s")"m/s" -> '$;
' -> !OUT::write

// Tagged identifiers must be made into raw numbers when used in arithmetic
// Then you can cast the result back to a tagged identifier if you like
inventory-id´300 -> inventory-id´($::raw + 1) -> get-number-type -> '$;
' -> !OUT::write

// Fields get auto-typed, tagging raw strings or numbers by default
// You cannot assign the wrong type to a field
def item: { inventory-id: 23, name: 'thingy', length: 12"m" };

'Field inventory-id $item.inventory-id -> get-number-type;
' -> !OUT::write
'Field name $item.name -> get-string-type;
' -> !OUT::write
'Field length $item.length -> get-number-type;
' -> !OUT::write

// You can define types and use as type-tests. This also defines a field.
// It would be an error to assign a non-standard plate to a standard-plate field
data standard-plate <'[A-Z]{3}[0-9]{3}'>

[['Audi', 'XYZ345'], ['BMW', 'I O U']]... -> \(
  when <?($(2) <standard-plate>)> do {make: $(1), standard-plate: $(2)}!
  otherwise {make: $(1), vanity-plate: $(2)}!
\) -> '$;
' -> !OUT::write

// You can define union types
data age <"years"|"months">

[ {name: 'Cesar', age: 20"years"},
  {name: 'Francesca', age: 19"years"},
  {name: 'Bobby', age: 11"months"}]...
-> \(
// Conditional tests on structures look a lot like literals, with field tests
  when <{age: <13"years"..19"years">}> do '$.name; is a teenager'!
  when <{age: <"months">}> do '$.name; is a baby'!
// You don't need to handle all cases, 'Cesar' will just be ignored
\) -> '$;
' -> !OUT::write

// Array/list indexes start at 1 by default, but you can choose
// Slices return whatever overlaps with the actual array
[1..5] -> $(-2..2) -> '$;
' -> !OUT::write // Outputs [1,2]
0:[1..5] -> $(-2..2) -> '$;
' -> !OUT::write // Outputs [1,2,3]
-2:[1..5] -> $(-2..2) -> '$;
' -> !OUT::write // Outputs [1,2,3,4,5]

// Arrays can have indexes of measures or tagged identifiers
def game-map: 0"y":[
  1..5 -> 0"x":[
    1..5 -> level´1:[
      1..3 -> {
        level: $,
        terrain-id: 6 -> SYS::randomInt,
        altitude: (10 -> SYS::randomInt)"m"
      }
    ]
  ]
];

// Projections (indexing) can span several dimensions
$game-map(3"y"; 1"x"..3"x"; level´1; altitude:) -> '$;
' -> !OUT::write // Gives a list of three altitude values

// Flatten and do a grouping projection to get stats
// Count and Max are built-in collector processors
[$game-map... ... ...] -> $(collect {
      occurences: Count,
      highest-on-level: Max&{by: :(altitude:), select: :(level:)}
    } by $({terrain-id:}))
-> !OUT::write
'
' -> !OUT::write

// Relations are sets of structures/records.
// Here we get all unique {level:, terrain-id:, altitude:} combinations
def location-types: {|$game-map... ... ...|};

// Projections can re-map structures. Note § is the relative accessor
$location-types({terrain-id:, foo: §.level::raw * §.altitude})
-> '$;
' -> !OUT::write

// Relational algebra operators can be used on relations
($location-types join {| {altitude: 3"m"} |})
-> !OUT::write
'
' -> !OUT::write

// Define your own operators for binary operations
operator (left dot right)
  $left -> \[i]($ * $right($i)!\)... -> ..=Sum&{of: :()} !
end dot

([1,2,3] dot [2,5,8]) -> 'dot product: $;
' -> !OUT::write

// Supply parameters to vary templates behaviour
templates die-rolls&{sides:}
  1..$ -> $sides::raw -> SYS::randomInt -> $ + 1 !
end die-rolls

[5 -> die-rolls&{sides:4}] -> '$;
' -> !OUT::write

// Pass templates as parameters, maybe with some parameters pre-filled
source damage-roll&{first:, second:, third:}
  (1 -> first) + (1 -> second) + (1 -> third) !
end damage-roll

$damage-roll&{first: die-rolls&{sides:4},
  second: die-rolls&{sides:6}, third: die-rolls&{sides:20}}
-> 'Damage done is $;
' -> !OUT::write

// Write tests inline. Run by --test flag on command line
// Note the ~ in the matcher means "not",
// and the array content matcher matches elements < 1 and > 4
test 'die-rolls'
  assert [100 -> die-rolls&{sides: 4}] <~[<..~1|4~..>]> 'all rolls 1..4'
end 'die-rolls'

// Provide modified modules to tests (aka test doubles or mocks)
// IN is the standard input object and ::lines gets all lines
source read-numbers
  $IN::lines -> #
  when <'\d+'> do $!
end read-numbers

test 'read numbers from input'
  use shadowed core-system/
    processor MockIn
      source lines
        [
          '12a',
          '65',
          'abc'
        ]... !
      end lines
    end MockIn
    def IN: $MockIn;
  end core-system/
  assert $read-numbers <=65> 'Only 65 is read'
end 'read numbers from input'

// You can work with byte arrays
composer hexToBytes
  <HEX>
end hexToBytes

'1a5c678d' -> hexToBytes -> ($ and [x 07 x]) -> $(last-1..last) -> '$;
' -> !OUT::write // Outputs 0005
```

## Further Reading

- [Main Tailspin site](https://github.com/tobega/tailspin-v0/)
- [Tailspin language reference](https://github.com/tobega/tailspin-v0/blob/master/TailspinReference.md)