Introduction to Flix

Flix is a principled functional, logic, and imperative programming language developed at Aarhus University, at the University of Waterloo, and by a community of open source contributors.

Flix is inspired by OCaml and Haskell with ideas from Rust and Scala. Flix looks like Scala, but its type system is based on Hindley-Milner. Two unique features of Flix are its polymorphic effect system and its support for first-class Datalog constraints. Flix compiles to efficient JVM bytecode, runs on the Java Virtual Machine, and supports full tail call elimination.

Here are a few Flix programs to illustrate the look and feel of the language:

This program illustrates the use of algebraic data types and pattern matching:

/// An algebraic data type for shapes.
enum Shape {
    case Circle(Int32),          // circle radius
    case Square(Int32),          // side length
    case Rectangle(Int32, Int32) // height and width
}

/// Computes the area of the given shape using
/// pattern matching and basic arithmetic.
def area(s: Shape): Int32 = match s {
    case Circle(r)       => 3 * (r * r)
    case Square(w)       => w * w
    case Rectangle(h, w) => h * w
}

// Computes the area of a 2 by 4.
def main(): Unit & Impure =
    area(Rectangle(2, 4)) |> println

Here is a Flix program using polymorphic records:

/// Returns the area of the polymorphic record `r`.
/// Note that the use of the type variable `a` permits the record `r`
/// to have labels other than `x` and `y`.
def polyArea[a : RecordRow](r: {x:: Int32, y:: Int32 | a}): Int32 = r.x * r.y

/// Computes the area of various rectangle records.
/// Note that some records have additional fields.
def polyAreas(): List[Int32] =
    polyArea({x = 1, y = 2}) ::
    polyArea({x = 2, y = 3, z = 4}) :: Nil

def main(): Unit & Impure =
    polyAreas() |> println

and here is one using processes and channels:

/// A function that sends every element of a list
def send(o: Channel[Int32], l: List[Int32]): Unit & Impure =
    match l {
        case Nil     => ()
        case x :: xs => o <- x; send(o, xs)
    }

/// A function that receives n elements
/// and collects them into a list.
def recv(i: Channel[Int32], n: Int32): List[Int32] & Impure =
    match n {
        case 0 => Nil
        case _ => (<- i) :: recv(i, n - 1)
    }

/// A function that calls receive and sends the result on d.
def wait(i: Channel[Int32], n: Int32, d: Channel[List[Int32]]): Unit & Impure =
    d <- recv(i, n);
    ()

/// Spawn a process for send and wait, and print the result.
def main(): Unit & Impure =
    let l = 1 :: 2 :: 3 :: Nil;
    let c = chan Int32 100;
    let d = chan List[Int32] 100;
    spawn send(c, l);
    spawn wait(c, List.length(l), d);
    println(<- d)

Data Types

Flix comes with a collection of built-in data types, such as booleans, floats and integers, and compound types, such as tuples and records. Moreover, the standard library defines types such as Option[a], Result[t, e], List[a], Set[a], and Map[k, v].

In addition to these types, Flix allows programmers to define their own types, including enumerated types, recursive types, and polymorphic types.

Flix also supports type aliases (new types).

Primitive Types

Flix supports the usual primitive types:

TypeSyntaxDescription
Unit()The unit value.
Booltrue, falseA boolean value.
Char'a', 'b', 'c'A character value.
Float320.0f32, 21.42f32, -21.42f32A 32-bit floating point integer.
Float640.0f64, 21.42f64, -21.42f64A 64-bit floating point integer.
Int80i8, 1i8, -1i8, 127i8, -128i8A signed 8-bit integer.
Int160i16, 123i16, -123i16A signed 16-bit integer.
Int320i32, 123i32, -123i32A signed 32-bit integer.
Int640i64, 123i64, -123i64A signed 64-bit integer.
String"hello", "world"A string value.
BigInt0ii, 123ii, -123iiAn arbitrary precision integer.

Float64 and Int32 values can be written without suffix, i.e. 123.0f64 can simply be written as 123.0 and 123i32 can be written as 123.

Built-in Literals

Flix has built-in syntactic sugar for lists, sets, and maps.

List Literals

A list literal is written using the infix :: constructor. For example:

1 :: 2 :: 3 :: Nil

which is syntactic sugar for:

Cons(1, Cons(2, Cons(3, Nil)))

Set Literals

A set literal is written using the notation Set#{v1, v2, ...}. For example:

Set#{1, 2, 3}

which is syntactic sugar for:

Set.insert(1, Set.insert(2, Set.insert(3, Set.empty())))

Map Literals

A map literal is written using the notion Map#{k1 => v1, k2 => v2, ...}. For example:

Map#{1 => "Hello", 2 => "World"}

which is syntactic sugar for:

Map.insert(1, "Hello", Map.insert(2, "World", Map.empty()))

Tuples

A tuple is a product of values.

A tuple is written with parentheses. For example, here is a 2-tuple (a pair) of an Int32 and a Bool:

(123, true)

The type of the tuple is (Int32, Bool).

We can destructure a tuple using pattern matching. For example:

let t = ("Lucky", "Luke", 42, true); // 4-tuple
let (fstName, lstName, age, male) = t;
lstName

evaluates to the string "Luke".

The Flix prelude defines the fst and snd functions:

let t = (1, 2);
let x = fst(t); // x = 1
let y = snd(t)  // y = 2

which are useful when working with 2-tuples (i.e. pairs). For example:

let l = (1, 1) :: (2, 2) :: Nil; // has type List[(Int32, Int32)]
List.map(fst, l)                 // has type List[Int32]

which evaluates to a list that contains all the first components of the list l.

Enums

Enumerated Types

Enumerated types are used to define a type that has a finite (enumerated) set of values. Enumerated types are useful for things such as modeling compass directions, the cards in a deck, and the days in a week.

For example, here is an enumeration of the days in a week:

enum Weekday {
    case Monday,
    case Tuesday,
    case Wednesday,
    case Thursday,
    case Friday,
    case Saturday,
    case Sunday
}

Here Monday, Tuesday and so on are referred to as the constructors of the enum.

We can refer to a weekday as Monday or Weekday.Monday. The latter is required if we have multiple enums in scope with similarly named constructors.

We can use pattern matching to destruct an enum value. For example:

enum Animal {
    case Cat,
    case Dog,
    case Giraffe
}

def isTall(a: Animal): Bool = match a {
    case Cat        => false
    case Dog        => false
    case Giraffe    => true
}

The function isTall takes a value of type Animal and performs a pattern match on it. If the value is Giraffe the function returns true. Otherwise it returns false.

Flix guarantees that pattern matches are exhaustive, i.e. that all cases have been covered. It is a compile-time error if a pattern match is non-exhaustive. A pattern match can always be made exhaustive by adding a default case as the last case. A default case is written with an underscore case _ => ???.

Recursive Types

Recursive types are used to define types that are self-referential.

For example, we can define a binary tree of integers as follows:

enum Tree {
    case Leaf(Int32),
    case Node(Tree, Tree)
}

A tree is either a Leaf with an Int32 value or an internal Node with a left and a right sub-tree. Note that the definition of Tree refers to itself.

We can write a function, using pattern matching, to compute the sum of all integers in such a tree:

def sum(t: Tree): Int32 = match t {
    case Leaf(x)    => x
    case Node(l, r) => sum(l) + sum(r)
}

The sum function pattern matches on a tree value. If the tree is a leaf its value is simply returned. Otherwise the function recurses on both subtrees and adds their results.

Polymorphic Types

Polymorphic types are types parameterized by other types. For example, we can write:

enum Bottle[a] {
    case Empty,
    case Full(a)
}

def isEmpty[a](b: Bottle[a]): Bool = match b {
    case Empty   => true
    case Full(_) => false
}

Here the Bottle type is parameterized by the type parameter a. In Flix, type parameters, like ordinary parameters are always written in lowercase. The Bottle type has two cases: either the bottle is empty (and contains no value) or it is full (and contains one value of type a). The isEmpty function takes a bottle, type parameterized by a, and determines if the bottle is empty.

The careful reader might have noticed that Bottle is equivalent to the more well-known Option type.

In general, polymorphic types can have more than one type argument. For example, the standard library implement of the Result has two type parameters:

enum Result[t, e] {
    case Ok(t),
    case Err(e)
}

Shorthand Enum Syntax

A typical enum may look like:

enum Weekday {
    case Monday,
    case Tuesday,
    case Wednesday,
    case Thursday,
    case Friday,
    case Saturday,
    case Sunday
}

The same enum can also be declared as:

enum Weekday {
    case Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
}

This shorthand syntax is always available, but should only be used for simple enums.

Singleton Enum Syntax

An enum with a single case:

enum USD {
  case USD(Int32)
}

can be shortened to:

enum USD(Int32)

Type Aliases

Type aliases introduces a short-hand name for a type. For example:

///
/// A type alias for a map from keys of type `k`
/// to values of type `Result[v, String]`
///
type alias M[k, v] = Map[k, Result[v, String]]

def foo(): M[Bool, Int32] = Map#{true => Ok(123)}

A type alias does not define a new distinct type. Rather a type alias is simply a syntactic short-hand for a (usually complex) type.

The Flix compiler expands type aliases before type checking. Consequently, type errors are always reported with respect to the actual underlying types.

Note: A type alias cannot be recursively defined in terms of itself. The Flix compiler will detect and report such recursive cycles.

Identifiers

