Language

Effects

Introduction

This language is an experiment in making a highly portable functional language. The syntax shown below only illustrates the features of the language, it is only one of many possible projections to view a program. Creating programs is not done by editing text files instead a structured editor is needed.

The language has both a compiler and interpreter, either or even both can be used in one program. Anonymous functions can be captured, serialised and sent to other computers. For example a client and server app can be written as one function.

_ -> {
  let html = vacant
  request -> client -> {
    let method = request.method
    let handle_click = perform Alert(method)
    html.button("click")(handle_click)
  }
}

A fully exhaustive type checker exists for the language. i.e. if the checks pass it is guaranteed not to crash. This can be optionally run, it's not worth type checking a build script you get the same error anyway. It's possible to type check a single function.

Because the type system is complete and structural no type ever needs to be declared up front and no annotation is required, in fact annotation is not supported in the language. This choice is to make programmers never need to think about types. Type annotations are possible in the editor but they are only a debug tool and not committed to the source.

The type system contains extensible records and unions as well as an algebraic effect system. These three components are all built on row types, using the same approach for each keeps the implementation simple.

All of the goals of the language are achieved by having the Abstract Syntax Tree (AST) of the language be the public interface and keeping that interface as small as possible. There are currently only 19 different node types that make up the AST.

_ -> {
  let string = vacant
  let welcome = person -> {
    let message = string.append("Hello ")(person)
    perform Log(message)
  }
  welcome("Alan")
}

literal

_ -> {
  let i = 100
  let s = "hello"
  let list = [1, 2]
  {}
}

functions

All functions are anonymous. Functions are first class an can be returned by other functions. There is no support for multi-argument functions, to accept multiple arguments a function must return a function, and is therefore automatically curried

_ -> {
  let single = x -> [x]
  let _ = single(10)
  let double = x -> y -> [x, y]
  let _ = double(1)(2)
  let start_with_one = double(1)
  start_with_one(7)
}

Let bindings

A value can be given a name using let. Names can be reused by later let bindings, but the values contained are immutable, meaning the values themselves cannot be changed.

_ -> {
  let a = 1
  let b = a
  let a = 2
  b
}

Records

Records are used to store multiple values with a name. Typing is structural and so there is no need to define types a head of time. Because typing is structural any record with the fields required by a function can be passed to that function

a -> {
  let alice = {name: "Alice", age: 10}
  let name = alice.name
  let alice = {age: 11, ..alice}
  let age = alice.age
  let get_name = user -> user.name
  let _ = get_name(alice)
  let bob = {name: "Bob"}
  let _ = get_name(bob)
  {}
}

Unions

Unions are tagged unions, they are extensible. Case statements are first class i.e. it is possible to compose them.

_ -> {
  let ok = Ok(5)
  let unwrap = fallback -> match {
    Ok value -> value
    Error _ -> fallback
  }
  let _ = unwrap(0)(ok)
  {}
}

matches can be open

_ -> {
  let multiline = match {
    Let _ -> True({})
    _other -> False({})
  }
  {}
}

matches can be composed

_ -> {
  let pets = match {
    Cat _ -> "felix"
    Dog _ -> "fido"
  }
  let animals = match {
    Platypus _ -> {
      let _ = perform Log("special pet")
      "Alan"
    }
    pets
  }
  {}
}