Learning Tailspin by comparing to Javascript

Since the Tailspin programming language is a little different syntaxwise from most programming languages, I recently got a suggestion to put Tailspin code examples next to some well-known language. So with some help from Rosettacode, here goes, Javascript on the left vs Tailspin on the right, with logic as similar as is reasonable. Note that the syntax-highlighting algorithm doesn't really know Tailspin so sometimes it may mislead.

To begin with, you should probably try to forget everything you think you know about programming language syntax, Tailspin is very different.

Hello World

Tailspin uses an arrow -> to denote that the value created on the left is input to the transform on the right. This is similar to the "pipe" operation in shell-script programming. You can also think that the arrow corresponds to something like ".map" or ".forEach" in javascript. Note also that the bang ('!') in Tailspin shows where a value disappears, so the chain stops after the value has been sent with the write-message to OUT.

Javascript


console.log("Hello world!")
  

Tailspin


'Hello world!' -> !OUT::write
  

Simple math

Tailspin uses a dollar-sign to denote when you source a value, e.g. from a defined symbol. Note also the round parentheses used for array indexing and that indexes start at 1.

Javascript


const a = 2;
const b = 3;
const c = [1, 6];
console.log(a + b + c[0] - c[1])
  

Tailspin


def a: 2;
def b: 3;
def c: [1, 6];
$a + $b + $c(1) - $c(2) -> !OUT::write
  

A+B

Add two numbers given on an input line. Note that the javascript version would handle more than two numbers on the line.

Tailspin comes with a built-in PEG-like parser syntax, used inside a "composer". Things within angle brackets, '<' and '>', are matchers, here the built-in WS for whitespace and INT that produces an integer. So here we match a string with two integers, discarding whitespace around them, and output as an array (of two integers). After parsing, we add the first and second elements of the array together. The $ without a name refers to the current value being handled (here first as the array produced by nums, and in the next step it is the produced sum that is interpolated into the string to append a line break). Note also the $ in front of IN, the $ denotes a source, a place where a value (or several values) appears, here as a result of sending the readline-message to IN.

Javascript


process.stdin.on("data", buffer => {
  console.log(
    (buffer + "").trim().split(" ").map(Number)
        .reduce((a, v) => a + v, 0)
  );
});
  
  

Tailspin


composer nums
  [ (<WS>?) <INT> (<WS>) <INT> (<WS>?) ]
end nums
 
$IN::readline -> nums -> $(1) + $(2) -> '$;
' -> !OUT::write
  

FizzBuzz

In Tailspin, "templates" corresponds fairly well to "function", except that templates only take one input value (and can produce zero or more output values). The "when .. do" checks if the current value matches the expression inside the angle brackets and if so, executes the following code up to the next when case (remember, angle brackets are always around matchers, although these matchers are slightly different from matchers in a composer in that they can match other things than strings). The "otherwise" statement is executed if no "when" matches. To emit a value (similar to a "return", but processing can continue afterwards), you use a lone bang ('!') which also ends that value stream by "disappearing" the value into the calling context. The "\(" to "\)" section is an anonymous inline templates (a lambda, essentially, the backslash looks a bit like a lambda-sign if you squint). There is no for-loop in Tailspin, we simply create a stream of the integers 1 to 100, inclusive. Note how the string interpolation of a value starts with a $ and ends with a semi-colon (';') and remember that a lone $ refers to the current value.

Javascript


var fizzBuzz = function (i) {
  function fizz(i) {
    return !(i % 3) ? 'Fizz' : '';
  };
  function buzz(i) {
    return !(i % 5) ? 'Buzz' : '';
  };
  return `${fizz(i)}${buzz(i)}` || i;
};
for (var i = 1; i < 101; i += 1) {
  console.log(fizzBuzz(i));
}

Tailspin


templates fizzBuzz
  templates fizz
    when <?($ mod 3 <=0>)> do 'Fizz'!
  end fizz
  templates buzz
    when <?($ mod 5 <=0>)> do 'Buzz'!
  end buzz
  def i: $;
  '$->fizz;$->buzz;'
    -> \(when <=''> do $i! otherwise $! \) !
