Tailspin

Личный сайт Go-разработчика из Казани

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

Further Reading