Flix supports several different kinds of identifier:

  • Uppercase name: An identifier that starts with an uppercase letter followed by any number of uppercase and lowercase letters, underscore, and exclamation mark (AZ, az, _, !).
    • e.g. String, ALL_UPPER, Shriek!
    • Can be used to name: namespaces, annotations, type classes, effects, predicates (within datalog), tags (within enums), types
  • Lowercase name: An identifier that starts with aa lowercase letter followed by any number of uppercase and lowercase letters, underscore, and exclamation mark (AZ, az, _, !).
    • e.g. anIdentifier, x, this_and_that
    • Can be used to name: annotations, attributes (within datalog), functions, fields (within records), variables
  • Greek name: An identifier consisting of any combination of letters from the Greek alphabet (the unicode range U+0370 to U+03FF).
    • e.g. Χαίρετε, αναγνωριστικό
    • Can be used to name: functions, variables
  • Math name: An identifier consisting of any combination of math symbols (the unicode range U+2190 to U+22FF).
    • e.g. , ,
    • Can be used to name: functions, variables
  • Operator name: An identifier of minimum length 2 consisting of any combination of +, -, *, <, >, =, !, &, |, ^, and $.
    • e.g. >==>, <*>
    • Can be used to name: functions

Note that greek letters, math symbols, and operator letters cannot be combined within a single identifier.

Reserved Identifiers

The following are reserved by Flix and cannot be redefined within user code:

!=, **, .., ::, :=, <-, <=, ==, =>, >=, or, &&&, <+>, <<<, <=>, >>>, ???, ^^^, and, mod, not, rem, |||, ~~~, $DEFAULT$, *, +, -, /, :, <, >, @, Absent, Bool, Impure, Nil, Predicate, Present, Pure, Read, RecordRow, Region, SchemaRow, Type, Write, alias, case, catch, chan, class, def, deref, else, enum, false, fix, force, if, import, inline, instance, into, lat, law, lawful, lazy, let, let*, match, namespace, null, opaque, override, pub, ref, region, reify, reifyBool, reifyEff, reifyType, rel, sealed, set, spawn, Static, true, type, use, where, with, discard, object

Functions

Functions and higher-order functions are the key building block of a functional programming language.

In Flix, top-level functions are defined with the def keyword. For example:

def add(x: Int32, y: Int32): Int32 = x + y + 1

A definition consists of the function name followed by an argument list, the return type, and the function body. Although Flix supports type inference, top-level function definitions must declare the type of their arguments and their return type.

In Flix, all function arguments and local variables must be used. If a function argument is not used it must be prefixed with an underscore to explicitly mark it as unused.

First-Class and Higher-Order Functions

A higher-order function is a function that takes a parameter which is itself a function. For example:

def twice(f: Int32 -> Int32, x: Int32): Int32 = f(f(x))

Here the twice function takes two arguments, a function f and an integer x, and applies f to x two times.

We can pass a lambda expression to the twice function:

twice(x -> x + 1, 42)

which evaluates to 44 since 42 is incremented twice.

We can also define a higher-order function that requires a function which takes two arguments:

def twice(f: (Int32, Int32) -> Int32, x: Int32): Int32 =
    f(f(x, x), f(x, x))

which can be called as follows:

twice((x, y) -> x + y, 42)

We can call a higher-order function with a top-level function as follows:

def inc(x: Int32): Int32 = x + 1

def twice(f: Int32 -> Int32, x: Int32): Int32 = f(f(x))

twice(inc, 42)

Function Type Syntax

Depending on the number of arguments to a function, the syntax for the function type differs:

Unit -> Int32                // For nullary functions
Int32 -> Int32               // For unary functions
(Int32, Int32, ...) -> Int32 // For the rest

Function Composition

Flix supports several operators for function composition and pipelining:

let f = x -> x + 1;
let g = x -> x * 2;
let h = f >> g;     // equivalent to x -> g(f(x))

Here >> is forward function composition.

We can also write function applications using the pipeline operator:

List.range(1, 100) |>
List.filter(x -> x mod 2 == 0) |>
List.map(x -> x * x) |>
println;

Here x |> f is equivalent to the function application f(x).

Curried by Default

Functions are curried by default. A curried function can be called with fewer arguments than it declares returning a new function that takes the remainder of the arguments. For example:

def sum(x: Int32, y: Int32): Int32 = x + y

def main(): Unit & Impure =
    let inc = sum(1);
    inc(42) |> println

Here the sum function takes two arguments, x and y, but it is only called with one argument inside main. This call returns a new function which is similar to sum, except that in this function x is always bound to 1. Hence when inc is called with 42 it returns 43.

Currying is useful in many programming patterns. For example, consider the List.map function. This function takes two arguments, a function of type a -> b and a list of type List[a], and returns a List[b] obtained by applying the function to every element of the list. Now, if we combine currying with the pipeline operator |> we are able to write:

def main(): Unit & Impure =
    List.range(1, 100) |>
    List.map(x -> x + 1) |>
    println

Here the call to List.map passes the function x -> x + 1 which returns a new function that expects a list argument. This list argument is then supplied by the pipeline operator |> which, in this case, expects a list and a function that takes a list.

Pipelines

Flix supports the pipeline operator |> which is simply a prefix version of function application (i.e. the argument appears before the function).

The pipeline operator can often be used to make functional code more readable. For example:

let l = 1 :: 2 :: 3 :: Nil;
l |>
List.map(x -> x * 2) |>
List.filter(x -> x < 4) |>
List.count(x -> x > 1)

Here is another example:

"Hello World" |> String.toUpperCase |> println

Pure, Impure, and Effect Polymorphic Functions

In Flix every function is pure, impure, or effect polymorphic.

The Flix type and effect system ensures that a pure function always returns the same result when given the same arguments and that it cannot have (observable) side effects.

In Flix every function definition is implicitly marked as Pure. For example, the function definition:

def add(x: Int32, y: Int32): Int32 = x + y

is actually equivalent to:

def add(x: Int32, y: Int32): Int32 & Pure = x + y

A function that prints to the console is Impure and must be marked as such:

def addAndPrint(x: Int32, y: Int32): Int32 & Impure =
    let r = x + y;
    println(r);
    r

since the type signature of the library function println specifies that it is Impure.

The purity (or impurity) of a higher-order function may depend on the purity of its argument(s). For example, whether List.map is pure or impure depends on whether function we map is pure or impure. Fortunately Flix can model such behavior using effect polymorphism. For example:

def map(f: a -> b & ef, l: List[a]): List[b] & ef = ???

Here the signature of map captures that if the function argument f has type a -> b with effect ef then the effect of map itself is ef. This means that if map is called with a pure (resp. impure) function argument then the overall expression is pure (resp. impure). For example:

List.map(x -> x + 123, l)    // pure
List.map(x -> println(x), l) // impure

Design Note

The Flix standard library enforces several program invariants using purity. For example, in Flix, the Eq and Order type classes require that their operations are pure. This ensures that collections, such as lists, sets, and maps, do not leak internal implementation details.

Operators

Flix has a number of built-in unary and infix operators. In addition Flix supports infix function application by enclosing the function name in backticks. For example:

123 `sum` 456

is equivalent to the normal function call:

sum(123, 456)

In addition, a function named with an operator name (some combination of +, -, *, <, >, =, !, &, |, ^, and $) can also be used infix. For example:

def <*>(x: Int32, y: Int32): Int32 = ???

can be used as follows:

1 <*> 2

Precedence

  1. Unary operators (+, -, ~~~, and not)
  2. User-defined operators (including operators defined in the standard library such as |>)
  3. Functions applied infix with backticks
  4. Composition (<+>)
  5. Multiplicative (**, *, /, mod, and rem)
  6. Additive (+ and -)
  7. Shift (<<< and >>>)
  8. Comparison (<=, >=, <, and >)
  9. Equality (==, !=, and <==>)
  10. Bitwise And (&&&)
  11. Bitwise Xor (^^^)
  12. Bitwise Or (|||)
  13. Logical and
  14. Logical or
  15. Channel <-

Immutable Data

Flix is a functional programming language and the bread-and-butter of the language is in its immutable data types. We have already seen enumerated types. In this chapter, we look at lists and records.

Lists

The bread and butter of functional programming is list processing. A list is either the empty list, written as Nil, or a cons cell, written as x :: xs where x is the head element and xs is the tail of the list. The List type is polymorphic so you can have a list of integers, written as List[Int32], or a list of strings written as List[String]. The List type and list operations are part of the Flix standard library.

We write the empty list as follows:

Nil

We can construct a list of strings with the strings "Hello" and "World" as follows:

"Hello" :: "World" :: Nil

Given a list there are many useful operations we can perform on it.

For example, we can compute the length of a list as follows:

List.length(1 :: 2 :: 3 :: Nil)

We can also reverse the order of elements in a list:

List.reverse(1 :: 2 :: 3 :: Nil)

We can append two lists using the List.append function as follows:

let xs = (1 :: 2 :: 3 :: Nil);
let ys = (4 :: 5 :: 6 :: Nil);
List.append(xs, ys)

Or, alternatively, we can use the built-in append operator ::: as follows:

let xs = (1 :: 2 :: 3 :: Nil);
let ys = (4 :: 5 :: 6 :: Nil);
xs ::: ys

Flix has an extensive collection of functions to operate on lists.

Here are some of the most common:

List.count(x -> x == 1, 1 :: 2 :: 3 :: Nil);
List.filter(x -> x == 1, 1 :: 2 :: 3 :: Nil);
List.map(x -> x + 1, 1 :: 2 :: 3 :: Nil);
List.foldLeft((x, y) -> x + y, 0, 1 :: 2 :: 3 :: Nil)