end fizzBuzz
1..100 -> '$->fizzBuzz;
' -> !OUT::write

Fibonacci

Return the nth fibonacci number.

In Tailspin, the only values that can be modified are the @-values that live inside a templates. The pound sign ('#') denotes that the value should be matched against the matchers (the when-statements). To repeat, we send a new value back to be matched. Note that the "<0~..>" matcher matches a value strictly greater than zero.

Javascript


function fib(n) {
  var a = 0, b = 1, t;
  while (n-- > 0) {
    t = a;
    a = b;
    b += t;
  }
  return a;
}

Tailspin


templates nthFibonacci
  @: {a: 0, b: 1};
  $ -> #
  when <0~..> do
    @: {a: $@.b, b: $@.a + $@.b};
    $ - 1 -> #
  otherwise $@.a!
end nthFibonacci

Matrix multiplication

The Javascript version has been written to mirror the Tailspin version, though it would probably naturally be written slightly differently. The A-matrix is used as a template for the rows of the output, while the first row of the B-matrix is used as a template for the columns of the output.

We can define binary (two-argument) operators in Tailspin. Note also the "\[i](" construct where backslash is the start of an inline function definition (a lambda), which ends at "\)". The i inside the square brackets says that the lambda should apply to each element of an array and that the index should be provided as the defined symbol 'i'. The result is still an array, but with each element replaced with the result of the lambda.

Javascript


matmul = function(A, B) {
  return A.map((_r, i) => {
    return B[0].map(_c, j) => {
      var cell = 0;
      for (var k = 0; k < B.length; k++) {
        cell += A[i][k] * B[k][j];
      }
      return cell;
    });
  });
}
 
const a = [[1,2],[3,4]];
const b = [[-3,-8,3],[-2,1,4]];
print(matmul(a,b));

Tailspin


operator (A matmul B)
  $A -> \[i](
    $B(1) -> \[j](
      @: 0;
      1..$B::length -> @: $@ + $A($i;$) * $B($;$j);
      $@ !
    \) !
  \) !
end matmul

def a: [[1,2],[3,4]];
def b: [[-3,-8,3],[-2,1,4]];
($a matmul $b) -> !OUT::write

Reverse words in a string

In the Tailspin version we keep the whitespace between the words while the Javascript removes it and replaces it. The Tailspin input is also already divided into lines.

The ellipsis ('...') streams out the individual elements of the array. The tilde ('~') denotes inverse, or "not", in Tailspin. The composer produces an array of word-productions, where the word rule in turn produces a sequence of non-whitespace characters followed by an optional sequence of whitespace characters. The two sequences (strings) produced are just separate strings in the resulting array on the same level as the other strings. Note also how we can select a sequence of elements from an array, with an optional stride, in this case we take all elements in reverse order, by "$(last..first:-1)".

Javascript


const input =
"---------- Ice and Fire ------------\n\
\n\
fire, in end will world the say Some\n\
ice. in say Some\n\
desire of tasted I've what From\n\
fire. favor who those with hold I\n\
\n\
... elided paragraph last ...\n\
\n\
Frost Robert -----------------------";
 
function reverseString(s) {
  return s.split('\n').map(
    function (line) {
      return line.split(/\s/).reverse().join(' ');
    }
  ).join('\n');
}
 
console.log(
  reverseString(input)
);

Tailspin


def input: ['---------- Ice and Fire ------------',
            '',
            'fire, in end will world the say Some',
            'ice. in say Some',
            'desire of tasted I''ve what From',
            'fire. favor who those with hold I',
            '',
            '... elided paragraph last ...',
            '',
            'Frost Robert -----------------------']
;
 
composer words
  [ <word>* ]
  rule word: <~WS> <WS>?
end words
 
$input... -> '$ -> words -> $(last..first:-1)...;
' -> !OUT::write

Water collected between towers

Fill a "skyline" with water, so the input [1, 5, 3, 7, 2] will result in a total of two units of water held above the 3. For a better description and interesting links, see the rosetta code page for this problem.

