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 Option<T>(
None(unit), Some(T)
) {
method isEmpty(): bool =
match (this) {
| None _ -> true
| Some _ -> false
}
}

Type Inference

class TypeInference {
function example(): unit = {
// a: (int) -> bool
// b: int, c: int
val _ = (a, b, c) -> (
if a(b + 1) then b else c
);
}
}

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 X86 assembly and machine code.

Getting Started

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

Program Layout

Here is an example program:

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

A module contains a list of classes, and a class can either be a normal class or utility class, which will be explained later. 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.

Classes and Types

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.

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.

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 = { name: "Immortal", age: 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 PrimitiveType(
U(unit),
I(int),
S(string),
B(bool),
) {
// some random functions
function getUnit(): PrimitiveType = U({})
function getInteger(): PrimitiveType = I(42)
function getString(): PrimitiveType = S("samlang")
function getBool(): PrimitiveType = 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 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.

Generics

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

class FunctionExample {
function <T> getIdentityFunction(): (T) -> T = (x) -> x
}
class Box<T>(val content: T) {
// object short hand syntax
function <T> init(content: T): Box<T> = { content }
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))
}
}

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.

For example, you can write:

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

Tuple

You can construct a tuple by surrounding a list of comma separated expressions within [].

  • This is a tuple: [1, 2];
  • This is also a tuple: ["foo", "bar", true, 42];
  • Tuples can live inside another tuple: [["hi", "how"], ["are", "you"]];

Variant

A variant constructor is like a function, but it must start with an uppercase letter: 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 [_, c] = ["dd", 3];
val { e as d } = { d: 5, e: 4 }
val _ = 42;
a + b * c / d
}
}

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 required type annotated happens at the top-level class function and method level. The type-checker can infer most of the types at the local level.

Here is a simple example to show you the power of the type-inference algorithm:

The type checker can correctly deduce the type of lambda (a, b, c) -> if a(b + 1) then b else c must be ((int) -> bool, int, int) -> int.

The type-checker first decides that b must be int since + adds up two ints. Then it knows that c must also be int, because b and c must have the same type. From the syntax, a must be a function. Since the function application of a happens at the boolean expression part of if, we know it must return bool. Since a accepts one argument that is int, the type of a must be (int) -> bool.

Although the type-checker is smart, in some cases it simply cannot determine the type because there is not enough information.

class NotEnoughTypeInfo {
function <T> randomFunction(): T = Builtins.panic("I can be any type!")
function main(): unit = { val _ = randomFunction(); }
}

The type of randomFunction() cannot be decided. We decide not to make it generic because we want every expression to have a concrete type. In this case, the type checker will instantiate the generic type T as unit.