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:
- algebraic data types
- pattern matching support
- compare both with Scala 2 and Scala 3 capabilities
In future parts I plan to explore:
- higher order functions, user-defined symbolic operators
- encodings of smart constructors
- parametric polymorphism, combinators, type aliases, opaque types
- row polymorphism (comparing to PureScript since neither Scala nor Haskell have an ergonomic equivalent)
- type classes including derivation (comparing with Haskell, PureScript and Scala's implicits/givens)
- higher-kinded types (comparing to Scala and Haskell)
- interoperability with Java (comparing to Scala's interop)
- channel-based concurrency (comparing to Go)
- logic programming primitives
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 this link with a friend, following my GitHub, Twitter/X or LinkedIn accounts, or subscribing to my RSS feed.