Susan Potter
software: Created / Updated

Getting Started with Flix, Part 0

Definition of a sum type and pattern matching function in Flix
Caption: Definition of a sum type and pattern matching function in Flix

This part 0 post should work for any developer who wants to get started with Flix. Flix is a programming language that supports constructs native to functional programming languages like Scala and Haskell. It also supports row polymorphic extensible records like PureScript. In a unique twist Flix provides first-class support for Datalog constraints too plus other less notable features.

Today, in part 0, we will cover basic project setup plus the more basic language features to get us on the road to being dangerous with Flix:

In future parts I plan to explore:

Future parts may expect some experience with related languages like Scala, Haskell, or PureScript to play with specific parts of the language or ecosystem.

Web-based Flix Playground

If you want to get started with Flix quickly just to play around, there is a web-based playground.

It will give you a single buffer text editor to write Flix code and compile, run, and preview in the right-hand-side pane quickly.

This is great for a quick test drive but sometimes you want more, so the rest of this post will walk through how to setup a local environment using the documented setup for Flix.

Local Setup (UPDATED OCTOBER 2023)

Initialize project & directory structure

First let us create a project directory we can play with Flix inside:

# Create project directory
$ mkdir flixproject
$ cd flixproject

To get started, I created a Nix Flake that includes flix and the jre. However, if you aren't indoctrinated by Nix like me, you can follow the Flix documentation under the Manually downloading and running the compiler section of the Getting Started page.

The Nix flake I used so you can follow along is:

{
  description = "mbbx6spp's Flix blog series flake";

  inputs.nixpkgs.url     = "github:nixos/nixpkgs";

  inputs.flake-utils.url = "github:numtide/flake-utils";

  inputs.devShell.url    = "github:numtide/devShell";
  inputs.devShell.inputs.flake-utils.follows = "flake-utils";

  outputs = { self, nixpkgs, flake-utils, devShell }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs {
          inherit system;
          overlays = [ ];
        };
      in
      {
        devShells.default = (import devShell { inherit system; }).mkShell {
          packages = with pkgs; [
            flix
            jre
          ];
        };
      }
    );
}

If you want to use the Nix Flake, you just need to download the above Nix expression into a file named flake.nix inside of the project root directory.

At the time of updating this blog post, after loading the Nix flake above, I had v0.40.0 of Flix installed and available to me to play with. To load the Nix Flake environment you can run nix develop or setup direnv-nix to load the Flake each time you enter the project directory.

Anatomy of an initialized Flix project

For a quick Flix project initialization we just need to run flix init which produces a directory tree like the following:

$ flix init
$ tree
.
├── build
├── flake.nix
├── flake.lock
├── flix.toml
├── HISTORY.md
├── lib
├── LICENSE.md
├── README.md
├── src
    └── Main.flix
└── test
    └── TestMain.flix

4 directories, 8 files

You will notice a flix.toml file which contains content like the following:

[package]
name        = "flixproject"
description = "test project"
version     = "0.1.0"
flix        = "0.40.0"
authors     = ["John Doe <john@example.com>"]

Edit the file to update the author details as you prefer.

The Flix project initialization generated a hello world project. We can run the Main via the Flix run subcommand like so:

$ flix run
Hello World!

The first time you run this project with changes, you may also see the following output above Hello World! output:

Found `flix.toml'. Checking dependencies...
Resolving Flix dependencies...
Downloading Flix dependencies...
Resolving Maven dependencies...
  Running Maven dependency resolver.
Downloading external jar dependencies...
Dependency resolution completed.

Peeking at the Main source at src/Main.flix we see this:

// The main entry point.
def main(): Unit \ IO =
    println("Hello World!")

This shows the main function entry taking no arguments and returning a Unit wrapped in an IO effect. In Scala, we might use IO[Unit] from Cats Effect or ZIO[R, E, A] in the ZIO ecosystem instead of Unit \ IO. The major difference here is that IO is in Flix's standard library.

To run the generated hello world test suite we just run flix test:

$ flix test
Found `flix.toml'. Checking dependencies...
Resolving Flix dependencies...
Downloading Flix dependencies...
Resolving Maven dependencies...
  Running Maven dependency resolver.
Downloading external jar dependencies...
Dependency resolution completed.
Running 1 tests...

   PASS  test01 2.6ms

Passed: 1, Failed: 0. Skipped: 0. Elapsed: 7.6ms.

The hello world test source at test/TestMain.flix looks like so:

@Test
def test01(): Bool = 1 + 1 == 2

So we see something that looks like a Java annotaiton @Test designating the following function definition as a test. The function definition has the signature Unit -> Bool.

Build a barebones calculator

When learning new functional languages, I like to build a simple calculator expression builder and evaluator.

To reduce the scope of this part 0 (already longer than I wanted) we will do a barebones model of a calculator expression and one eval interpreter. We will start out writing out how we want to describe the calculation expressions as fixutres that we can use in example-based tests later:

// I first start out defining fixtures to use in example-based tests
def expr1(): Expr = Add(Mul(Lit(3), Lit(2)), Lit(5))
def expr2(): Expr = Mul(Sub(Lit(4), Lit(1)), Lit(9))

To avoid getting into the weeds with parametric polymorphism, type class constraints, etc in Flix, we will make the expression model and evaluator monomorphic such that the only numeric values we can use right now are integers.

We want to support addition, multiplication, division, and subtraction for our calculator, so I usually start out with an initial encoding of the model (via an algebraic data type representing these operations).

