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.
Hello World
Every Fuse program starts with a main function that returns an i32 exit code.
fun main() -> i32
print("Hello, World!\n")
0The print function outputs a string to standard output. The last expression in the function body is the return value. Here, 0 indicates successful execution.
You can define helper functions and call them from main:
fun println(s: str) -> Unit
print(s)
print("\n")
()
fun main() -> i32
println("Hello from Fuse!")
0Fuse 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:
| Type | Description |
|---|---|
i32 | 32-bit signed integer |
f32 | 32-bit floating point |
str | String |
bool | Boolean (true or false) |
Unit | Unit 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
0Operators
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
0Strings can be concatenated with +:
fun main() -> i32
let greeting = "Hello " + "World"
print(greeting)
0Comparison 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
0Algebraic 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
BlueVariants can carry data, and types can be parameterized with type variables:
type Option[T]:
None
Some(T)Types can refer to themselves, enabling structures like linked lists:
type List[A]:
Cons(h: A, t: List[A])
NilRecords
Records are product types where each field has a name:
type Point:
x: i32
y: i32Records can have type parameters:
type Map[K, V]:
key: K
value: VTuples
Tuples are product types with unnamed, positional fields:
type Pair(i32, str)
type Tuple[A, B](A, B)Type aliases
You can create type aliases for convenience:
type List[A]:
Cons(h: A, t: List[A])
Nil
type Ints = List[i32]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() -> i32
let result = sum(5, 3)
print(int_to_str(result))
0Recursive functions
fun fib(n: i32, a: i32, b: i32) -> i32
match n:
0 => b
_ => fib(n - 1, b, a + b)
fun main() -> i32
let result = fib(10, 0, 1)
print(int_to_str(result))
0Lambda expressions
Anonymous functions use the => arrow syntax:
fun main() -> i32
let f = a => a + 1
let g = (x, y) => x + y
print(int_to_str(f(5)))
0Lambdas are first-class values. They can be passed to functions, stored in variables, and returned from functions.
Function types
The type of a function is written with ->:
i32 -> i32takes ani32, returns ani32(i32, i32) -> i32takes twoi32s, returns ani32A -> B -> Ccurried: takesA, returns a functionB -> C
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() -> i32
print(describe(2))
0The _ pattern matches anything and is used as a catch-all.
Matching on variants
type Option[A]:
None
Some(A)
fun describe_option(o: Option[i32]) -> str
match o:
Some(v) => "has value: " + int_to_str(v)
None => "empty"
fun main() -> i32
let o = Some(42)
print(describe_option(o))
0Inline blocks
When a match arm needs multiple expressions, use braces:
type Option[A]:
None
Some(A)
fun main() -> i32
let o = Some(5)
match o:
Some(v) => {
print("found: ")
print(int_to_str(v))
0
}
None => 1Generics
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.
type Option[A]:
None
Some(A)
fun is_some[A](o: Option[A]) -> bool
match o:
Some(v) => true
_ => false
fun main() -> i32
let x = Some(42)
match is_some(x):
true => 0
false => 1In 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:
type List[A]:
Cons(h: A, t: List[A])
Nil
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
trait Functor[A]:
fun map[B](self, f: A -> B) -> Self[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))Default implementations
Trait methods can have default implementations:
type Option[A]:
None
Some(A)
trait Monad[A]:
fun unit[T](a: T) -> Self[T];
fun flat_map[B](self, f: A -> Self[B]) -> Self[B];
fun map[B](self, f: A -> B) -> Self[B]
let f = a => Self::unit(f(a))
self.flat_map(f)
impl Monad for Option[A]:
fun unit[T](a: T) -> Option[T]
Some(a)
fun flat_map[B](self, f: A -> Option[B]) -> Option[B]
match self:
Some(v) => f(v)
_ => NoneThe Option implementation only needs to provide unit and flat_map. The map method is inherited from the default.
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() -> i32
let result = apply(5, a => a + 1)
print(int_to_str(result))
0Map, filter, and fold
type List[A]:
Cons(h: A, t: List[A])
Nil
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)
trait Functor[A]:
fun map[B](self, f: A -> B) -> Self[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 main() -> i32
let l = Cons(2, Cons(3, Nil))
let result = l.map(v => v + 1).filter(e => e > 3)
let s = List::sum(result)
print(int_to_str(s))
0Methods
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 Option[A]:
None
Some(A)
impl Option[A]:
fun map[B](self, f: A -> B) -> Option[B]
match self:
Some(v) => Some(f(v))
_ => None
fun main() -> i32
let o = Some(5)
let o1 = o.map(a => a + 1)
match o1:
Some(v) => {
print(int_to_str(v))
0
}
None => 1Static methods
Methods without self are static and are called using Type::method() syntax:
type List[A]:
Cons(h: A, t: List[A])
Nil
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 append[A](l1: List[A], l2: List[A]) -> List[A]
List::fold_right(l1, l2, (h, t) => Cons(h, t))
fun main() -> i32
let a = Cons(1, Cons(2, Nil))
let b = Cons(3, Nil)
let c = List::append(a, b)
0Do Notation
Do notation provides syntactic sugar for chaining operations that may fail or produce effects. It desugars into calls to flat_map.
type Option[A]:
None
Some(A)
trait Monad[A]:
fun unit[T](a: T) -> Self[T];
fun flat_map[B](self, f: A -> Self[B]) -> Self[B];
fun map[B](self, f: A -> B) -> Self[B]
let f = a => Self::unit(f(a))
self.flat_map(f)
impl Monad for Option[A]:
fun unit[T](a: T) -> Option[T]
Some(a)
fun flat_map[B](self, f: A -> Option[B]) -> Option[B]
match self:
Some(v) => f(v)
_ => None
fun main() -> i32
let x = Some(1)
let y = Some(2)
let z = Some(3)
let result = {
do:
i <- x
j <- y
k <- z
i + j + k
}
match result:
Some(v) => v
_ => 0The <- operator binds the value inside a monadic context. If any step produces None, the whole chain short-circuits to None.