public
Last active

Aqua programming language

  • Download Gist
gistfile1.md
Markdown

TODO

  • Exceptions
  • Delimited continuations, because continuations are cool.
  • Macros (maybe)
  • Attribute lookup – lenses?
  • Keyword parameters

Syntax elements

(Parentheses), like other languages, delimit sub-expressions.

[Square brackets] create arrays (see below).

{Braces} create pipes and functions (see below).

Encoding

Source files are UTF-8. No exceptions.

Automatic semicolon insertion

Semicolons are used to delimit elements in arrays and statements in blocks. However, they are annoying to type and easy to forget.

So this language has a simple insertion rule:

If a line ends on a symbol OTHER THAN closing brackets }]), merge it with the next line.

Otherwise, insert a semicolon.

Example:

ducks =
    sum [
x - y -
    z
        cheese
    ]
waffles

is interpreted as:

ducks = sum [ x - y - z; cheese; ]; waffles;

Basic types

Integer

42
-42
0x2A
0b101010
1_048_576

Infinite precision integer type.

0x and 0b prefixes specify hexadecimal and binary, respectively. Octal is omitted because no-one uses it and it's a common source of newbie confusion.

Underscores are syntactic sugar. They make large numbers easier to read.

Character

c'x'
char 'x'  # `char` is a built-in function that converts a 1-char string to an actual char

Unicode code point. 4 bytes per character.

String

"Hello, world!"
"String with a
newline in it"
"Whitepace within \
    \two backslashes is ignored, just like Haskell"

Opaque text type. Implemented as UTF-16 internally – this means indexing is O(n).

Array

Fixed length array. These can be homogeneous (like Python lists), but are faster when all elements are of the same type.

An array literal is a sequence of elements, surrounded by '[]' and delimited by ';'. They are subject to semicolon insertion (see above).

  • Bytestring = array of bytes

    b'Hello, world!'
    encode 'utf-8' 'Hello, world!'
    bytes [0x48; 0x65]
    
  • Matrix = array of arrays

Functions

Functions are declared as follows:

distance-sq = { x y -> add (mul x x) (mul y y) }

Notice that the identifier contains a hyphen. With few exceptions, tokens must be separated by spaces – so there is no ambiguity as to what distance-sq means.

A function with multiple arguments is simply a function that returns another function, i.e. they are curried.

distance-sq = { x -> { y -> add (mul x x) (mul y y) } }

They can then be applied using juxtaposition, a la ML:

result = distance-sq 3 4

Functions can be composed left-to-right using the - operator:

f = { x -> add x 1 }
g = { x -> mul x 2 }

say ((f - g) 1)  # 4
say ((g - f) 1)  # 3

In general, f - g is equivalent to { x -> g (f x) }.

Pipes

Similar to conduits in Haskell or pipes in sh. A pipe awaits multiple inputs and yields multiple outputs. When it finishes, it returns a final value (called the result).

Pipes are applied using the | operator:

read-file "story.txt" | decode "utf-8" | split "\n" | filter (contains "e") | write

| pipes the output on the left to the input on the right. It discards the left result and returns the right one.

You may have noticed that the - and | operators look quite similar. This is intentional.

  • |, or "horizontal" composition, runs both arguments simultaneously.
  • -, or "vertical" composition, runs the first argument to completion before executing the second.

Creating a pipe is easy as well:

identity = {
    forever {
        value = await
        yield value
        }
    }

Using the composition operators described in the previous section, we can shorten this to:

identity = {
    forever {
        await - yield
        }
    }

forever runs its argument in an infinite loop. await waits for an upstream value. yield pushes a value downstream. So the identity pipe continually reads values, then writes them unchanged.

In fact, the whole program can be thought of as a pipe with both ends closed. Pipes are everywhere!

One important concept is the difference between the output and result values. In Python (and Aqua), the former is specified by yield and the latter with return. In sh, the former is standard output and the latter is the exit code.

So:

  • await takes the argument and turns it into a result
  • yield takes the argument and turns it into an output

Special case: Streams

Often we would need to pass a single value into a pipe. In that case, we can use yield:

yield 15 | sum

But what do we do when we need to stream in more than one value? Surprisingly, this doesn't work:

[1; 2; 3; 4; 5] | sum    # Returns 0

Why? When a non-pipe is used in a context that expects a pipe, it acts as a pipe that returns that value. Note returns, not outputs – the sum pipe never receives anything and the array is discarded.

We can solve this by using the stream function:

stream [1; 2; 3; 4; 5] | sum    # Returns 15

This is equivalent to:

each yield [1; 2; 3; 4; 5] | sum

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.