In Scala 2.x this might look like the following although depending on who you ask there are variations of exactly how to do this "best" in Scala 2.x; most of the differences are not relevant to the modeling we are doing here, so we will ignore this:

sealed trait Expr
final case class Lit(i: Int) extends Expr
final case class Add(e1: Expr, e2: Expr) extends Expr
final case class Mul(e1: Expr, e2: Expr) extends Expr
final case class Div(e1: Expr, e2: Expr) extends Expr
final case class Sub(e1: Expr, e2: Expr) extends Expr

The above encoding of a sum type is comical for a language long hailed as a productive functional language with batteries, especially when you compare to the equivalent Haskell below:

data Expr = Lit Int
          | Add Int Int
          | Mul Int Int
          | Div Int Int
          | Sub Int Int

Ok, I know Scala fans will be saying "but it is fixed in Scala 3", ok, let's have a look:

enum Expr:
    case Lit(i: Int)
    case Add(e1: Expr, e2: Expr)
    case Mul(e1: Expr, e2: calc)
    case Div(e1: Expr, e2: Expr)
    case Sub(e1: Expr, e2: Expr)

This is definitely something I can live with in Scala 3.x.

So how can we express this in Flix?

enum Expr {
    case Lit(Int32),
    case Add(Expr, Expr),
    case Mul(Expr, Expr),
    case Div(Expr, Expr),
    case Sub(Expr, Expr)
}

It is amusingly almost identical as the Scala 3.x with minor syntactic differences and structurally identical to Scala, Haskell and PureScript.

My one minor irritation with the Flix syntax (for me) is that I cannot leave a trailing comma separating the constructors of Expr.

Write a naive expression evaluator

Now we want to evaluate expressions built using the Expr operations, so let's go back to our familiar languages (Scala 2.x, it is only syntactically different in Scala 3.x) to see how we might do this:

def eval(expr: Expr): Int = expr match {
  case Lit(i)      => i
  case Add(e1, e2) => eval(e1) + eval(e2)
  case Mul(e1, e2) => eval(e1) * eval(e2)
  case Div(e1, e2) => eval(e1) / eval(e2)
  case Sub(e1, e2) => eval(e1) - eval(e2)
}

This is the naive implementation. We will ignore its performance issues for large expressions which will lead to many layers of recursive calls which are not tail recursive in this definition (therefore cannot be optimized by the compiler). We might come back and clean this up in a future part in this series.

Let us see at how we can do this in Flix:

def eval(expr: Expr): Int32 = match expr {
  case Lit(i) => i
  case Add(e1, e2) => eval(e1) + eval(e2)
  case Mul(e1, e2) => eval(e1) * eval(e2)
  case Div(e1, e2) => eval(e1) / eval(e2)
  case Sub(e1, e2) => eval(e1) - eval(e2)
}

Is this even a different language to Scala? :)

Now before we go further, let us see what happens if we comment out the last case in the pattern match. We get and error message while typechecking:

-- Pattern Match -------------------------------------------------- ...flixproject/src/Main.flix
>> Non-Exhaustive Pattern. Missing case: Sub in match expression.

14 | def eval(expr: Expr): Int32 = match expr {
                                         ^^^^
                                         incomplete pattern.

Super! We get exhaustivity checking out-of-the box in Flix. Now we will add some tests to wrap this getting-started part 0 up.

Write tests

Since I like to write my tests with my main code especially for small codebases, I will just add two tests to check the evaluator function works as expected in the src/Main.flix file and remove the hello world test from test/TestMain.flix.

@Test
def checkExpr1(): Bool = eval(expr1()) == 11
@Test
def checkExpr2(): Bool = eval(expr2()) == 27

To check the tests we run (and remember to uncomment the last pattern match case to get back to being fully exhaustive):

$ flix test
Found `flix.toml'. Checking dependencies...
Resolving Flix dependencies...
Downloading Flix dependencies...
Resolving Maven dependencies...
  Running Maven dependency resolver.
Downloading external jar dependencies...
Dependency resolution completed.
Running 2 tests...

   PASS  checkExpr1 3.2ms
   PASS  checkExpr2 299.1us

Passed: 2, Failed: 0. Skipped: 0. Elapsed: 8.3ms.

You can see that both of these new tests ran.

Update the Main

I updated the given hello world main like so such that the two expression fixtures could be evaluated and the result printed out to stdout. It looks like the following:

def main(): Unit \ IO =
  eval(expr1()) |> println;
  eval(expr2()) |> println

We can run this again using flix run which will output:

11
27

Summary

So far we have just touched the surface of the Flix programming language which has a unique combination of features and a well defined set of design principles.

First impressions are that:

  • the tooling for the Flix language (build tools such as dependency management) covers the most basic usage but is sufficient for learning purposes
  • the language has multiple similarities to functional languages I'm already familiar with (Scala, Haskell, and PureScript)
  • the basic capabilities for effective functional programming (in my view) as an application developer are present and reasonably ergonomic (e.g. algebraic data types, pattern matching with exhaustive checking, writing functions and simple test support)

To be effective as a developer I like to have more than just the basic capabilities on display in this part 0, but over the rest of the series on Flix we will explore Flix's other major language features.

This part should have provided you with a basic understanding of the anatomy of a basic Flix project and how to get started without too much fanfare. Join me in the next parts of the series as I learn Flix while I contrast and compare it to the functional languages I am most familiar with (Scala, Haskell, and PureScript).

Tune in for the next installment on HOFs and user-defined infix operators in Flix.

If you enjoyed this content, please consider sharing via social media, following my accounts, or subscribing to the RSS feed.