Smålang

Smålang

A small, functional, structurally typed, embeddable language for JSON-to-JSON transformation.

Named after Småland, the kids’ play area at Ikea.


Key ideas


JSON is Smål

Smålang is a superset of JSON. Strings, numbers, booleans, null, objects and arrays work in a familiar way.

{ "hello": "world" }

This is a Smål program returns "world" when invoked with the argument "hello" and void when invoked with any other argument.


Types are values

Smålang values may be abstract or concrete. An abstract value is very similar to a “type” — it defines the set of concrete values that “match” it.

Unlike types, Smålang abstract values are first-class: they can be bound to names, passed to and returned from functions, etc.

{ "x": int, "y": int }
# "int" is an abstract value that matches all concrete integers

Objects and functions are tables

All values in Smålang can be called, like functions; what that does varies.

When JSON objects are called with string keys as arguments, they return the corresponding values. Smålang’s function syntax is a generalization of this: keys can be anything, including abstract values.

fibonacci <- {
    0: 1,
    1: 1,
    int -> n: fibonacci(n - 1) + fibonacci(n - 2)
    # The key here is "int"; we’ll discuss the "-> n" soon.
}

Smålang is functional

Every value is immutable. There is no way to, e.g. update one property of an object; you just construct a new one.

Every function is pure: outputs depend only on inputs. now is fixed for the duration if the computation. There is no random or IO.

First-class functions. They can be bound to names, passed to and returned from functions, etc.

Lexical closures. Inner functions can use bindings from outer functions.


Syntax


Smålang has:

And that’s pretty much it!


What makes it Smål?

It doesn’t have:


Identifiers and operators


Anatomy a Smål function

Functions are made up of ,-separated cases. Each case consists of:


Match / Bind

We’ve already seen binding: it uses the <- and -> operators and work symmetrically in both directions.

greeting <- "Hello"

"World" -> planet

Match / Bind

In functions, we match and bind at the same time:

{ int -> n : ... }, # Match any integer and bind it to the name "n"

You can “de-structure” and match on parts too.

{ { "age": int -> age, "name": string -> name, ...any } : ... }

Match / Bind Sugar

{ a: ... }
{ { a }: ... }

Rest / Spread

To specify to the “all remaining branches” of a function when constructing it or matching against it, use |. This is equivalent to the ... operator in JavaScript.

{ x: { "foo": x } | some_object }

Here, “some object” is (shallow) merged with the object { "foo": x }.

{ { foo: x } | a: ... }

Assuming a is not already in scope, this removes the property “foo” from the input and binds the remainder to a.


Declarations

These help break down computations into separate steps, and bind intermediate values to names. An example:

# This function accepts an object with "Price" and "Qty" properties
# and returns the total with shipping.
{
    { "Price": num -> price, "Qty": num -> qty }:        # match & bind
        total <- price * qty;                            # declaration
        shipping <- total < 100 {: true: 10, false: 0 }; # declaration
        total + shipping                                 # return value
}

Expressions

Expressions are sequences of values that are mostly evaluated left-to-right without arbitrary rules like PEDMAS.

1 + 1 * 2   # Unlike most other languages, this is 4.
1 + (1 * 2) # This is 3.

Given a sequence foo bar baz, Smålang will evaluate it as (foo bar) baz.

Parentheses can be used to alter the evaluation order.


Operators

Normally, the function on the left is evaluated with the argument on the right; e.g. factorial(3) evaluates the function factorial with the argument 3. However when there’s an operator, say 3!, it does the reverse and evaluates the ! function with the argument 3 instead.

In the absence of parentheses, runs of identifiers are evaluated before operators. E.g. foo bar + baz qux is evaluated as ((foo bar) +)(baz qux). The + operator is called with the result of invoking foo with the argument bar. The operator returns an anonymous function, which is then called with the result of calling baz with qux.


Special values


&, | and ?


Tricks


Infix operators

Here’s how the + operator is implemented in Smål. We use function currying with a mix of postfix and prefix functions so we can write 1 + 1.

+ <- {: num -> a : { num -> b :
    # Native code to add a and b
} }

Regular expressions

Written as re "foo.*" or ri"foo.*/gi". re is a function from the standard library. The string passed to it doesn’t have to be a literal.

Regexes can be used as match expressions:

{
  re"^a.*": "This string has "
}

Iteration

The basic idea is to have optimized tail recursion, and to also include functions like filter, map and reduce in the standard library. Details TBD.


Todo