And here are some more exotic functions:

List.intersperse("X", "a" :: "b" :: "c" :: Nil)

which inserts "X" between every element in the list.

let l1 = "X" :: "Y" :: Nil;
let l2 = ("a" :: "b" :: Nil) :: ("c" :: "d" :: Nil) :: Nil;
List.intercalate(l1, l2)

which inserts the list l1 between every element in the list l2.

We can write our own recursive functions to operate on lists.

For example, here is an implementation of the map function:

///
/// Returns the result of applying `f` to every element in `l`.
/// That is, the result is of the form: `f(x1) :: f(x2) :: ...`.
///
pub 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)
}

Records

Flix supports row polymorphic extensible records. Flix records are immutable (but may contain mutable reference cells).

Record Literals

A record literal is written with curly braces:

{ x = 1, y = 2 }

which has the record type { x :: Int32, y :: Int32 }.

The order of fields in a record does not matter, hence the above record is equivalent to the record:

{ y = 2, x = 1 }

which has the record type { y :: Int32, x :: Int32 }. This type is equivalent to the record type { x :: Int32, y :: Int32 }. That is, the order of fields within a record type do not matter.

Field Access

We can access the field of a record using a dot:

let p = { x = 1, y = 2 };
p.x + p.y

The Flix type system ensures that we cannot access a field that does not exist.

Records are immutable. A record, once constructed, cannot have the values of any of its fields changed.

Field Update

While records are immutable, we can construct a new record with an updated field value:

let p1 = { x = 1, y = 2 };
let p2 = { x = 3 | p1 };
p1.x + p2.x

The expression { x = 3 | p1 } updates the record p1 with a new value of its x field. Note that updating a field requires that the field exists on the record (!) A record cannot be updated with a new field, but it can be extended with a new field, as we shall see later.

Record Extension

We can add a new field to an existing record as follows:

let p1 = { x = 1, y = 2 };
let p2 = { +z = 3 | p1 };
p1.x + p1.y + p2.z

Here the expression { +z = 3 | p1 } extends the record p1 with a new field z such that the result has three fields: x, y, and z all of which are of Int32 type.

Record Restriction

Similarly to record extension, we can also remove a field from a record:

let p1 = { x = 1, y = 2 };
let p2 = { -y | p1 };

Here the record p2 has the same fields as p1 except that the y field has been removed.

Row Polymorphism: Open and Closed Records

A function may specify that it requires a record with two fields:

def f(r: {x :: Int32, y :: Int32}): Int32 = r.x + r.y

We can call this function with the records { x = 1, y = 2 } and { y = 2, x = 1 }, but we cannot call it with the record { x = 1, y = 2, z = 3 } since the signature of f demands a record with exactly two fields: x and y. We say that the record r is closed.

We can lift this restriction by using row polymorphism:

def g(r: {x :: Int32, y :: Int32 | s}): Int32 = r.x + r.y

We can call this function with any record as long as it has x and y fields which are of type Int32. We say that the record type of r is open.

Named Parameters with Records

When a function has multiple parameters that share the same type, it is easy to get confused about the right argument order. For example, what does String.contains("Hello","Hello World") return? What does String.contains("Hello World", "Hello") return?

A common solution to this problem is to use named parameters. Flix supports a form of named parameters building on records. For example, we can write a function translate to translate from one language to another as follows:

def translate(from: {from :: Language}, to: {to :: Language}, text: String): String = ???

We can call this function as follows:

translate({from = English}, {to = French}, "Where is the library?")

Since such verbosity gets tedious, we can also use the syntactic sugar:

translate(from = English, to = French, "Where is the library?")

which is equivalent to the above.

Illegal Record Field: length

A record field cannot be named length. The reason is that the expression:

a.length

is understood as accessing the length of the array a, not as accessing a field named length on a record a.

Mutable Data

We now turn our attention to mutable data.

All mutable data is built from two the basic mutable data types:

  • References
  • Arrays

References

Flix supports references in the ML-tradition. The three key operations are ref e, deref e, and e := e. The ref e operation allocates a reference cell in the heap and returns its location, the deref operation dereferences a location and returns the content of a reference cell, and finally the assigment := operation changes the value of a reference cell. Informally, a reference cell can be thought of as an "object" with a single field that can be changed.

All operations on references are impure. As such, all functions that use references must be marked as Impure or be casted to Pure.

Allocation

A reference cell is allocated as follows:

ref 42

which evaluates to a value of type Ref[Int32] which is a reference (pointer) to a single memory cell that holds the value 42.

Dereference

A reference cell is accessed (de-referenced) as follows:

let l = ref 42;
deref l

which evaluates to 42 as expected.

Assignment

A reference cell can have its value updated as follows:

let l = ref 42;
l := 84;
deref l

which evaluates to 84 as expected.

Example: A Simple Counter

The following program models a simple counter that can be incremented:

enum Counter {
    case Counter(Ref[Int32])
}

def newCounter(): Counter & Impure = Counter(ref 0)

def getCount(c: Counter): Int32 & Impure =
    let Counter(l) = c;
    deref l

def increment(c: Counter): Unit & Impure =
    let Counter(l) = c;
    l := (deref l) + 1

def f(): Unit & Impure =
    let c = newCounter();
    increment(c);
    increment(c);
    increment(c);
    getCount(c) |> println

Note that the newCounter, getCount, increment and f functions must all be marked as Impure.

Aliasing and References to References

References naturally support aliasing since that is exactly their purpose. For example:

let l1 = ref 42;
let l2 = l1;
l2 := 84;
deref l1

Evaluates to 84 because the reference cell that l1 points to is modified through the alias l2.

References can point-to references as the following example illustrates:

let l1 = ref 42;
let l2 = ref l1;
deref (deref l2)

Evaluates to 42 as expected.

Design Note

Flix does not support any notion of global mutable state. If you need to maintain a program-wide counter (or other mutable state) then you have to allocate it in the main function and explicitly thread it through the program.

Mutable Tuples and Records

Flix tuples and records are immutable. However, tuples and records may contain mutable references.

For example, here is a pair that contains two mutable references:

let p = (ref 1, ref 2);
fst(p) := 123

The type of the pair is (Ref[Int32], Ref[Int32]). The assignment does not change the pair itself (it is immutable), but rather changes the value of the reference cell in the first component of the pair.

Similarly, here is a record that contains two mutable references:

let r = { fstName = ref "Lucky", lstName = ref "Luke" };
r.fstName := "Unlucky"

The type of the record is { fstName :: Ref[String], lstName :: Ref[String] }. Again, the assignment does not change the record itself, but rather changes the value of the reference cell corresponding to the fstName field.

Arrays

While Flix recommends the use of immutable data structures (such as immutable lists, sets, and maps), mutable arrays may be useful for performance critical code.

We recommend that arrays are used sparingly and that when possible their use is hidden as an implementation detail. For example, the Flix Datalog engine uses arrays internally but exposes a functional (immutable) interface.

Flix uses monomorphization and consequently primitive arrays are not boxed. For example, the representation of an Array[Int32] is compact and efficient.

All operations on arrays are impure. As such, all functions that use arrays must be marked as Impure or be casted to Pure. However, accessing the length of an array is pure since the size of an array cannot change after it has been created.

Arrays should only be used for low-level code. The MutList data structure, available in the standard library, provides a mutable dynamically-expanding data structure similar to java.util.ArrayList. Its implementation is backed by an array that is dynamically resized and it provides amortized O(1) push operations.

Array Literals

An array literal is of the form [e1, e2, ... en]. For example, the expression:

[1, 2, 3, 4]

evaluates to an array with the four elements: 1, 2, 3, 4.

In some cases it is useful to allocate a large array filled with the same value. The expression:

["Hello World"; 100]

evaluates to an array of length 100 where every entry contains the string "Hello World".

Design Note

Flix does not allow the allocation of an array without assigning a "default value" to each entry in the array.

Reading and Writing from Arrays

Arrays can be accessed and updated using standard syntax. For example:

let a = [0; 10];
a[0] = 21;
a[1] = 42;
a[0] + a[1]

evaluates to 63, as expected.

Array Slicing

Arrays can be sliced. Slicing an array (shallowly) copies a subrange of the array. For example:

let a = [1, 2, 3, 4, 5];
a[2..4]

evaluates to the array [3, 4].

The start or end index may be omitted. For example:

let a = [1, 2, 3, 4, 5];
let a1 = a[2..]; // evaluates to [3, 4, 5]
let a2 = a[..4]  // evaluates to [1, 2, 3, 4]

If both the start and end index are omitted the entire array is copied. For example:

let a = [1, 2, 3, 4, 5];
a[..]

evaluates to the (copied) array [1, 2, 3, 4, 5].

Design Note

Slicing an array using the same start and end index returns the empty array. For example, [0, 1, 2, 3][2..2] evaluates to [].

Warning

Slicing with negative indices is undefined and results in runtime errors.

Array Length

The length of an array is accessed as follows:

let a = [1, 2, 3, 4, 5];
a.length

which evaluates to 5.

Pattern Matching

Let Pattern Match

In addition to the pattern match construct, a let-binding can be used to destruct a value. For example:

let (x, y, z) = (1, 2, 3)

Binds the variables x, y, and z to the values 1, 2, and 3, respectively.

Any exhaustive pattern may be used in a let-binding. For example:

