Logosamlang

A statically-typed, functional, and sound programming language with type inference.

Hello World

class HelloWorld {
function getString(): string =
"Hello World"
}

42

class Math {
function answerToLife(): int =
2 * 21
}

Pattern Matching

class Opt<T>(
None(unit), Some(T)
) {
method isEmpty(): bool =
match (this) {
| None _ -> true
| Some _ -> false
}
method <R> map(f: (T) -> R): Opt<R> =
match (this) {
| None _ -> Opt.None({})
| Some v -> Opt.Some(f(v))
}
}

Type Inference

class TypeInference {
function <A, B, C> pipe(
a: A, f1: (A)->B, f2: (B)->C
): C = f2(f1(a))
function main(): unit = {
// n: int
// s: string
val _ = Main.pipe(
1,
(n) -> Builtins.intToString(n),
(s) -> Builtins.stringToInt(s)
)
}
}

Introduction

samlang is a statically-typed functional programming language designed and implemented by Sam Zhou. The language is still under development so the syntax and semantics may be changed at any time.

The language can be compiled down to WebAssembly with reference counting based garbage collection.

Getting Started

yarn add @dev-sam/samlang-cli
yarn samlang --help

Program Layout

Here is an example program:

interface GlobalMessageProducer {
function getGlobalMessage(): string
}
class HelloWorld(val message: string): GlobalMessageProducer {
private method getMessage(): string = {
val { message } = this;
message
}
function getGlobalMessage(): string = {
val hw = HelloWorld.init("Hello World");
hw.getMessage()
}
}
class Main {
function main(): string = HelloWorld.getGlobalMessage()
}

A module contains a list of classes and interfaces.

If there is a module named Main, then the entire program will be evaluated to the evaluation result of the function call Main.main(). If there is no such module, then the evaluation result will be unit.

Each .sam source file defines a module. You can use a different module's classes by import.

import { ClassA, ClassB } from Foo.Bar.Module
import { ClassC, ClassD } from Baz.Foo.Module
class ClassD {
function main(): int = ClassA.value() + ClassC.value()
}

Cyclic dependencies and mutual recursion between different classes are allowed. However, cyclic dependencies between modules are strongly discouraged.

Types

Primitive and Compound Types

You already see two several primitive types: string and int. There are 4 kinds of primitive types: unit, int, string, bool.

The unit type has only one possible value, which is {}. It is usually an indication of some side effects. The int type includes all 64-bit integers. The string type includes all the strings quoted by double-quotes, like "Hello World!". The bool types has two possible values: true and false.

samlang enables you to use these primitive types to construct more complex types. You can also have tuple types like [int, bool, string] and function types like ((int) -> int, int) -> int.

You may want to have some named tuple so that the code is more readable. samlang allows that by letting you create an object class.

Utility Class

We first introduce the simplest utility class. Utility classes serve as collections of functions. For example, we should put some math functions inside a utility class. i.e.

class Math {
function plus(a: int, b: int): int = a + b
function cosine(degree: int): int = 0
}

Here you see how you would define functions. Each top-level function defined in the class should have type-annotations for both arguments and return type for documentation purposes. The return value is written after the = sign. Note that we don't have the return keyword because everything is an expression.

A utility class is implicitly a class with no fields.

There is a special kind of function called method. You can define methods in utility classes although they are not very useful.

Object Class

Here we introduce the first kind of class: object class. You can define it like this:

class Student(private val name: string, val age: int) {
method getName(): string = this.name
private method getAge(): int = this.age
function dummyStudent(): Student = Student.init("Immortal", 65535)
}

The class shown above defines a function, 2 methods, and a type Student. You can see that the type Student is already used in the type annotation of dummyStudent function. You can create a student object by the JavaScript object syntax as shown above. This kind of expression can only be used inside the class.

You can also see methods defined here. You can think of method as a special kind of function that has an implicit this passes as the first parameter. (You cannot name this as a parameter name because it is a keyword.)

The private keyword tells the type-checker that this function, field or method cannot be used outside of the class that defines it.

Variant Class

An object class defines a producct type; a variant class defines a sum type. With variant class, you can define a type that can be either A or B or C. Here is an example:

class Type(
U(unit),
I(int),
S(string),
B(bool),
) {
// some random functions
function getUnit(): PrimitiveType = Type.U({})
function getInteger(): PrimitiveType = Type.I(42)
function getString(): PrimitiveType = Type.S("samlang")
function getBool(): PrimitiveType = Type.B(false)
// pattern matching!
method isTruthy(): bool =
match this {
| U _ -> false
| I i -> i != 0
| S s -> s != ""
| B b -> b
}
}

Inside the class, you can construct a variant by VariantClass.VariantName(expr).

Each variant carries some data with a specific type. To perform a case-analysis on different possibilities, you can use the match expression to pattern match on the expression.

Interfaces

An interface defines a set of functions and methods that must be implemented by classes that claim to implement this interface. An interface can extends multiple interfaces, and a class can implement multiple interfaces. Both use the colon syntax to declare the subtyping relationship.

interface A { function f1(): int }
interface B {
function f1(): int
method m2(): bool
}
interface C : A, B {
// f1 exists in both A and B.
// Since their signatures are the same, it's OK.
function f3(): string
}
class D : A, C {
function f1(): int = 3
method m2(): bool = true
function f3(): string = "samlang"
}

