Flix —
A powerful effect-oriented programming language
Flix is a principled effect-oriented functional, imperative, and logic programming language developed at Aarhus University and by a community of open source contributors.
Why effect-oriented? And why Flix?
Why Effects? Effect systems represent the next major evolution in statically typed programming languages. By explicitly modeling side effects, effect-oriented programming enforces modularity and helps program reasoning. User-defined effects and handlers allow programmers to implement their own control structures.
Why Flix? We claim that of all the upcoming effect-oriented programming languages, Flix offers the most complete language implementation, the most extensive standard library, the most detailed documentation, and the best tool support.
Moreover, Flix builds on proven programming language technology, including: algebraic data types and pattern matching, extensible records, traits, higher-kinded types, associated types and effects, structured concurrency, and more.
/// Demonstrates composing multiple HTTP middleware
/// via `with` clauses. Stacks base URL, default
/// headers, retry, circuit breaker, and logging.
/// Each `with` wraps the
/// preceding block. The `Http` and `Logger` effects
/// propagate to `main` and are handled automatically
/// via their default handlers. Relative paths are
/// resolved against the base URL; absolute URLs
/// bypass it.
def main(): Unit \ { Clock, Http, Logger, IO } =
let defaultHeaders = Map#{
"Accept" => List#{"application/json"},
"Authorization" => List#{"Bearer tok123"}
};
run {
let urls = List#{"/api/users", "/api/posts"};
foreach (url <- urls) {
match Http.get(url) {
case Ok(res) => println("${url} -> ${status(res)}")
case Err(err) => println("${url} -> ${err}")
}
};
match Http.get("https://notfound.flix.dev/") {
case Ok(res) => println("notfound -> ${status(res)}")
case Err(err) => println("notfound -> ${err}")
}
} with Http.withBaseUrl("https://flix.dev")
with Http.withDefaultHeaders(defaultHeaders)
with Http.withRetry(
Retry.linear(maxRetries = 2, delay = milliseconds(100)))
with Http.withCircuitBreaker(
failureThreshold = 3, cooldown = seconds(5))
with Http.withLoggingenum Shape {
case Circle(Int32),
case Square(Int32),
case Rectangle(Int32, Int32)
}
def area(s: Shape): Int32 = match s {
case Circle(r) => 3 * (r * r)
case Square(w) => w * w
case Rectangle(h, w) => h * w
}Algebraic Data Types and Pattern Matching
Algebraic data types and pattern matching are the bread-and-butter of functional programming and are supported by Flix with minimal fuss.
Tuples and Records
Flix has built-in support for tuples and records.
Records use structural typing and are extensible.
def origin(): (Int32, Int32) = (0, 0)
def oneByOne(): {w = Int32, h = Int32} = {w = 1, h = 1}
def twoByFour(): {w = Int32, h = Int32} = {w = 2, h = 4}
def area(rect: {w = Int32, h = Int32 | r}): Int32 =
rect#w * rect#h
def f(): Int32 = area({h = 1, color = "Blue", w = 2})/// A pure function is annotated with `\ {}`.
def inc1(x: Int32): Int32 \ {} = x + 1
/// An impure function is annotated with `\ IO`.
def inc2(x: Int32): Int32 \ IO =
println("x = ${x}");
x + 1
def f(): Int32 \ IO = // f is impure
let r1 = inc1(123); // pure
let r2 = inc2(456); // impure
r1 + r2 // purePurity and Impurity
Flix precisely tracks the purity of every expression in a program.
The Flix compiler provides an ironclad guarantee that if an expression is pure then it cannot have side-effects and it is referentially transparent.
Polymorphic Effects
Flix is able to track purity through higher-order effect polymorphic functions.
For example, Flix knows that the purity of List.map depends on the purity of its function argument f.
///
/// The purity of `map` depends on the purity of `f`.
///
def map(f: a -> b \ ef, l: List[a]): List[b] \ ef =
match l {
case Nil => Nil
case x :: xs => f(x) :: map(f, xs)
}import java.time.LocalDateTime
eff HourOfDay {
def getCurrentHour(): Int32
}
def greeting(): String \ {HourOfDay} =
let h = HourOfDay.getCurrentHour();
if (h <= 12) "Good morning"
else if (h <= 18) "Good afternoon"
else "Good evening"
def main(): Unit \ IO =
run {
println(greeting())
} with handler HourOfDay {
def getCurrentHour(_, resume) =
let dt = LocalDateTime.now();
resume(dt.getHour())
}Algebraic Effects
Flix supports algebraic effects, i.e. user-defined effects and handlers. In particular, Flix supports multi-shot resumptions.
Effect-oriented programming, with algebraic effects, allows programmers to write pure functions modulo effects. Effect handlers enable program reasoning, modularity, and testability.
For example, the program on the left expresses a greeting function that is pure modulo the current time of the day. In main we call the function and handle the HourOfDay effect by getting the real-world time from Java's LocalDateTime.
Library Effects
The Flix Standard Library comes with a rich collection of built-in effects, including Clock, Console, Env, FileSystem, Http, Logger, Process, and Random.
Every library effect has a default handler, hence programs can use them without any handler boilerplate.
Library effects support composable middleware: HTTP requests can be configured with retries and circuit breakers, and the filesystem can be sandboxed, made read-only, or replaced with an in-memory implementation, e.g. for testing.
use Fs.FileRead
use Sys.Env
use Time.Clock
use Time.TimeUnit
/// The `Clock`, `Env`, `FileRead`, and `Logger`
/// effects all have default handlers, hence `main`
/// requires no explicit handlers.
def main(): Unit \ { Clock, Env, FileRead, Logger } =
let ts = Clock.currentTime(TimeUnit.Milliseconds);
let os = Env.getOsName();
Logger.info("Timestamp: ${ts}");
Logger.info("Operating System: ${os}");
match FileRead.read("data.txt") {
case Ok(content) => Logger.info("Read: ${content}")
case Err(err) => Logger.warn("Error: ${err}")
}///
/// We can implement a *pure* `sort` function which
/// internally converts an immutable list to an array,
/// sorts the array in-place, and then converts it
/// back to an immutable list.
///
def sort(l: List[a]): List[a] \ {} with Order[a] =
region r {
List.toArray(r,l) !> Array.sort! |> Array.toList
}
///
/// We can also write a *pure* `toString` function which
/// internally uses a mutable StringBuilder.
///
def toString(l: List[a]): String with ToString[a] =
region r {
let sb = StringBuilder.new(r);
foreach (x <- l) {
StringBuilder.appendString!("${x} :: ", sb)
};
StringBuilder.appendString!("Nil", sb);
StringBuilder.toString(sb)
}Region-based Local Mutation
Flix supports region-based local mutation, which makes it possible to implement pure functions that internally use mutable state and destructive operations, as long as these operations are confined to the region.
We can use local mutation when it is more natural to write a function using mutable data and in a familiar imperative-style while still remaining pure to the outside world.
We can also use local mutation when it is more efficient to use mutable data structures, e.g. when implementing a sorting algorithm.
Mutable Structs
Flix supports mutable structs. Fields are immutable by default, but can be marked with the mut modifier. Like all mutable memory in Flix, every struct belongs to a region.
Struct fields are unboxed, i.e. primitive fields do not require indirection. This makes structs a memory efficient building block for higher-level data structures, e.g. mutable lists, stacks, and queues.
The fields of a struct are only visible from within its companion module, providing compiler-enforced encapsulation.
struct Person[r] {
name: String,
mut age: Int32
}
mod Person {
/// Creates a fresh `Person` in the region `rc`.
pub def mkPerson(name: String, rc: Region[r]): Person[r] \ r =
new Person @ rc { name = name, age = 0 }
/// Increments the age of the given person `p`.
pub def birthday(p: Person[r]): Unit \ r =
p->age = p->age + 1
/// Returns a description of the given person `p`.
pub def describe(p: Person[r]): String \ r =
"${p->name} is ${p->age} years old"
}///
/// We can inspect the purity of a function argument.
///
def inspect(f: a -> b \ ef): Unit \ IO =
reifyEff(f) {
case Pure(g) => println("f is pure")
case _ => println("f is not pure")
}
///
/// We can use purity information to safely switch between
/// lazy (or parallel) evaluation. In this case, if f is
/// pure then perform the map operation lazily.
///
def map(f: a -> b \ ef, l: LazyList[a]): LazyList[b] \ ef =
reifyEff(f) {
case Pure(g) => mapL(g, l)
case _ => mapE(f, l)
}Purity Reflection
Flix supports a meta-programming construct that enables higher-order functions to inspect the purity of a function argument and use that information to vary their behavior.
For example, the DelayList.map function varies its behavior between eager and lazy evaluation depending on the purity of its function argument.
We can exploit purity reflection to selectively use lazy or parallel evaluation inside a library without changing the semantics from the point-of-view of the clients.
Parallelism
Flix makes it simple and easy to evaluate pure code in parallel.
For example, the code on the right shows a parallel implementation of the List.map function using the par construct.
Internally, the par construct uses light-weight VirtualThreads.
///
/// A parallel implementation of the List.map function.
///
def parMap(f: a -> b, l: List[a]): List[b] = match l {
case Nil => Nil
case x :: xs =>
// Evaluate f(x) and parMap(f, xs) in parallel.
par (r <- f(x); rs <- parMap(f, xs))
yield r :: rs
}def main(): Unit \ IO =
region rc {
// A channel which can buffer one message.
let (tx, rx) = Channel.buffered(rc, 1);
spawn say("Meow!", tx) @ rc; // thread 1
spawn say("Woof!", tx) @ rc; // thread 2
Channel.recv(rx) |> println
} // Execution is blocked until both threads finish.
/// Sends the string s on the given channel tx.
def say(s: String, tx: Sender[String, r]): Unit \ r =
Channel.send(s, tx)Structured Concurrency
Flix supports structured concurrency.
For example, the code on the left shows the creation of a fresh region named rc in which two threads are spawned.
Importantly, control-flow does not leave the region before both threads have terminated. Hence the two threads cannot outlive the lifetime of their enclosing region.
Traits
Flix uses traits to abstract over types that support a common set of operations.
For example, the Eq trait captures the notion of equality and is used throughout the standard library.
trait Eq[a] {
def eq(x: a, y: a): Bool
def neq(x: a, y: a): Bool = not Eq.eq(x, y)
}
instance Eq[(a1, a2)] with Eq[a1], Eq[a2] {
def eq(t1: (a1, a2), t2: (a1, a2)): Bool =
let (x1, x2) = t1;
let (y1, y2) = t2;
x1 == y1 and x2 == y2
}trait Foldable[t : Type -> Type] {
///
/// Left-associative fold of a structure.
///
def foldLeft(f: (b, a) -> b \ ef, s: b, t: t[a]): b \ ef
///
/// Right-associative fold of a structure.
///
def foldRight(f: (a, b) -> b \ ef, s: b, t: t[a]): b \ ef
}Higher-Kinded Types
Flix supports higher-kinded types making it possible to abstract over type constructors. For example, both Option and List implement Foldable.
The Flix standard library ships with many common traits, such as Monoid, Functor, and Foldable.
Associated Types
Flix supports associated types, which allow the types in instance signatures to depend on the instance type.
The code on the right defines a trait with an associated type Elm, which enables each Coll instance to define its element type.
trait Coll[a] {
/// The element type of the collection.
type Elm
/// Converts the collection to a list of its elements.
def toList(coll: a): List[Coll.Elm[a]]
}
instance Coll[Map[k, v]] {
type Elm = (k, v)
def toList(m: Map[k, v]): List[(k, v)] = ...
}trait Coll[a] {
/// The element type of the collection.
type Elm
/// The effect associated with the collection.
type Aef
/// Converts the collection to a list of its elements.
def toList(coll: a): List[Coll.Elm[a]] \ Coll.Aef[a]
}
instance Coll[MutMap[k, v, r]] {
type Elm = (k, v)
type Aef = r
def toList(m: Map[k, v]): List[(k, v)] \ r = ...
}Associated Effects
Associated effects allow the effects in trait members to depend on the instance type. This makes it easy to create abstractions over both pure and effectful operations, and mutable and immutable data structures.
The code on the left adds an associated effect Aef to the Coll trait, which makes it possible to add instances for mutable collections.
Monadic For-Yield
Flix supports a monadic forM-yield construct similar to Scala'sfor-comprehensions and Haskell's do notation. The forM construct is syntactic sugar for uses of point and flatMap (which are provided by the Monad trait).
def divide(x: Int32, y: Int32): Option[Int32] =
if (y == 0) None else Some(x / y)
def f(): Option[Int32] =
forM (
x <- divide(5, 2);
y <- divide(x, 8);
z <- divide(9, y)
) yield x + y + zdef validateUser(s: String): Validation[Err, String] = ...
def validatePass(s: String): Validation[Err, String] = ...
def conn(u: String, p: String): Validation[Err, Connection] =
forA (
user <- validateUser(u);
pass <- validatePass(p)
) yield Connection(user, pass)Applicative For-Yield
In addition to the monadic forM expression, Flix supports an applicative forA expression that builds on the Applicative trait. The forA construct makes it simple to write error-handling code which uses the Validation[e, t] data type.
Seamless Java Interoperability
Flix supports seamless Java interoperability, making it possible to reuse code from the Java Standard Library and the Java ecosystem, e.g., via Maven.
Java support includes object creation, method invocation, exceptions, and class/interface extension.
import java.io.File
import java.io.FileWriter
import java.io.IOException
def main(): Unit \ IO =
let f = new File("foo.txt");
try {
let w = new FileWriter(f);
w.append("Hello World\n");
w.close()
} catch {
case ex: IOException =>
println("Unable to write file")
}enum Tree[a] {
case Leaf(a)
case Node(Tree[a], Tree[a])
}
/// The compiler verifies that `size` is structurally
/// recursive and hence terminates on all inputs.
@Terminates
def size(t: Tree[Int32]): Int32 = match t {
case Tree.Leaf(_) => 1
case Tree.Node(l, r) => size(l) + size(r)
}Termination Checking
Flix supports the @Terminates annotation, which asks the compiler to verify that a function is structurally recursive — and hence guaranteed to terminate on all inputs.
The compiler checks that every recursive call is on a strict substructure of a formal parameter. The check supports tree recursion, functions with multiple parameters, local definitions, and higher-order functions.
First-class Datalog Constraints
Another unique feature of Flix is its embedded Datalog support. Datalog, a powerful logic programming language in its own right, makes it simple and elegant to express many fixpoint problems (including various graph reachability problems):
def reachable(g: List[(String, Int32, String)], minSpeed: Int32): List[(String, String)] =
let facts = inject g into Road/3;
let rules = #{
Path(x, y) :- Road(x, maxSpeed, y), if maxSpeed >= minSpeed.
Path(x, z) :- Path(x, y), Road(y, maxSpeed, z), if maxSpeed >= minSpeed.
};
query facts, rules select (src, dst) from Path(src, dst) |> Foldable.toListDatalog constraints are first-class which means that they may be passed to and returned from functions, stored in data structures, composed with other Datalog constraints, and solved. This makes it possible to express families of Datalog programs.
Datalog Enriched with Lattice Semantics
Flix supports Datalog constraints enriched with lattice semantics.
The program on the right computes the delivery date for a collection of parts. Each part is assembled from a collection of sub-components with various delivery dates. For example, a car depends on a chassis and an engine. To build a car, we need to wait for the chassis and engine to be assembled and then we can assemble the car itself.
Note that parts may depend on sub-components that themselves may depend on other sub-components. In other words, the problem is recursive.
Support for Datalog constraints enriched with lattice semantics is one of the more advanced features of Flix and requires some background knowledge of lattice theory and fixpoints.
let p = #{
/// Parts and the components they depend on.
PartDepends("Car", "Chassis").
PartDepends("Car", "Engine").
PartDepends("Engine", "Piston").
PartDepends("Engine", "Ignition").
/// Time required to assemble a part from its components.
AssemblyTime("Car", 7).
AssemblyTime("Engine", 2).
/// Expected delivery date for certain components.
DeliveryDate("Chassis"; 2).
DeliveryDate("Piston"; 1).
DeliveryDate("Ignition"; 7).
/// A part is ready when it is delivered.
ReadyDate(part; date) :-
DeliveryDate(part; date).
/// Or when it can be assembled from its components.
ReadyDate(part; assemblyTime + componentDate) :-
PartDepends(part, component),
AssemblyTime(part, assemblyTime),
ReadyDate(component; componentDate).
};
// Computes the delivery date for each component.
let r = query p select (c, d) from ReadyDate(c; d)Complete Feature List
- algebraic data types
- pattern matching
- first-class functions
- extensible records
- parametric polymorphism
- traits (i.e. type classes)
- automatic trait derivation
- higher-kinded types
- associated types and effects
- effect polymorphism + subeffecting
- default effect handlers
- purity reflection
- CSP-style concurrency
- buffered & unbuffered channels
- first-class datalog constraints
- seamless interoperability with Java
- unboxed primitives
- keyword-based syntax
- monadic forM expressions
- applicative forA expressions
- string interpolation
- expression holes
- compilation to JVM bytecode
- full tail call elimination
- resilient compiler architecture
- parallel compiler architecture
- human friendly errors
Standard Library with Batteries Included
Flix comes with a fully-featured Standard Library that offers access to 4,000+ functions.
For example, the List module has more than 100 functions and the Foldable trait has more than 47 functions.
The full library can be explored at: https://api.flix.dev/
In addition, Flix also provides access to the entire Java ecosystem via Maven.

