Susan Potter

Getting Started with Flix, Part 0

Sun June 6, 2022

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 while supporting row polymorphic extensible records like PureScript and also provides first-class support for Datalog constraints. There is even more, but I am out of breath so let's lay out the series.

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

Initialize project & directory structure

To get started, I followed the Flix documentation under the Manually downloading and running the compiler section of the Getting Started page.

I use Nix so this is what I did for a quick bootstrapping:

# Create project directory
$ mkdir flixproject
$ cd flixproject
# Install JRE from nixpkgs channel
$ nix-shell -p jre
# Download v0.28.0 of Flix
$ wget https://github.com/flix/flix/releases/download/v0.28.0/flix.jar -O flix.jar
# Initialize directory structure
$ java -jar flix.jar init
$ tree
.
├── HISTORY.md
├── LICENSE.md
├── README.md
├── build
├── flix.jar
├── lib
├── src
    └── Main.flix
└── test
    └── TestMain.flix

4 directories, 6 files

Now to run the generated hello world template of a project we would run the run subcommand like so:

$ java -jar flix.jar run
Hello World!

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

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

To run the generated hello world test suite:

$ java -jar flix.jar test
-- Tests -------------------------------------------------- root

  ✓ test01

  Tests Passed! (Passed: 1 / 1)

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

Just to test out the basic functional programming features, I like to write a simple calculator expression builder and relevant interpreters.

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(): Calc = Add(Mul(Val(3), Val(2)), Val(5))
def expr2(): Calc = Mul(Sub(Val(4), Val(1)), Val(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 Calc
final case class Val(i: Int) extends Calc
final case class Add(e1: Calc, e2: Calc) extends Calc
final case class Mul(e1: Calc, e2: Calc) extends Calc
final case class Div(e1: Calc, e2: Calc) extends Calc
final case class Sub(e1: Calc, e2: Calc) extends Calc

This is comical really for a language long hailed as a productive functional language with batteries, especially when you compare to Haskell (the PureScript for this is almost identical):

data Calc = Val 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 see:

enum Calc:
    case Val(i: Int)
    case Add(e1: Calc, e2: Calc)
    case Mul(e1: Calc, e2: calc)
    case Div(e1: Calc, e2: Calc)
    case Sub(e1: Calc, e2: Calc)

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

So how can we express this in Flix?

enum Calc {
    case Val(Int32),
    case Add(Calc, Calc),
    case Mul(Calc, Calc),
    case Div(Calc, Calc),
    case Sub(Calc, Calc)
}

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

Write a naive expression evaluator

Now we want to evaluate expressions built using the Calc 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: Calc): Int = match expr {
  case Val(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: Calc): Int32 = match expr {
  case Val(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 -------------------------------------------------- ...flix-getting-started/src/Main.flix

>> Non-Exhaustive Pattern. Missing case: Sub(_, _) in match expression.

12 | def eval(expr: Calc): Int32 = match expr {
                                         ^^^^
                                         incomplete pattern.

Super! We get exhaustivity checking out-of-the box in Flix. I am now satisfied to now 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):

$ java -jar flix.jar test
-- Tests -------------------------------------------------- root

  ✓ checkExpr1
  ✓ checkExpr2

  Tests Passed! (Passed: 2 / 2)

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 & Impure =
  eval(expr1()) |> println;
  eval(expr2()) |> println

We can run this again using java -jar flix.jar 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 functional

  • 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)

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 language features.

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