let (x, Foo(y, z)) = (1, Foo(2, 3))

is legal provided that the Foo constructor belongs to a type where it is the only constructor.

The following let-bindings are illegal because they are not exhaustive:

let (1, 2, z) = ...
let Some(x) = ...

The Flix compiler will reject such non-exhaustive patterns.

Match Lambdas

Pattern matches can also be used with lambda expressions. For example:

List.map(match (x, y) -> x + y, (1, 1) :: (2, 2) :: Nil)

is equivalent to:

List.map(w -> match w { case (x, y) => x + y }, (1, 1) :: (2, 2) :: Nil)

As for let-bindings, such pattern matches must be exhaustive.

Note the difference between the two lambda expressions:

let f = (x, y, z) -> x + y + z + 42i32
let g = match (x, y, z) -> x + y + z + 42i32

Here f is a function that expects three Int32 arguments,whereas g is a function that expects one three tuple (Int32, Int32, Int32) argument.

Let* (Do-notation)

Flix supports a feature similar to do-notation in Haskelland for-comprehensions in Scala.

The following monadic code:

use Option.flatMap;
let o1 = Some(21);
let o2 = Some(42);
flatMap(x -> flatMap(y -> Some(x + y), o2), o1)

May be expressed more concisely as:

use Option.flatMap;
let* o1 = Some(21);
let* o2 = Some(42);
Some(o1 + o2)

where each let* corresponds to a flatMap use.

Concurrency with Channels and Processes

Flix supports CSP-style concurrency with channels and processes inspired by Go.

Spawning Processes

We can spawn a process with the spawn keyword:

spawn (1 + 2)

This spawns a process that computes 1 + 2 and throws the result away. The spawn expression always returns Unit. We can spawn any expression, but we typically spawn functions to run in a new process:

def sum(x: Int32, y: Int32): Int32 = x + y

def main(): Unit & Impure = spawn sum(1, 2)

Communicating with Channels

To communicate between processes we use channels. A channel allows two or more processes to exchange data by sending immutable messages to each other.

A channel comes in one of two variants: buffered or unbuffered.

A buffered channel has a size, set at creation time, and can hold that many messages. If a process attempts to put a message into a buffered channel that is full, then the process is blocked until space becomes available. If, on the other hand, a process attempts to get a message from an empty channel, the process is blocked until a message is put into the channel.

An unbuffered channel works like a buffered channel of size zero; for a get and a put to happen both processes must rendezvous (block) until the message is passed from sender to receiver.

Here is an example of sending and receiving a message over a channel:

def send(c: Channel[Int32]): Unit & Impure = c <- 42; ()

def main(): Unit & Impure =
    let c = chan Int32 0;
    spawn send(c);
    <- c;
    ()

Here the main function creates an unbuffered channel c, spawns the send function, and waits for a message from c. The send function simply puts the value 42 into the channel.

Selecting on Channels

We can use the select expression to receive a message from a collection of channels. For example:

def meow(c: Channel[String]): Unit & Impure = c <- "Meow!"; ()

def woof(c: Channel[String]): Unit & Impure = c <- "Woof!"; ()

def main(): Unit & Impure =
    let c1 = chan String 1;
    let c2 = chan String 1;
    spawn meow(c1);
    spawn woof(c2);
    select {
        case m <- c1 => m
        case m <- c2 => m
    } |> println

Many important concurrency patterns such as producer-consumer and load balancers can be expressed using the select expression.

Selecting with Default

In some cases, we do not want to block until a message arrives, potentially waiting forever. Instead, we want to take some alternative action if no message is readily available. We can achieve this with a default case as shown below:

def main(): Unit & Impure =
    let c1 = chan String 1;
    let c2 = chan String 1;
    select {
        case m <- c1 => "one"
        case m <- c2 => "two"
        case _       => "default"
    }

Here a message is never sent to c1 nor c2. The select expression tries all cases, and if no channel is ready, it immediately selects the default case. Hence using a default case prevents the select expression from blocking forever.

Selecting with Tickers and Timers

As an alternative to a default case, we can use tickers and timers to wait for pre-defined periods of time inside a select expression.

For example, here is a program that has a slow function that takes a minute to send a message on a channel, but the select expression relies on Timer.seconds to only wait 5 seconds before giving up:

def slow(c: Channel[String]): Unit & Impure =
    import static java.lang.Thread.sleep(Int64): Unit & Impure;
    sleep(Time/Duration.oneMinute() / 1000000i64);
    c <- "I am very slow";
    ()

def main(): Unit & Impure =
    use Concurrent/Channel/Timer.seconds;
    let c = chan String 1;
    spawn slow(c);
    select {
        case m <- c              => m
        case _ <- seconds(5i64)  => "timeout"
    } |> println

This program prints the string "timeout" after five seconds.

Flix also supports tickers which are similar to timers, but instead of sending a message one after a pre-defined time they repeatedly send a message every tick.

Planned Feature

Flix does not currently support put operations in select expressions. This is something that we might support in the future.

Parallelism

Note: This feature is available in Flix 0.31 and onwards.

We have seen how the spawn expression can be used to run an expression in another light-weight thread:

spawn (1 + 2)

This allows us to write both concurrent and parallel programs. The downside is that we must manually coordinate communication between threads using channels. A more light-weight approach is to use the par expression.

The par expression:

par f(e1, e2, e3)

evaluates the function expression f, and its argument expressions e1, e2, and e3 in parallel. Once all four expressions have been reduced to a value, the function f is called with the arguments.

This allows us to write a parallel List.map function:

def parMap(f: a -> b, l: List[a]): List[b] = match l {
    case Nil     => Nil
    case x :: xs => par f(x) :: parMap(f, xs)
}

This function will evaluate f(x) and parMap(f, xs) in parallel and combine their results.

Effect System

The Flix type and effect system separates pure and impure expressions. A pure expression is guaranteed to be referentially transparent. A pure function always returns the same value when given the same argument(s) and cannot have any (observable) side-effects.

For example, the following expression is of type Int32 and is Pure:

1 + 2 : Int32 & Pure

whereas the following expression is Impure:

println("Hello World") : Unit & Impure

A higher-order function can specify that a function argument must be pure, impure, or that it is effect polymorphic.

For example, the definition of Set.exists requires that its function argument f is pure:

// The syntax a -> Bool is short-hand for a -> Bool & Pure
def exists(f: a -> Bool, s: Set[a]): Bool = ???

The requirement that f must be pure ensures that implementation details do not leak. For example, since f is pure it cannot be used to determine in what order the elements of the set are traversed. If f was impure such details could leak, e.g. by passing a function that also prints the current element, revealing the internal element order inside the set.

The type and effect system is sound, but not complete. That is, if a function is pure then it cannot cause an effect, whereas if a function is impure then it may, but does not necessarily, cause an effect. For example, the following expression is impure even though it cannot produce an effect at run-time:

if (1 == 2) println("Hello World!") else ()

A higher-order function can also be effect polymorphic: its effect(s) can depend on its argument(s).

For example, the standard library definition of List.map is effect polymorphic:

def map(f: a -> b & ef, xs: List[a]): List[b] & ef

The List.map function takes a function f from elements of type a to b with effect ef. The effect of the function itself is ef. Consequently, if List.map is invoked with a pure function then the entire expression is pure whereas if it is invoked with an impure function then the entire expression is impure.

A higher-order function that takes multiple function arguments may combine their effects.

For example, the standard library definition of forward function composition >> is pure if both its function arguments are pure:

def >>(f: a -> b & ef1, g: b -> c & ef2): a -> c & (ef1 and ef2) = x -> g(f(x))

The type and effect signature can be understood as follows: The >> function takes two function arguments: f with effect ef1 and g with effect ef2. The effect of >> is effect polymorphic in the conjunction of ef1 and ef2. If both are pure (their effect is true) then the overall expression is pure (true). Otherwise it is impure.

The type and effect system allows arbitrary boolean expressions to control the purity of function arguments.

For example, it is possible to express a higher-order function h that accepts two function arguments f and g of which at most one is impure:

def h(f: a -> b & ef1, g: b -> c & (not ef1 or ef2)): Unit

Note that here ef1 and ef2 are arbitrary boolean variables which are not directly associated with the effect of f or g (like it was the case in the simpler example above). In general, the possible effects for argument functions and the to-be-defined function are described by arbitrary boolean expressions. Here the possible effects of g (whether it can be pure or impure) are specified by the boolean not ef1 or ef2. For a specific combination of pure and impure to be accepted, there must be an assignment of the boolean variables ef1 and ef2 to true and false such that the boolean expressions for pure arguments evaluate to true and those for impure arguments evaluate to false.

If in this example h is called with a function argument f which is impure, then the variable ef1 must be false and thus the second argument must be pure (because not ef1 or ef2 will always be true, no matter how we choose ef2). Conversely, if f is pure, then ef1 must be true and g may be pure (ef2 = true) or impure (ef2 = false). It is a compile-time error to call h with two impure functions.

The type and effect system can be used to ensure that statement expressions are useful, i.e. that if an expression or function is evaluated and its result is discarded then it must have a side-effect. For example, compiling the program fragment below:

List.map(x -> x + 1, 1 :: 2 :: Nil);
123

causes a compiler error:

-- Redundancy Error -------------------------------------------------- ???

>> Useless expression: It has no side-effect(s) and its result is discarded.

2 |     List.map(x -> x + 1, 1 :: 2 :: Nil);
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
useless expression.


Possible fixes:

