Overview

An overview of the Fuse programming language.

Fuse is a statically typed, purely functional language that compiles to native code through GRIN and LLVM. This overview walks through the language from the basics to advanced features.

Fuse ships a small standard library that is always in scope, with no import statement. It defines List and Option, the Monad trait they both implement, the IO type that carries every effect, and string helpers such as str_len, char_at, and substring. Anything the examples below use without declaring it comes from there. Because impl blocks are open, you can add methods to standard-library types from your own code, and this overview does so in a few places.

Hello World

Every Fuse program starts with a main function.

fun main() -> IO[Unit]
    print("Hello, World!\n")

print does not write to the terminal when you call it. It returns an IO[Unit], a value that describes the write. Returning that value from main is what performs it, and the program exits with status 0. Input and Output covers this properly; until then, read fun main() -> IO[Unit] as “this program produces output”.

Functions that produce output return an action in the same way:

fun greet(name: str) -> IO[Unit]
    print("Hello, " + name + "!\n")

fun main() -> IO[Unit]
    greet("Fuse")

Fuse uses indentation to define blocks. No braces or end keywords are needed. The Unit type (and its value ()) represents “no meaningful value”, similar to void in other languages.

Primitives

Fuse provides a small set of primitive types:

TypeDescription
i3232-bit signed integer
f3232-bit floating point
strString
boolBoolean (true or false)
UnitUnit type (value is ())

Let bindings

Use let to bind a value to a name:

fun main() -> i32
    let x = 5
    let y = x + 1
    let name = "Fuse"
    let pi = 3.14
    let active = true
    0

A main that returns i32 returns an exit code directly. Use that form when the program produces no output, or when you need to choose the exit status yourself.

Operators

fun main() -> i32
    let sum = 1 + 1          # addition
    let diff = 5 - 3         # subtraction
    let product = 4 * 3      # multiplication
    let quotient = 10 / 2    # division
    let remainder = 7 % 3    # modulo
    0

Strings can be concatenated with +:

fun main() -> IO[Unit]
    let greeting = "Hello " + "World"
    print(greeting)

Comparison and logical operators:

fun main() -> i32
    let a = 1 == 2     # false
    let b = 1 != 2     # true
    let c = 3 < 5      # true
    let d = true && false   # false
    let e = true || false   # true
    0

Algebraic data types

Fuse supports three kinds of algebraic data types: sum types (variants), records (product types with named fields), and tuples (product types with positional fields).

Sum types

Sum types let you define a type that can be one of several variants:

type Color:
    Red
    Green
    Blue

Variants can carry data, and types can be parameterized with type variables. The standard library’s Option is defined exactly this way:

type Maybe[T]:
    Nothing
    Just(T)

Types can refer to themselves, enabling structures like trees and linked lists. The standard library’s List is a self-referential sum type:

type Tree[A]:
    Leaf
    Node(value: A, left: Tree[A], right: Tree[A])

Records

Records are product types where each field has a name:

type Point:
    x: i32
    y: i32

Records can have type parameters:

type Entry[K, V]:
    key: K
    value: V

Tuples

Tuples are product types with unnamed, positional fields:

type Pair(i32, str)

type Tuple[A, B](A, B)

Functions

Functions are defined with the fun keyword, followed by the name, parameters with types, and a return type.

fun sum(x: i32, y: i32) -> i32
    x + y

fun main() -> IO[Unit]
    print(int_to_str(sum(5, 3)))

int_to_str converts an i32 to a str, which is how numbers reach print.

Recursive functions

fun fib(n: i32, a: i32, b: i32) -> i32
    match n:
        0 => b
        _ => fib(n - 1, b, a + b)

fun main() -> IO[Unit]
    print(int_to_str(fib(10, 0, 1)))

Lambda expressions

Anonymous functions use the => arrow syntax:

fun main() -> IO[Unit]
    let f = a => a + 1
    let g = (x: i32, y: i32) => x + y
    print(int_to_str(f(5)) + " and " + int_to_str(g(4, 7)))

Lambdas are first-class values. They can be passed to functions, stored in variables, and returned from functions. A lambda taking more than one parameter needs its parameter types annotated.

Function types

The type of a function is written with ->:

Pattern Matching

Pattern matching is a core feature of Fuse, used to inspect and destructure values.

Matching on literals

fun describe(x: i32) -> str
    match x:
        1 => "one"
        2 => "two"
        _ => "something else"

fun main() -> IO[Unit]
    print(describe(2))

The _ pattern matches anything and is used as a catch-all.

Matching on variants

Matching destructures a variant and binds its payload:

fun describe_option(o: Option[i32]) -> str
    match o:
        Some(v) => "has value: " + int_to_str(v)
        None => "empty"