The chosen algorithm goes first from left to right to find the height of the left containing wall, then from right to left to see how high the water level can be at that point. The Javascript is written to match the Tailspin as closely as possible.

Here we mostly put stuff together into a more complex algorithm. The matchers with the dots in are range matchers, so "<$val..>" matches a value greater than or equal to "val", while a tilde acts to exclude the value, so "<$val~..>" matches a value strictly greater than "val". The array is reversed as we did in the previous example and then we stream the elements out individually (by '...') and send them to the matchers (by '#').

Javascript


function histogramWater(a) {
  var leftMax = 0;
  return a.map((h) => {
    if (h > leftMax) {
      leftMax = h;
    }
    return { leftMax: leftMax, value: h };
  }).reduceRight((acc, point) => {
    if (point.value >= acc.rightMax) {
      acc.rightMax = point.value;
    } else if (point.value >= point.leftMax) {
      // do nothing
    } else if (point.leftMax <= acc.rightMax) {
      acc.sum += point.leftMax - point.value;
    } else {
      acc.sum += acc.rightMax - point.value;
    }
    return acc;
  }, {rightMax: 0, sum: 0}).sum;
}

console.log(histogramWater([1, 5, 3, 7, 2]));

Tailspin


templates histogramWater
  $ -> \( @: 0;
    [$... -> { leftMax: $ -> #, value: $ } ] !
    
    when <$@~..> do @: $; $ !
    otherwise $@ !
  \) -> \( @: { rightMax: 0, sum: 0 };
    $(last..1:-1)... -> #
    $@.sum !
    
    when <{ value: <$@.rightMax..> }> do @.rightMax: $.value;
    when <{ value: <$.leftMax..> }> do !VOID
    when <{ leftMax: <..$@.rightMax>}> do
      @.sum: $@.sum + $.leftMax - $.value;
    otherwise
      @.sum: $@.sum + $@.rightMax - $.value;
  \) !
end histogramWater

[1, 5, 3, 7, 2] -> histogramWater -> !OUT::write

Range expansion

A string with compressed ranges is to be expanded into a list of integers, e.g. "-6,-3-1,3-5,7-11,14,15,17-20" will expand to [-6, -3, -2, -1, 0, 1, 3, 4, 5, 7, 8, 9, 10, 11, 14, 15, 17, 18, 19, 20]

Tailspin has a built-in PEG-like parser syntax which is THE way to do string manipulation. You could use a PEG library in Javascript, but normally you soldier on with primitive string handling.

Here we produce an array of zero or more elements. The element rule will either be a range or an integer, optionally followed by a comma that is ignored. If it is a range, it will be an integer that is captured into the definition of the "start" symbol, followed by a dash that is ignored, then another integer which is sent on to produce a stream of integers from start to the current value, inclusive.

Javascript


function rangeExpand(rangeExpr) {
 
    function getFactors(term) {
        var matches = term.match(/(-?[0-9]+)-(-?[0-9]+)/);
        if (!matches) return {first:Number(term)};
        return {first:Number(matches[1]), last:Number(matches[2])};
    }
 
    function expandTerm(term) {
        var factors = getFactors(term);
        if (factors.length < 2) return [factors.first];
        var range = [];
        for (var n = factors.first; n <= factors.last;  n++) {
            range.push(n);
        }
        return range;
    }
 
    var result = [];
    var terms = rangeExpr.split(/,/);
    for (var t in terms) {
        result = result.concat(expandTerm(terms[t]));
    }
 
    return result;
}

console.log(rangeExpand('-6,-3--1,3-5,7-11,14,15,17-20'));

Tailspin


composer expand
  [<element>*]
  rule element: <range|INT> (<=','>?)
  rule range: (def start: <INT>; <='-'>) <INT> -> $start..$
end expand
 
'-6,-3--1,3-5,7-11,14,15,17-20' -> expand -> !OUT::write
Hopefully you now have a sense for how the basic syntax of Tailspin works so that you can better understand more complex code examples.

Comments

Popular Posts