Functions

Every function is implemented on a type. The general form is:

Type.functionName = (params) -> ReturnType {
    body
}

The only exception is main, the program's entry point.

A First Method

Greeting = String

Greeting.shout = () -> String {
    "HELLO"
}

main = (Stdout) -> Noop {
    Greeting("howdy").shout().print(Stdout)
}

Greeting.shout is a method on Greeting. It is called with dot syntax: Greeting("howdy").shout().

Method Bodies

A body is a newline-separated sequence of expressions. The last expression is the return value. There are no semicolons.

  • match is an expression — it can be the final line of a body or appear as a sub-expression.
  • while and for are expressions of type Noop.
  • Non-final lines whose results are discarded are valid (they exist for side effects or ? propagation).
File.readConfig = (Path) -> Result<Config, IoError | ParseError> {
    File.read(Path)?
        .parse()?
        .validate()
}

There are no local variables. The only way to thread a value through multiple operations is method chaining. That is the intended style.

Referring to the Receiver

Inside a method body, the receiver value is referenced by the receiver type's name:

String.print = (Stdout) -> Noop {
    Stdout.write(String)    // `String` here is the receiver value
}

The Self keyword is an alias, available everywhere. It is required only when the receiver's type name collides with a parameter of the same type:

Int.add = (Int) -> Int {
    ...   // ambiguous: which `Int`?
}

Resolve it either by using Self:

Int.add = (Int) -> Int {
    Self.plus(Int)
}

…or by introducing a newtype for the parameter:

OtherInt = Int

Int.add = (OtherInt) -> Int {
    Int.plus(OtherInt)
}

Self is the lighter-weight choice. The alias is right when the distinction is meaningful enough to warrant a name.

Declaration Order

Multiple methods on the same type must be declared in alphabetical order. This is a compile-time requirement, not a convention:

User.add    = (...) -> ...
User.export = (...) -> ...
User.remove = (...) -> ...

Visibility

Everything is public by default. Prefix a method with * to make it private to its declaring file:

Type.*helper = () -> Noop {
    ...
}

Optional Parameters

There is no special syntax. Use Option<T>:

String.print = (Option<Color>) -> Noop {
    ...
}

This allows both forms at the call site:

"hello".print()
"hello".print(Red)

First-Class Functions

Methods are first-class values. Refer to one by its qualified name Type.method and pass it where a matching trait signature is expected:

Numbers = ...Int

Numbers.doubleAll = () -> Numbers {
    Numbers.map(Int.double)
}

Lambdas

For one-off operations, write a lambda literal with its full signature. There is no signature inference:

Numbers.tripleAll = () -> Numbers {
    Numbers.map((Int) -> Int { Int.mul(Int(3)) })
}

Lambda syntax mirrors method declaration syntax: (params) -> ReturnType { body }. The only difference is the absence of a Type.name = prefix.

Generic Methods

A method can be parameterized by a type. Declare type parameters with <...> before the parameter list, optionally with a trait constraint:

List.print = <T: Print>() -> Noop {
    ...
}

When calling a generic method whose type parameter can't be inferred from context, pin it with ::<...> (turbofish) after the method name:

Json.parse::<List<Int>>("[1, 2, 3]")?

Turbofish is only required when the surrounding type context doesn't already determine the parameter. A function with an explicit Result<List<Int>, _> return type lets the compiler infer from the return position without an annotation.

The main Function

main is the single exception to "every function is on a type". It is a top-level free function and the program's entry point. It typically takes the capabilities the program needs:

main = (Stdout) -> Noop {
    "hello".print(Stdout)
}

See Capabilities for how this connects to side effects.