(1)  Use the result computed by the expression.
(2)  Remove the expression statement.
(3)  Introduce a let-binding with a wildcard name.

because it is non-sensical to evaluate the pure expression List.map(x -> 2 * x, 1 :: 2 :: Nil) and then to discard its result. Most likely the programmer wanted to use the result (or alternatively the expression is redundant and could be deleted). Consequently, Flix rejects such programs.

In summary, Flix function types are of the form:

Function TypeSyntaxShort Hand
The type of a pure function from a to b.a -> b & Purea -> b
The type of an effect polymorphic function from a to b with effect ef.a -> b & efn/a
The type of an effect polymorphic function from a to b with effect ef1 and ef2 (i.e. pure if both ef1 and ef2 are true.)a -> b & (ef1 and ef2)n/a

Fixpoints

A unique feature of Flix is its built-in support for fixpoint computations on constraint on relations and constraint on lattices.

We assume that the reader is already familiar with Datalog and focus on the Flix specific features.

Using Flix to Solve Constraints on Relations

We can use Flix to solve a fixpoint computation inside a function.

For example, given a set of edges s, a src node, and dst node, compute if there is a path from src to dst. We can elegantly solve this problem as follows:

def isConnected(s: Set[(Int32, Int32)], src: Int32, dst: Int32): Bool =
    let rules = #{
        Path(x, y) :- Edge(x, y).
        Path(x, z) :- Path(x, y), Edge(y, z).
    };
    let edges = inject s into Edge;
    let paths = query edges, rules select true from Path(src, dst);
    not (paths |> Array.isEmpty)

def main(): Unit & Impure =
    let s = Set#{(1, 2), (2, 3), (3, 4), (4, 5)};
    let src = 1;
    let dst = 5;
    if (isConnected(s, src, dst)) {
        println("Found a path between \${src} and \${dst}!")
    } else {
        println("Did not find a path between \${src} and \${dst}!")
    }

The isConnected function behaves like any other function: We can call it with a set of edges (Int32-pairs), an Int32 source node, and an Int32 destination node. What is interesting about isConnected is that its implementation uses a small Datalog program to solve the task at hand.

In the isConnected function, the local variable rules holds a Datalog program fragment that consists of two rules which define the Path relation. Note that the predicate symbols, Edge and Path do not have to be explicitly introduced; they are simply used. The local variable edges holds a collection of edge facts that are obtained by taking all the tuples in the set s and turning them into Edge facts. Next, the local variable paths holds the result of computing the fixpoint of the facts and rules (edges and rules) and selecting the Boolean true if there is a Path(src, dst) fact. Note that here src and dst are the lexically-bound function parameters. Thus, paths is either an empty array (no paths were found) or a one-element array (a path was found), and we simply return this fact.

Flix is strongly typed. Any attempt to use predicate symbol with terms of the wrong type (or with the wrong arity) is caught by the type checker. Note also that Flix supports type inference, hence we did not have to declare the type of Edge nor of Path.

Stratified Negation

Flix supports stratified negation which allow restricted use of negation in rule bodies. For example:

def main(): Unit & Impure =
    let movies = #{
        Movie("The Hateful Eight").
        Movie("Interstellar").
    };
    let actors = #{
        StarringIn("The Hateful Eight", "Samuel L. Jackson").
        StarringIn("The Hateful Eight", "Kurt Russel").
        StarringIn("The Hateful Eight", "Quentin Tarantino").
        StarringIn("Interstellar", "Matthew McConaughey").
        StarringIn("Interstellar", "Anne Hathaway").
    };
    let directors = #{
        DirectedBy("The Hateful Eight", "Quentin Tarantino").
        DirectedBy("Interstellar", "Christopher Nolan").
    };
    let rule = #{
        MovieWithoutDirector(title) :-
            Movie(title),
            DirectedBy(title, name),
            not StarringIn(title, name).
    };
    query movies, actors, directors, rule
        select title from MovieWithoutDirector(title) |> println

The program defines three local variables that contain information about movies, actors, and directors. The local variable rule contains a rule that captures all movies where the director does not star in the movie. Note the use negation in this rule. The query returns an array with the string "Interstellar" because Christopher Nolan did not star in that movie.

Design Note

Flix enforces that programs are stratified, i.e. a program must not have recursive dependencies on which there is use of negation. If there is, the Flix compiler rejects the program.

Programming with First-class Constraints

A unique feature of Flix is its support for first-class constraints. A first-class constraint is a value that can be constructed, passed around, composed with other constraints, and ultimately solved. The solution to a constraint system is another constraint system which can be further composed. For example:

def getParents(): #{ ParentOf(String, String) | r } = #{
    ParentOf("Pompey", "Strabo").
    ParentOf("Gnaeus", "Pompey").
    ParentOf("Pompeia", "Pompey").
    ParentOf("Sextus", "Pompey").
}

def getAdoptions(): #{ AdoptedBy(String, String) | r } = #{
    AdoptedBy("Augustus", "Caesar").
    AdoptedBy("Tiberius", "Augustus").
}

def withAncestors(): #{ ParentOf(String, String),
                        AncestorOf(String, String) | r } = #{
        AncestorOf(x, y) :- ParentOf(x, y).
        AncestorOf(x, z) :- AncestorOf(x, y), AncestorOf(y, z).
}

def withAdoptions(): #{ AdoptedBy(String, String),
                        AncestorOf(String, String) | r } = #{
    AncestorOf(x, y) :- AdoptedBy(x, y).
}

def main(): Unit & Impure =
    let c = false;
    if (c) {
        query getParents(), getAdoptions(), withAncestors()
            select (x, y) from AncestorOf(x, y) |> println
    } else {
        query getParents(), getAdoptions(), withAncestors(), withAdoptions()
            select (x, y) from AncestorOf(x, y) |> println
    }

The program uses three predicate symbols: ParentOf, AncestorOf, and AdoptedBy. The getParentsfunction returns a collection of facts that represent biological parents, whereas the getAdoptions function returns a collection of facts that represent adoptions. The withAncestors function returns two constraints that populate the AncestorOf relation using the ParentOf relation. The withAdoptions function returns a constraint that populates the ParentOf relation using the AdoptedBy relation.

In the main function the local variable c controls whether we query a Datalog program that only considers biological parents or if we include adoptions.

As can be seen, the types the functions are row-polymorphic. For example, the signature of getParents is def getParents(): #{ ParentOf | r } where r is row polymorphic type variable that represent the rest of the predicates that the result of the function can be composed with.

Design Note

The row polymorphic types are best understood as an over-approximation of the predicates that may occur in a constraint system. For example, if a constraint system has type #{ A(String), B(Int32, Int32) } that does necessarily mean that it will contain facts or rules that use the predicate symbols A or B, but it does guarantee that it will not contain any fact or rule that refer to a predicate symbol C.

Polymorphic First-class Constraints

Another unique feature of Flix is its support for first-class polymorphic constraints. That is, constraints where one or more constraints are polymorphic in their term types. For example:

def edgesWithNumbers(): #{ LabelledEdge(String, Int32 , String) | r } = #{
    LabelledEdge("a", 1, "b").
    LabelledEdge("b", 1, "c").
    LabelledEdge("c", 2, "d").
}

def edgesWithColor(): #{ LabelledEdge(String, String, String) | r } = #{
    LabelledEdge("a", "red", "b").
    LabelledEdge("b", "red", "c").
    LabelledEdge("c", "blu", "d").
}

def closure(): #{ LabelledEdge(String, l, String),
                  LabelledPath(String, l, String) } with Boxable[l] = #{
    LabelledPath(x, l, y) :- LabelledEdge(x, l, y).
    LabelledPath(x, l, z) :- LabelledPath(x, l, y), LabelledPath(y, l, z).
}

def main(): Unit & Impure =
    query edgesWithNumbers(), closure()
        select (x, l, z) from LabelledPath(x, l, z) |> println;
    query edgesWithColor(), closure()
        select (x, l, z) from LabelledPath(x, l, z) |> println

Here we use two predicate symbols: LabelledEdge and LabelledPath. Each predicate has a type parameter named l and is polymorphic in the "label" type associated with the edge/path. Note how edgesWithNumbers returns a collection of edge facts where the labels are integers, whereas edgesWithColor returns a collection of facts where the labels are strings. The closure function is polymorphic and returns two rules that compute the transitive closure of edges that have the same label.

The Flix type system ensures that we cannot accidentally mix edges (or paths) with different types of labels.

Design Note

The Boxable type class constraint simply requires that each label type has Eq, Order, and ToString instances.

Injecting Facts into Datalog

Flix provides a flexible mechanism that allows functional data structures (such as lists, sets, and maps) to be converted into Datalog facts.

For example, given a Flix list of pairs we can convert it to a collection of Datalog facts:

let l = (1, 2) :: (2, 3) :: Nil;
let p = inject l into Edge;

where l has type List[(Int32, Int32)]. The inject expression converts l into a Datalog constraint set p of type #{ Edge(Int32, Int32) | ... }.

The inject expression works with any type that implements the Foldable type class. Consequently, it can be used with lists, sets, maps, and so forth.

The inject expression can operate on multiple collections simultaneously. For example:

let names = "Lucky Luke" :: "Luke Skywalker" :: Nil;
let jedis = "Luke Skywalker" :: Nil;
let p = inject names, jedis into Name, Jedi;

where p has type #{ Name(String), Jedi(String) | ... }.

Pipelines of Fixpoint Computations