Generics

Generics is supported in all kinds of classes and interfaces. Here are some examples.

class FunctionExample {
function <T> getIdentityFunction(): (T) -> T = (x) -> x
}
class Box<T>(val content: T) {
method getContent(): T = {
val { content } = this; content
}
}
class Option<T>(None(unit), Some(T)) {
function <T> getNone(): Option<T> = None(unit)
function <T> getSome(d: T): Option<T> = Some(d)
method <R> map(f: (T) -> R): Option<R> =
match (this) {
| None _ -> None(unit)
| Some d -> Some(f(d))
}
}

Generics can have bounds. The compiler will ensure that type arguments (either explicitly annotated or inferred) are subtypes of the declared bounds. Note that samlang does not support subtyping in general. Most places the samlang compiler still requires that the expected type and the actual type are exactly the same.

interface Comparable<T> {
method compare(other: T): int
}
class BoxedInt(val i: int): Comparable<BoxedInt> {
method compare(other: BoxedInt): int = this.i - other.i
}
class TwoItemCompare {
function <C: Comparable<C>> compare1(v1: C, v2: C): int =
v1.compare(v2)
function <C: Comparable<C>> compare2(v1: C, v2: C): int =
v1.compare<C>(v2)
}

Expressions

The expressions are listed in order of precedence so you know where to add parenthesis.

Literal

These are all valid literals: 42, true, false, "aaa".

These are not: 3.14, 'c'.

This

The syntax is simple: this. It can be only used inside a method.

Variable

You can refer to a local variable or function parameter just by typing its name. For example, you can have:

function identity(a: int): int = a

or

function random(): int = { val a = 42; a }

Class Function

You can refer to a class function by ClassName.functionName.

An object class implicitly defines a special function init that serves as the constructor.

For example, you can write:

class Foo(a: int) {
function bar(): int = 3 + Foo.init(3).a
}
class Main {
function oof(): int = 14
function main(): int = Foo.bar() * Main.oof()
}

Variant

A variant constructor is a normal function: VariantClass.Some(42).

Field/Method Access

You can access a field/method simply by using the dot syntax: expr.name. You always need to use this syntax to access the field. i.e. this.field and field refer to different things.

Unary Expressions

  • Negation: -42, -(-42)
  • Not: !true, !(!(false))

Function Call

You can call a function as you would expect: functionName(arg1, arg2).

However, you do not need to have a function name: a lambda can also be used: ((x) -> x)(3).

Currying is not supported.

Binary Expressions

Here are the supported ones:

  • a * b, a / b, a % b, a + b, a - b: a and b must be int, and the result is int;
  • a < b, a <= b, a > b, a >= b: a and b must be int, and the result is bool;
  • a == b, a != b: a and b must have the same type, and the result is bool;
  • a && b, a || b: a and b must be bool, and the result is bool;
  • a::b (string concatenation of a and b): a and b must be string, and the result is string.

If-Else Expressions

In samlang, we do not have ternary expression, because if-else blocks are expressions.

You can write: if a == b then c else d. c and d must have the same type and the result has the same type as c and d.

Match Expressions

Suppose you have a variant type like class Option<T>(None(unit), Some(T)) {}. You can match on it like:

function matchExample(opt: Option<int>): int =
match (opt) {
| None _ -> 42
| Some a -> a
}

Pattern matching must be exhaustive. For example, the following code will have a compile-time error:

function badMatchExample(opt: Option<int>): int =
match (opt) {
| None _ -> 42
// missing the Some case, bad code
}

Lambda

You can easily define an anonymous function as a lambda. Here is the simpliest one: () -> 0. Here is a more complex one: identity function: (x) -> x. Note that the argument must always be surrounded by parenthesis.

You can optionally type-annotate some parameters: (x: int, y) -> x + y.

Statement Block Expression

You can define new local variables by using the val statement within a block of statements:

class Obj(val d: int, val e: int) {
function valExample(): int = {
val a: int = 1;
val b = 2;
val { d as c } = { d: 5, e: 4 }
val _ = 42;
a + b * c
}
}

The above example shows various usages of val statement. You can choose to type-annotate the pattern (variable, tuple, object, or wildcard), destruct on tuples or object, and ignore the output by using wildcard (supported in tuple pattern and wildcard pattern). Note that the semicolon is optional.

Statement blocks can be nested:

function nestedBlocks(): int = {
val a = {
val b = 4;
val c = {
val d = b;
b
};
b
};
a + 1
}

You can create a unit value by {}.

Type Inference

The only absolutely required type annotated happens at the top-level class function and method level. Most other types can be correctly inferred by the compiler and can be omitted from your program.

The type checker uses local type inference to infer types of most expressions. Therefore, it cannot infer types from the whole program like OCaml does. Instead, it will push down type hints from local, nearby contexts.

Despite the fundamental limitation, the compiler can correctly infer most of the local expression types. If your code does not use generics or unannotated lambda parameters, then all types can be inferred correctly. Most of the type arguments can be inferred, so they do not need to be explicitly supplied. Unannotated lambda parameters with local context but without parametric polymorphism can also be inferred perfectly.

Even when you combine polymorphic function call and unannotated lambda parameters, the type checker still attempts to infer the types from left to right. It will work in most of the code. An illustrating type inference example is available near the top of the page.