Modern Compiler Architecture
Flix features a modern compiler architecture which is resilient, incremental, and parallel.
In Flix, every compiler phase is parallel. The plot on the right shows the speed-up of each compiler phase when run on a 24 core machine.
In other words, Flix can take full advantage of modern hardware, leading to speed-ups of between 5x – 7x on multi-core machines.
Furthermore, the Flix compiler is incremental which leads to significant speed-ups when recompiling code that has already been compiled in the same compiler instance.

Compiler Performance: The Raw Numbers
The following table illustrates the performance of the Flix compiler on an Apple M2 Pro with a 10‑core CPU running on OpenJDK 26:
| Throughput (entire compiler): | 64,956 lines/sec |
| Throughput (frontend only): | 133,287 lines/sec |
The above results can be reproduced by running the commands: java -jar flix.jar Xperf --n 21and java -jar flix.jar Xperf --frontend --n 21.
The Flix compiler achieves these results despite supporting costly programming language features, including: (a) type and effect inference, (b) monomorphization, and (c) whole-program optimization.
The performance of the Flix compiler is mostly determined by CPU performance and memory bandwidth.
Visual Studio Code Support
The Flix compiler integrates with Visual Studio Code to provide a rich development experience.
The VSCode extension uses the real Flix compiler hence there is a 1:1 correspondence between the extension and the compiler.
If VSCode reports no errors there are no errors. Moreover, if there is no error,VSCode will never report a spurious error.
The VSCode extension supports most features, including:
- Semantic Syntax Highlighting
- Code highlighting for *.flix files.
- Diagnostics
- Inline compiler error messages.
- Auto-complete
- Auto-complete as you type.
- Auto-completion is context aware.
- Auto-complete trait instances.
- Type-directed hole completion.
- Snippets
- Auto-complete common code constructs.
- Inlay Hints
- Shows inline type information.
- Type and Effect Hovers
- Hover over any expression to see its type and effect.
- Hover over any local variable or formal parameter to see its type.
- Hover over any function to see its type signature and documentation.
- Jump to Definition
- Jump to the definition of any function.
- Jump to the definition of any local variable.
- Jump to the definition of any enum.
- Find References
- Find all references to a function.
- Find all references to a local variable.
- Find all references to an enum.
- Find all implementations of a trait.
- Symbols
- List all document symbols.
- List all workspace symbols.
- Rename
- Rename local variables.
- Rename functions.
- Code Lenses
- Run main from within the editor.
- Run tests from within the editor.
Tooling Comparison Table
Many programming languages come with a lot of external tooling.
Such tooling must be installed and configured correctly.
In Flix, most tooling is built directly into the compiler.
| Tool | Flix | OCaml | Haskell |
|---|---|---|---|
| Compiler | flix | ocaml | ghc |
| LSP | flix | ocaml-lsp | HLS |
| REPL | flix | utop | ghci |
| Test Framework | flix | OUnit, alcotest | HUnit, tasty |
| Build Tool | flix | dune | cabal, stack |
| Package Manager | flix | opam | cabal |
| Package Repository | GitHub | opam | Hackage |
Actively Developed and Maintained
Flix is actively developed by programming language researchers from Aarhus University in Denmark in collaboration with researchers from the University of Waterloo in Canada, the University of Tübingen in Germany, and University of Copenhagen in Denmark.
Flix is also increasingly developed by a growing community of open source contributors from all over the world.
We invite everyone to contribute.
Project Statistics
| 7,400+ | Merged Pull Requests (PRs) |
| 4,100+ | Resolved Issues (Tickets) |
| 97+ | Contributors |
| 272,000+ | Lines in Compiler Codebase |

Flix runs on Java
Flix targets the Java Virtual Machine (JVM) for a multitude of reasons:
- The JVM has multiple battle-tested, open-source and commercial implementations, including OpenJDK, J9, Azul, Graal, and more.
- JVMs exist for all platforms: Mac, Linux, and Windows.
- Modern JVMs feature multiple state-of-the-art garbage collectors.
- Modern JVMs have excellent support for concurrency and parallelism. In particular, light-weight threads were added in Java 21.
- Excellent tool support, including debuggers and profilers.
- The Java Platform comes with a rich ecosystem of packages which is accessible through integration with Maven.
Funding and Grants
Flix is generously funded by a range of instruments from:
Total Funding: €1.3 million
This funding helps ensure the continuity and independence of the project.
Sponsors and Funding





Collaborators



We kindly thank EJ Technologies for providing us with JProfiler and JetBrains for providing us with IntelliJ IDEA.