The solution (i.e. fixpoint) of a constraint system is another constraint system. We can use this to construct pipelines of fixpoint computations, i.e. to feed the result of one fixpoint computation into another fixpoint computation. For example:

def main(): Unit & Impure =
    let f1 = #{
        ColorEdge(1, "blue", 2).
        ColorEdge(2, "blue", 3).
        ColorEdge(3, "red", 4).
    };
    let r1 = #{
        ColorPath(x, c, y) :- ColorEdge(x, c, y).
        ColorPath(x, c, z) :- ColorPath(x, c, y), ColorEdge(y, c, z).
    };
    let r2 = #{
        ColorlessPath(x, y) :- ColorPath(x, _, y).
    };
    let m = solve f1, r1 inject ColorPath;
    query m, r2 select (x, y) from ColorlessPath(x, y) |> println

The program uses three predicates: ColorEdge, ColorPath, and ColorlessPath. Our goal is to compute the transitive closure of the colored edges and then afterwards construct a graph where the edges have no color.

The program first computes the fixpoint of f1 and r1 and injects out the ColorPath fact. The result is stored in m. Next, the program queries m and r2, and selects all ColorlessPath facts.

Using Flix to Solve Constraints on Lattices

Flix supports not only constraints on relations, but also constraints on lattices. To create such constraints, we must first define the lattice operations (the partial order, the least upper bound, and so on) as functions, associate them with a type, and then declare the predicate symbols that have lattice semantics.

We begin with the definition of the Sign data type:

enum Sign {
              case Top,

    case Neg, case Zer, case Pos,

              case Bot
}

We need to define the usual Eq, Order, and ToString instances for this new type. Note that the order instance is unrelated to the partial order instance we will later define, and is simply used to sort elements for pretty printing etc.

instance Boxable[Sign]

instance Eq[Sign] {
    pub def eq(x: Sign, y: Sign): Bool = match (x, y) {
        case (Bot, Bot) => true
        case (Neg, Neg) => true
        case (Zer, Zer) => true
        case (Pos, Pos) => true
        case (Top, Top) => true
        case _          => false
    }
}

instance Order[Sign] {
    pub def compare(x: Sign, y: Sign): Comparison =
        let num = w -> match w {
            case Bot => 0
            case Neg => 1
            case Zer => 2
            case Pos => 3
            case Top => 4
        };
        num(x) <=> num(y)
}

instance ToString[Sign] {
    pub def toString(x: Sign): String = match x {
        case Bot => "Bot"
        case Neg => "Neg"
        case Zer => "Zer"
        case Pos => "Pos"
        case Top => "Top"
    }
}

With these type class instances in place, we can now define the lattice operations on Sign.

We define the bottom element and the partial order:

instance LowerBound[Sign] {
    pub def minValue(): Sign = Bot
}

instance PartialOrder[Sign] {
    pub def lessEqual(x: Sign, y: Sign): Bool =
        match (x, y) {
            case (Bot, _)   => true
            case (Neg, Neg) => true
            case (Zer, Zer) => true
            case (Pos, Pos) => true
            case (_, Top)   => true
            case _          => false
        }
}

Next, we define the least upper bound and greatest lower bound:

instance JoinLattice[Sign] {
    pub def leastUpperBound(x: Sign, y: Sign): Sign =
        match (x, y) {
            case (Bot, _)   => y
            case (_, Bot)   => x
            case (Neg, Neg) => Neg
            case (Zer, Zer) => Zer
            case (Pos, Pos) => Pos
            case _          => Top
        }
}

instance MeetLattice[Sign] {
    pub def greatestLowerBound(x: Sign, y: Sign): Sign =
        match (x, y) {
            case (Top, _)   => y
            case (_, Top)   => x
            case (Neg, Neg) => Neg
            case (Zer, Zer) => Zer
            case (Pos, Pos) => Pos
            case _          => Bot
        }
}

With all of these definitions we are ready to write Datalog constraints with lattice semantics. But before we proceed, let us also write a single monotone function:

def sum(x: Sign, y: Sign): Sign = match (x, y) {
    case (Bot, _)   => Bot
    case (_, Bot)   => Bot
    case (Neg, Zer) => Neg
    case (Zer, Neg) => Neg
    case (Zer, Zer) => Zer
    case (Zer, Pos) => Pos
    case (Pos, Zer) => Pos
    case (Pos, Pos) => Pos
    case _          => Top
}

We can now finally put everything to use:

pub def main(): Unit & Impure =
    let p = #{
        LocalVar("x"; Pos).
        LocalVar("y"; Zer).
        LocalVar("z"; Neg).
        AddStm("r1", "x", "y").
        AddStm("r2", "x", "y").
        AddStm("r2", "y", "z").
        LocalVar(r; sum(v1, v2)) :-
            AddStm(r, x, y), LocalVar(x; v1), LocalVar(y; v2).
    };
    query p select (r, v) from LocalVar(r; v) |> println

Warning

Note the careful use of ; to designate lattice semantics.

Type Classes

Type classes are one of the ways to support a high level of genericity in functional programming. Flix's type classes largely follow the style of those of Haskell, with some additional principles.

Essentials

The function isSingleton naively determines whether a list has exactly one element. However, it only works with lists. Although checking the length of a collection like this is possible for all standard collections, we have to implement a separate isSingleton function for each of them.

def isSingleton(l: List[a]): Bool =
    List.length(l) == 1

We can generalize this behavior by using a type class constraint. Rather than requiring the argument to be a list, we use a type variable a and constrain it with to the type class Length, which means that the function Length.length can be applied to the argument.

def isSingleton(l: a): Bool with Length[a] =
    Length.length(l) == 1

The type class declaration Length specifies what can be done with its members. In this case, there is only one function: Length.length, which takes the member type as an argument and returns an integer. The law nonnegative is also defined as part of the class. Laws will be further explored below.

pub class Length[a] {
    pub def length(x: a): Int

    law nonnegative: forall(x: a) . Length.length(x) >= 0
}

If we try to use the new isSingleton function, we will see that it fails to compile:

isSingleton(1 :: 2 :: Nil)

While we know that a list has a length, we haven't told this to the compiler. To do this, we introduce an instance of the type class for the generic type List[a].

instance Length[List[a]] {
    pub def length(x: List[a]): Int = List.length(x)
}

This instance simply states that in order to get the length of the list, we just use the List.length function from the standard library. With this instance around, the call to the isSingleton function will compile. However, you may have noticed that our implementation is inefficient. While comparing the length to 1 is a correct solution generally, for lists specifically the solution has a greater runtime complexity than necessary. In order to preserve the general solution while allowing for optimizations where needed, we can use a default implementation in the type class and an override implementation in the instance.

pub class Length[a] {

    pub def length(x: a): Int

    pub def isSingleton(x: a): Bool = length(x) == 1

    law nonnegative: forall(x: a) . Length.length(x) >= 0

    law singletonMeansOne(x: a): forall(x: a) . Length.length(x) == 1 <==> Length.isSingleton(x)
}

instance Length[List[a]] {
    pub def length(x: List[a]): Int = List.length(x)
    override pub def isSingleton(x: List[a]): Bool = match x {
        case _ :: Nil => true
        case _ => false
    }
}

We have added the isSingleton function to the Length type class, with a default implementation that works in general. (We also added a new law singletonMeansOne; see section Laws.) We have added an efficient override implementation of isSingleton to the Length instance for List[a]. The advantage of the default implementation is that if there's no special behavior needed for a type, the default is assumed. The function does not have to be implemented.

instance Length[String] {
    pub def length(x: String): Int = String.length(x)
}

The instance Length[String] simply uses the default implementation of the isSingleton function.

Laws

In addition to the functions forming part of their contract, type classes have laws that govern how the functions may be implemented.

pub class Length[a] {
    pub def length(x: a): Int

    law nonnegative: forall(x: a) . Length.length(x) >= 0
}

The nonnegative law asserts that the length of something can never be negative.

Planned Feature

We plan to implement a quickcheck framework to verify that these laws hold. For now, however, they only serve as a form of documentation.

Type Constraints

We've seen type constraints on on function definitions, but constraints can appear on on instances and type classes themselves as well.

pub class TreeSize[a] {
    /// Returns the number of nodes in the object graph of this object
    pub def size(x: a): Int32

    law positive: forall(x: a) . size(x) > 0
}

instance TreeSize[Int32] {
    pub def size(x: Int32): Int32 = 1
}

instance TreeSize[List[a]] with TreeSize[a] {
    pub def size(x: List[a]): Int32 = {
        // one node for each cons cell, one for the nil, and nodes for each node's value
        List.Length(x) + 1 + List.foldLeft((acc, y) -> acc + TreeSize.size(y), 0, x)
    }
}

Sealed Classes

In general, a user can add an instance of a class for any type they define. In some cases, however, it is useful to restrict membership in a class to a finite list of types, defined by the author of the class. This is the purpose of a sealed class, for which instances outside the class's namespace are not permitted.

sealed class Primitive[a]

instance Primitive[Bool]
instance Primitive[Int32]
instance Primitive[Float64]
// ... and so on

Namespaces

Flix supports hierarchical namespaces as known from many other programming languages.

Declaring a Namespace

We can declare a namespace to nest definitions and types within it. For example:

namespace Math {
    def sum(x: Int32, y: Int32): Int32 = x + y
}

Namespaces are hierarchical, so we can declare a deeper namespace:

namespace Core/Math {
    def sum(x: Int32, y32: Int): Int32 = x + y
}

Note that the fragments of a namespace are separated by /.

