diff --git a/tailspin.html.markdown b/tailspin.html.markdown new file mode 100644 index 00000000..a2d08dcc --- /dev/null +++ b/tailspin.html.markdown @@ -0,0 +1,383 @@ +--- +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. + +```tailspin +// 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 )> 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: (), name: <'\w+'> (), currency: <'.{3}'>, + unit-price: (?) ?} + rule parts: associated-parts: [+] + 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 )> 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 + +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)