summaryrefslogtreecommitdiffhomepage
path: root/tailspin.html.markdown
diff options
context:
space:
mode:
authorTorbjörn Gannholm <torbjorn.gannholm@normative.io>2024-01-02 21:25:11 +0100
committerGitHub <noreply@github.com>2024-01-02 21:25:11 +0100
commit4d3f15e10eb5d8427c9b2787eba24f76195980fc (patch)
tree4bcdfe7f010fa6c523b462ab3dfce8be0ee7ee6d /tailspin.html.markdown
parentc0fa7cf2c68033cce71b30116be6985f1506b6e4 (diff)
[Tailspin/en] Add page for the Tailspin language (#4819)
* Add page for the Tailspin language * [Tailspin/en] add more tutorial text
Diffstat (limited to 'tailspin.html.markdown')
-rw-r--r--tailspin.html.markdown383
1 files changed, 383 insertions, 0 deletions
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 <?($ 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)