We can freely nest namespaces. For example:

namespace Core {
    namespace Math {

        def sum(x: Int32, y: Int32): Int32 = x + y

        namespace Stats {
            def median(xs: List[Int32]): Int32 = ???
        }
    }
}

Using Definitions from a Namespace

We can refer to definitions from a namespace by their fully-qualified name. For example:

namespace Core/Math {
    pub def sum(x: Int32, y: Int32): Int32 = x + y
}

def main(): Unit & Impure =
    Core/Math.sum(21, 42) |> println

Note that we must declare sum as public (pub) to allow access to it from outside its own namespace.

It can quickly get tedious to refer to definitions by their fully-qualified name.

The use construct allows us to "import" definitions from another namespace:

namespace Core/Math {
    pub def sum(x: Int32, y: Int32): Int32 = x + y
}

def main(): Unit & Impure =
    use Core/Math.sum;
    sum(21, 42) |> println

Here the use is local to the main function. A use can also appear at the top of a file:

use Core/Math.sum;

def main(): Unit & Impure =
    sum(21, 42) |> println

namespace Core/Math {
    pub def sum(x: Int32, y: Int32): Int32 = x + y
}

Using Multiple Definitions from a Namespaces

We can also use multiple definitions from a namespace:

use Core/Math.sum;
use Core/Math.mul;

def main(): Unit & Impure =
    mul(42, 84) |> sum(21) |> println

namespace Core/Math {
    pub def sum(x: Int32, y: Int32): Int32 = x + y
    pub def mul(x: Int32, y: Int32): Int32 = x * y
}

Multiple such uses can be grouped together:

use Core/Math.{sum, mul};

def main(): Unit & Impure =
    mul(42, 84) |> sum(21) |> println

namespace Core/Math {
    pub def sum(x: Int32, y: Int32): Int32 = x + y
    pub def mul(x: Int32, y: Int32): Int32 = x * y
}

Design Note

Flix does not support wildcard uses because they are inherently ambiguous and may lead to subtle errors during refactoring.

Avoiding Name Clashes with Renaming

We can use renaming to avoid name clashes between identically named definitions. For example:

use A.{concat => stringConcat};
use B.{concat => listConcat};

def main(): Unit & Impure =
    stringConcat("Hello", " World!") |> println

namespace A {
    pub def concat(x: String, y: String): String = x + y
}

namespace B {
    pub def concat(xs: List[Int32], ys: List[Int32]): List[Int32] = xs ::: ys
}

In many cases a better approach is to use a local use to avoid the problem in the first place.

Using Types from a Namespace

We can use types from a namespace in the same way as definitions. For example:

use A/B.Color;

def redColor(): Color = Color.Red

namespace A/B {
    pub enum Color {
        case Red, Blue
    }
}

We can also use type aliases in the same way:

use A/B.Color;
use A/B.Hue;

def blueColor(): Hue = Color.Blue

namespace A/B {
    pub enum Color {
        case Red, Blue
    }
    pub type alias Hue = Color
}

Using Enums from a Namespace

We can use enumerated types from a namespace. For example:

def blueIsRed(): Bool =
    use A/B.Color.{Blue, Red};
    Blue != Red

namespace A/B {
    pub enum Color with Eq {
        case Red, Blue
    }
}

Note that A/B.Color is the fully-qualified name of a type whereas A/B.Color.Red is the fully-qualified name of a tag inside an enumerated type. That is, a fully-qualified definition is of the form A/B/C.d, a fully-qualified type is of the form A/B/C.D, and finally a fully-qualified tag is of the form A/B/C.D.T.

Interoperability

Flix supports interoperability with Java libraries through imports. The import construct allows a Java constructor, method, or field to be exposed as an impure Flix function.

The import mechanism should not be confused with the use mechanism. The former enables interoperability with Java, whereas the latter is part of the Flix namespace mechanism.

Creating Objects

We can use imports to retrieve the constructor of a Java class and then call its associated function to construct a new Java object. For example:

import new java.io.File(String): ##java.io.File & Impure as newFile;
newFile("HelloWorld.txt")

Here we import the constructor of the java.io.File class and give it the local name newFile. The newFile function takes a string argument and returns a fresh Java File object. Constructing a fresh object is impure, hence main is marked as Impure.

The type of the File object is written as ##java.io.File where the two hashes ## designate that it is a Java type. Notice that this is how the return type is specified.

A common trick is to use a type alias to make it easier to work with Java types. For example:

type alias File = ##java.io.File

def openFile(s: String): File & Impure =
    import new java.io.File(String): File & Impure as newFile;
    newFile(s)

The type alias can then be used to specify the return type of both openFile and new java.io.File(String).

The java.io.File class has another constructor that takes two arguments: one for parent pathname and one for the child pathname. We can use this constructor as follows:

import new java.io.File(String, String): ##java.io.File & Impure as newFile;
newFile("foo", "HelloWorld.txt")

The import describes the signature of the constructor. We can use this to import any constructor (or method), even if the constructor (or method) is overloaded, as in the above example. The return type is always part of the constructor (or method) signature.

Invoking Object Methods

We can use the import mechanism to invoke methods on objects. For example:

import new java.io.File(String): ##java.io.File & Impure as newFile;
import java.io.File.exists(): Bool & Impure;
let f = newFile("HelloWorld.txt");
exists(f)

In this case the method is imported without an as clause, hence its local name is simply the Java local name: exists. Note that Java methods (and fields) with names that are illegal as Flix names must be imported with the as clause using a legal Flix name. For example, a non-idiomatic Java method may start with an uppercase letter, whereas a Flix function must start with a lowercase letter.

All Java operations are marked as impure since Java is an impure language. If you call a function which you know to be pure, you can cast it from impure to pure, as the following example shows:

def startsWith(prefix: {prefix :: String}, s: String): Bool =
    import java.lang.String.startsWith(String): Bool & Pure;
    startsWith(s, prefix.prefix)

We can pass arguments to methods as the following example shows:

def charAt(i: Int32, s: String): Char =
    import java.lang.String.charAt(Int32): Char & Pure;
    charAt(s, i)

Type signatures should use Flix type names and not Java type names for primitive types. For example, if a Java method takes a Double its signature should use the Flix type Float64. Similarly, if a Java method takes a Boolean its signature should use the Flix type Bool. This goes for return types, too.

Reading Object Fields

Reading a field of an object is straightforward:

import new flix.test.TestClass(): ##flix.test.TestClass & Impure as newObject;
import get flix.test.TestClass.boolField: Bool & Impure as getField;
let o = newObject();
getField(o)

Here we assume that TestClass is a Java class with an instance field named boolField of type Bool.

Writing Object Fields

Writing a field of an object is also straightforward:

import new flix.test.TestClass(): ##flix.test.TestClass & Impure as newObject;
import get flix.test.TestClass.boolField: Bool & Impure as getField;
import set flix.test.TestClass.boolField: Unit & Impure as setField;
let o = newObject();
setField(o, false);
getField(o)

Invoking Static Methods

We can invoke a static method by writing the static keyword after import:

import static java.lang.String.valueOf(Bool): String & Impure;
valueOf(true)

Reading and Writing Static Fields

Reading or writing static fields is similar to reading or writing object fields. For example:

import static get java.lang.Integer.MIN_VALUE: Int32 & Impure as getMinValue;
getMinValue()

As above, the only difference is to write the static keyword to indicate that the reference is to a static field.

Summary

The table below gives an overview of the syntax.

Note: the return types and effects must always be specifed but are omitted for a simpler overview.

ImportSyntax
Constructorimport new Foo.Bar.Baz(...)
Object Methodimport Foo.Bar.baz(...) [as name]
Static Methodimport static Foo.Bar.baz(...) [as name]
Get Object Fieldimport get Foo.Bar.baz as getValue
Set Object Fieldimport set Foo.Bar.baz as setValue
Get Static Fieldimport static get Foo.Bar.baz as getValue
Set Static Fieldimport static set Foo.Bar.baz as setValue

Limitations

Flix does not currently support any of the following features:

  • Defining new classes (or interfaces).
  • Defining new anonymous classes (e.g. to implement a Java interface).

If any of these features are needed, we recommend that you write a small Java wrapper.

Design Note

The import mechanism is only supported at the expression level: it is not currently possible to import Java constructors, methods, and fields at the top-level.

Design Note

The Flix type system does not support sub-typing. Consequently, a sub-type is type incompatible with a super-type. For example, ##java.lang.String is not compatible with ##java.lang.Object. This limitation can be overcome by inserting explicit type casts. For example, e as ##java.lang.Object can be used to cast the type of e to Object.

Warning

The Flix compiler does not support any kind of cross-compilation (e.g. compiling Java sources together with Flix sources). Furthermore, the format of the JVM bytecode generated by the Flix compiler is not yet stable. If you write a library in Flix and use it from Java, you should be prepared for breakages with future versions of the Flix compiler.

Build and Package Management

Flix has a nascent build system and package manager. The package manager does not yet support dependency resolution, but the system is sufficient to build and share packages. There is no central package registry, so distribution and versioning must be handled manually for the moment. We propose that the semantic version of a package is included as part of its name, e.g. foo-1.2.1.fpkg.

The Flix build system makes it easy to create, compile, run, and test a Flix project.

Overview

The Flix build system supports the following commands:

CommandDescription
initCreates a new project in the current directory.
checkChecks the current project for errors.
buildBuilds (i.e. compiles) the current project.
build-jarBuilds a jar-file from the current project.
build-pkgBuilds a fpkg-file from the current project.
runRuns main for the current project.
testRuns tests for the current project.