fun main() -> IO[Unit]
    print(describe_option(Some(42)))

Option, Some, and None come from the standard library. Where an empty Option can’t be inferred from context, name the type it holds: None[i32].

Inline blocks

When a match arm needs multiple expressions, use braces. A match used as a value goes in braces too:

fun main() -> IO[Unit]
    let o = Some(5)
    let msg = {
        match o:
            Some(v) => {
                let doubled = v * 2
                "found: " + int_to_str(doubled)
            }
            None => "empty"
    }
    print(msg + "\n")

Generics

Generics let you write code that works with any type. Fuse uses monomorphization: at compile time, generic code is specialized into concrete versions for each type it’s used with, so there is no runtime overhead.

fun first_or[A](l: List[A], dflt: A) -> A
    match l:
        Cons(h, t) => h
        Nil => dflt

fun main() -> IO[Unit]
    let nums = Cons(10, Cons(20, Nil))
    let words = Cons("fuse", Nil)
    print(int_to_str(first_or(nums, 0)) + " " + first_or(words, "none"))

One definition of first_or serves both List[i32] and List[str]. In most cases, Fuse’s bidirectional type inference figures out the types automatically.

Traits

Traits (also known as type classes) define shared behavior that types can implement. They enable ad-hoc polymorphism, allowing different types to provide their own implementations of the same interface.

trait Functor[A]:
    fun map[B](self, f: A -> B) -> Self[B];

Methods ending with ; are required and must be provided by implementors. Use impl Trait for Type to provide an implementation. Because impl blocks are open, this example adds fold_right and sum to the standard library’s List before implementing Functor for it:

trait Functor[A]:
    fun map[B](self, f: A -> B) -> Self[B];

impl List[A]:
    fun fold_right[A, B](as: List[A], z: B, f: (A, B) -> B) -> B
        match as:
            Cons(x, xs) => f(x, List::fold_right(xs, z, f))
            Nil => z

    fun sum(l: List[i32]) -> i32
        List::fold_right(l, 0, (acc, b) => acc + b)

impl Functor[A] for List[A]:
    fun map[B](self, f: A -> B) -> List[B]
        List::fold_right(self, Nil[B], (h, t) => Cons(f(h), t))

fun fmap[A, B, F: Functor](f: A -> B, x: F[A]) -> F[B]
    x.map(f)

fun main() -> IO[Unit]
    let l = Cons(1, Cons(2, Cons(3, Nil)))
    let l1 = fmap(v => v + 1, l)
    print(int_to_str(List::sum(l1)))

fmap works on any type that implements Functor, not just List. Self[B] in the trait declaration stands for the implementing type applied to a new element type, which is what makes the abstraction possible.

Default implementations

Trait methods can have a body, which implementors inherit unless they override it. A default method may call the trait’s required methods:

trait Greet:
    fun name(self) -> str;

    fun greeting(self) -> str
        "Hello, " + self.name() + "!"

type Dog:
    tag: str

impl Greet for Dog:
    fun name(self) -> str
        self.tag

fun main() -> IO[Unit]
    let d = Dog("Rex")
    print(d.greeting() + "\n")

Dog provides only name and gets greeting for free.

The standard library’s Monad trait works this way. It requires unit and flat_map and derives map from them, so every monad in Fuse gets map without implementing it:

fun main() -> IO[Unit]
    let o = Some(21)
    let doubled = o.map(v => v * 2)
    match doubled:
        Some(v) => print(int_to_str(v) + "\n")
        None => print("nothing\n")

Trait bounds

You can constrain generic type parameters to require a trait implementation using the T: Trait syntax. This ensures the function can only be called with types that implement the specified trait.

trait Summary:
    fun summarize(self) -> str;

type Tweet:
    username: str
    content: str

impl Summary for Tweet:
    fun summarize(self) -> str
        self.username + ": " + self.content

fun notify[T: Summary](s: T) -> IO[Unit]
    print("Breaking news! " + s.summarize() + "\n")

fun main() -> IO[Unit]
    let tweet = Tweet("elon", "work!")
    notify(tweet)

The [T: Summary] syntax means T must implement the Summary trait. Calling notify with a type that doesn’t implement Summary results in a compile-time error.

Fuse includes built-in traits like Add for arithmetic operators:

fun add[T: Add](a: T, b: T) -> T
    a + b

fun main() -> IO[Unit]
    print(int_to_str(add(2, 3)))

Higher-Order Functions

In Fuse, functions are first-class values. They can be passed as arguments, returned from other functions, and stored in variables.

fun apply(x: i32, f: i32 -> i32) -> i32
    f(x)

fun main() -> IO[Unit]
    print(int_to_str(apply(5, a => a + 1)))

Closures

Closures are anonymous functions that capture their surrounding scope and can call themselves recursively:

fun main() -> IO[Unit]
    let sep = ", "
    let join = (n: i32, acc: str) => {
        match n < 1:
            true => acc
            false => join(n - 1, acc + int_to_str(n) + sep)
    }
    print(join(3, ""))

join refers to itself in its own body, and captures sep from the enclosing function.

Working with lists

The standard library’s List comes with append, length, reverse, and head_or, plus map and flat_map through its Monad implementation:

fun main() -> IO[Unit]
    let a = Cons(1, Cons(2, Cons(3, Nil)))
    let b = Cons(4, Nil)
    let joined = a.append(b)
    let spread = joined.flat_map(v => Cons(v, Cons(v * 10, Nil)))
    print(int_to_str(joined.length()) + " " + int_to_str(spread.length()))

fold_right, filter, and sum are not built in. Because impl blocks are open, you add them yourself and they become methods on the standard library type:

impl List[A]:
    fun fold_right[A, B](as: List[A], z: B, f: (A, B) -> B) -> B
        match as:
            Cons(x, xs) => f(x, List::fold_right(xs, z, f))
            Nil => z

    fun filter[A](self, f: A -> bool) -> List[A]
        List::fold_right(self, Nil[A], (h, t) => {
            match f(h):
                true => Cons(h, t)
                false => t
        })

    fun sum(l: List[i32]) -> i32
        List::fold_right(l, 0, (acc, b) => acc + b)

fun main() -> IO[Unit]
    let l = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))
    let big = l.filter(e => e > 2)
    print(int_to_str(List::sum(big)))

filter is written once in terms of fold_right and works for every element type.

Methods

Methods are functions attached to a type via impl blocks.

Instance methods

Methods that take self as their first parameter are called with dot syntax:

type Counter:
    count: i32

impl Counter:
    fun bump(self, by: i32) -> Counter
        Counter(self.count + by)

    fun show(self) -> str
        int_to_str(self.count)

fun main() -> IO[Unit]
    let c = Counter(0)
    let c1 = c.bump(5)
    print(c1.show() + "\n")

The standard library’s Option provides is_some, is_none, get_or_else, and filter the same way.

Static methods

Methods without self are static and are called using Type::method() syntax:

impl List[A]:
    fun fold_left[A, B](l: List[A], acc: B, f: (B, A) -> B) -> B
        match l:
            Cons(h, t) => List::fold_left(t, f(acc, h), f)
            Nil => acc

    fun product(l: List[i32]) -> i32
        List::fold_left(l, 1, (acc, b) => acc * b)

fun main() -> IO[Unit]
    let l = Cons(2, Cons(3, Cons(4, Nil)))
    print(int_to_str(List::product(l)))

fold_left calls itself as List::fold_left, since a static method has no self to dispatch on.

Do Notation

Do notation provides syntactic sugar for chaining operations that may fail or produce effects. It desugars into calls to flat_map, so it works with any type that implements Monad, including the standard library’s Option, List, and IO.

fun main() -> IO[Unit]
    let x = Some(1)
    let y = Some(2)
    let z = Some(3)
    let total = {
        do:
            i <- x
            j <- y
            k <- z
            i + j + k
    }
    match total:
        Some(v) => print(int_to_str(v) + "\n")
        None => print("nothing\n")

The <- operator binds the value inside a monadic context. If any step produces None, the whole chain short-circuits to None. The final line is a plain value, which do notation lifts back into the monad.

Input and Output

Every effect in Fuse is a value. An IO[A] describes an effect that, when run, produces an A. Building one does nothing; running it is a separate step.

do: chains actions in order, and _ <- discards a result you don’t need:

fun main() -> IO[Unit]
    do:
        _ <- print("Hello, ")
        _ <- print("World!\n")
        ()

Returning the chain from main is what runs it. That is why every example above ends in an action rather than performing one.

exec runs an action and returns its result. You rarely need it, since a main that returns IO[T] is executed for you and exits 0. Call it yourself when the exit code matters. Here main returns i32, so the action’s value becomes the process’s exit status:

fun rw(path: str, content: str) -> IO[i32]
    do:
        _ <- write(path, content)
        v <- read(path)
        _ <- print(v)
        0

fun main() -> i32
    rw("/tmp/hello.txt", "Hello from Fuse!\n").exec()

read and write are IO actions like print. So is get_args, which yields the arguments passed after the program name:

fun greet(args: List[str]) -> IO[Unit]
    match args.length() >= 1:
        true => print("Hello, " + args.head_or("") + "!\n")
        false => print("Hello, stranger!\n")

fun main() -> IO[Unit]
    do:
        args <- get_args()
        _ <- greet(args)
        ()

Running fuse run greet.fuse World prints Hello, World!. The standard library also provides read_stdin for reading standard input to a str.