A command is executed by running flix <command> in the project directory. For example:

$ java -jar path/to/flix.jar init

Build commands can also be invoked from within Visual Studio Code by pressing CTRL + SHIFT + P (to bring up the command palette) and typing the name of the relevant command. This is the recommended way to use the build system.

Tip: To create a new project in Visual Studio Code:

  1. create a new empty folder,
  2. open the folder in Visual Studio Code (File -> Open Folder...), and
  3. press CTRL + SHIFT + P and type Flix init.

Creating a New Project

We can create a new project by creating an empty directory and running the init command inside it:

$ mkdir myproject
$ cd myproject
$ java -jar path/to/flix.jar init

This will create a project structure with the following layout (running $ tree . in the directory will give the result below):

.
├── build
├── flix.jar
├── HISTORY.md
├── lib
├── LICENSE.md
├── README.md
├── src
│   └── Main.flix
└── test
    └── TestMain.flix

4 directories, 6 files

The most relevant files are src/Main.flix and test/TestMain.flix.

The lib/ directory is intended to hold Flix package files (.fpkg-files). The build system and Visual Studio Code will automatically detect Flix packages that are in the lib/ directory.

Checking a Project

We can check a project for errors by running the check command inside the project directory:

$ java -jar path/to/flix.jar check

Checking a project is equivalent to building a project, except no code is generated and the process is significantly faster than a complete build.

Building a Project

We can build a project by running the build command inside the project directory:

$ java -jar path/to/flix.jar build

Building a project populates the build directory with class files.

Design Note

There is no clean command, but deleting everything inside the build directory serves the same purpose.

Building a JAR-file

We can compile a project to a JAR-file with the build-jar command:

$ java -jar path/to/flix.jar build-jar

which will produce a myproject.jar ready to run:

$ java -jar myproject.jar

The JAR-file contains all class files from the build/ directory.

Warning

The project must have been built beforehand with the build command.

Design Note

At the time of writing (July 2021), the built JAR-file still depends on the flix.jar file. Thus to run a Flix program you must put both the generated JAR-file and flix.jar on the class path. For example, on Windows, the command would be: java -jar "flix.jar;myproject.jar" Main. In the future, the plan is to make the generated JAR-file fully self-contained.

Building a Flix Project File (fpkg)

We can compile a project to a Flix package file (fpkg) with the build-pkg command:

$ java -jar path/to/flix.jar build-pkg

which will produce a myproject.fpkg package.

A Flix package file is essentially a zip-file of the project source code. A Flix package file can be reused in another project by placing it into the lib/ directory.

It is recommended to include the semantic version in the filename of the package, e.g. foo-1.2.1.fpkg.

Design Note

Flix does not compile to an intermediate format, but instead relies on packages to contain source code. This means that Flix does not lose any information about a package and can perform cross-package optimizations.

Running a Project

We do not have to build a JAR-file to run a project, we can simply use the run command:

$ java -jar path/to/flix.jar run

which will compile and run the main entry point.

Testing a Project

We can use the test command to run all test cases in a project:

$ java -jar path/to/flix.jar test

Flix will collect all functions marked with @test, execute them, and print a summary of the results:

-- Tests -------------------------------------------------- root
✓ testMain01

Everyday Programming

In this chapter we cover some everyday programming features:

  • The signature of the main function.
  • How to print to standard out and standard error.
  • How to use string interpolation.
  • How to use anonymous and named holes for incomplete programs.
  • How to use type ascriptions to explicitly provide some types to the compiler.

The Main Function

The entry point of any Flix program is the main function which must have the signature:

def main(): Unit & Impure

That is, the main function

  1. must return Unit, and
  2. must be Impure.

The signature of main does not specify any arguments, but the command line arguments passed to the program can be accessed by calling Environment.getArgs().

def main(): Unit & Impure=
    let args = Environment.getArgs();
    ...

Flix requires main to be Impure. If main was pure there would be no reason to run the program. Typically the impurity requirement is satisfied because main prints to the console or has another side-effect.

Printing to Standard Out

The Flix prelude defines two impure functions: print and println that can be used to print a string to standard out. For example:

println("Hello World")

The println function prints with a newline after the string. The print function can be used to print without this newline. For example:

let name = "Lucky Luke";
print("Hello");
print(" ");
println(name)

which prints Hello Lucky Luke on one line.

The print and println functions can print any value whose type implements ToString type class and consequently can be converted to a String. For example:

let o = Some(123);
let l = 1 :: 2 :: 3 :: Nil;
println(o);
println(l)

The print and println functions are rightfully Impure. Consequently they cannot be called from a pure context. This can sometimes hinder debugging of a pure function where you want to log some intermediate computation. A solution is to cast the print and println functions as Pure. Here is an example:

def sum(x: Int32, y: Int32): Int32 =
    let _ = println(x) as & Pure;
    let _ = println(y) as & Pure;
    x + y

Note that sum remains a pure function despite the two calls to println. Moreover, since the call println(x) is pure we must introduce a let-binding with an unused variable to prevent Flix from rejecting the program due to a redundant pure computation.

String Interpolation

Flix strings support interpolation. Inside a string, the form "${e}" evaluates e to a value and converts it to a string using the ToString type class. For example:

let fstName = "Lucky";
let lstName = "Luke";
"Hello Mr. ${lstName}. Do you feel ${fstName}, punk?"

String interpolation works for any types that implements a ToString instance. For example:

let i = 123;
let o = Some(123);
let l = 1 :: 2 :: 3 :: Nil;
"i = ${i}, o = ${o}, l = ${l}"

String interpolations may contain arbitrary expressions. For example:

let x = 1;
let y = 2;
"${x + y + 1}"

String interpolation is the preferred way to concatenate two strings:

let x = "Hello";
let y = "World";
"${x}${y}" // equivalent to x + y

String interpolation is the preferred way to convert a value to a string:

let o = Some(123);
"${o}"

which is equivalent to an explicit use of the toString function from the ToString type class:

ToString.toString(o)

String interpolators may nest, but this is discouraged.

Anonymous and Named Holes

During development, Flix encourages the use of holes for incomplete code. For example:

def sum(x: Int32, y: Int32): Int32 = ???

The triple question marks ??? represents an anonymous hole and can be used wherever an expression is expected. In the above code, ??? represents a missing function body, but it can also be used inside an expression. For example:

def length(l: List[a]): Int32 = match l {
    case Nil     => 0
    case x :: xs => ???
}

When a program has multiple holes, it can be useful to name them. For example:

def length(l: List[a]): Int32 = match l {
    case Nil     => ?base
    case x :: xs => ?step
}

Flix requires that each named hole has a unique name.

Type Ascriptions

While Flix supports local type inference, it can sometimes be useful to annotate an expression or a let-binding with its type. We call such annotations type ascriptions. A type ascription cannot change the type of an expression nor can it be used to violate type safety.

A type ascription can be placed after an expression:

("Hello" :: "World" :: Nil) : List[String]

and it can also be placed on a let-binding:

let l: List[String] = "Hello" :: "World" :: Nil

Advanced Features

In this chapter, we discuss some advanced features of Flix, including:

  • Type Casts
  • Effect Casts

Type Casts

A type cast instructs the compiler that an expression has a specific type.

Warning️️: Type casts are by nature dangerous and should be used with caution!

A Flix programmer should not normally use type casts except in two cases:

  • To cast a Java type to one of its super-types.
  • To cast the null value to a nullable type.

Both use cases are legitimate and safe.

Example: Safe Cast to a Super-Type

The expression below casts a String to an Object:

"Hello World" as ##java.lang.Object

Example: Safe Cast from Null to an Object-Type

The expression below casts the null value (of type Null) to String:

null as ##java.lang.String

Example: Unsafe Cast

The expression below contains an illegal cast and triggers a ClassCastException:

(123, 456) as String

Primitive Values and Boxing

Note: A type cast should not be used to box or unbox primitive values. Instead use the designated Java methods. For example, Integer.valueOf and Integer.intValue.

Effect Casts

An effect cast instructs the compiler that an expression has a specific effect.

Warning️️: Effect casts are by nature extremely dangerous and should be used with utmost caution!

A Flix programmer should not normally use effect casts except in two cases:

  • To cast an pure function to an effect polymorphic function.
  • To cast a pure function to an impure function.

Both cases are legitimate and safe.

Example: Safe Cast of Pure Function to Effect Polymorphic

Flix does not (yet) have sub-effecting which means that in certain rare cases it can be necessary to manually insert a cast. For example:

def findRight(f: a -> Bool & ef, l: List[a]): Option[a] & ef =
    def loop(ll, k) = match ll {
        case Nil     => k()
        case x :: xs => loop(xs, () -> if (f(x)) Some(x) else k())
    };
    loop(l, () -> None as & ef)

Here the cast () -> None as & ef is required because otherwise the function () -> None would be pure and not effect polymorphic as required.

Warning: Never cast effectful expression to pure. You have been warned.

bug! and unreachable!

Flix supports two special "functions": bug! and unreachable! that can be used to indicate when an internal program invariant is broken and execute should abort. For example:

match o {
    case Some(x) => ...
    case None    => bug!("The value of `o` cannot be empty.")
}

As another example:

match k {
    case n if n == 0 => ...
    case n if n >= 0 => ...
    case n if n <= 0 => ...
    case n           =>  unreachable!()
}

Use of bug! and unreachable! should be avoided whenever possible.