Design Flaws in Flix

Inspired by the blog post Design Flaws in Futhark, I decided to take stock and reflect on some of the design flaws that I believe we made during the development of the Flix programming language. I went through old Github issues and pull requests to discover some of the challenging issues that we have been or still are struggling with. I will classify the design flaws into four categories: (i) design flaws that still plague the Flix language, (ii) design flaws that have been fixed, (iii) poor designs that were thankfully never implemented, and finally (iv) design choices where the jury is still out.

I want to emphasize that language design and implementation is a herculean task and that there are features planned for Flix which have not yet been implemented. The lack of a specific feature is not a design flaw, but rather a question of when we can get around to it.

Design Flaws Present in Flix

The following design flaws are still present in Flix. Hopefully some day they will be fixed.

The Switch Expression

Flix supports the switch expression:

switch {
    case cond1 => exp1
    case cond2 => exp2
    case cond3 => exp3
}

where the boolean expressions cond1, cond2, and cond3 are evaluated from top to bottom until one of them returns true and then its associated body expression is evaluated. The idea, quite simply, is to have a control-flow structure that visually resembles an ordinary pattern match, but where there is no match value.

In hind-sight, the switch expression is nothing more than a glorified if-then-else-if construct that does not carry its own weight. It is an expenditure on the complexity and strangeness budget that offers almost no gain over using plain if-then-else-if. Moreover, it is error-prone, because it lacks an explicit else branch in case none of the conditions evaluate to true. We plan to remove it in future versions of Flix.

String Concatenation with Plus

Like most contemporary languages, Flix uses + for string concatenation. While this is an uncontroversial design choice, it does not make much sense since strings are not commutative, e.g. "abc" + "def" is not the same as "def" + "abc". A better alternative would be to use ++ as in Haskell. However, I believe an even better design choice would be to forgo string concatenation and instead rely entirely on string interpolation. String interpolation is a much more powerful and elegant solution to the problem of building complex strings.

Design Flaws No Longer Present in Flix

The following design flaws have been fixed.

Compilation of Option to Null

Flix compiles to JVM bytecode and runs on the virtual machine. An earlier version of Flix had an optimization that would take the Option enum:

Option[a] {
    case None,
    case Some(a)
}

and compile the None value to null and Some(a) to the underlying value of a. The idea was to save allocation and de-allocation of Some values, speeding up evaluation.

But, this screws up interoperability with Java libraries. In Java null might be given a special meaning that is incompatible with the meaning None. For example, certain Java collections cannot contain null and trying to put None into one of these would raise an unexpected exception. Consequently, Flix no longer has this optimization.

Useless Library Functions

Flix aims to have a robust standard library that avoids some of the pitfalls of other standard libraries. We have been particularly focused on two aspects: (i) ensuring that functions and types have consistent names, e.g. map is named map for both Option and List, and (ii) to avoid partial functions, such as List.head and List.tail which are not defined for empty lists.

Yet, despite these principles, we still managed to implement some problematic functions in the library. For example, we used to have the functions Option.isNone and Option.isSome. The problem with these functions is that they are not really useful and they lead to brittle code. For example, if Option.isSome returns true then that information cannot be used to unwrap the option anyway. Thus such functions are not really useful.

Function Call Syntax

Inspired by Scala, early versions of Flix did not always use parentheses to mark a function call. For example, the function:

def f: Int32 = 21

could be called by writing:

def g: Int32 = f + 42 // returns 63

The problem with this design is at least two-fold: (i) it hides when a function is applied, which is terrible in a language with side-effects, and (ii) how does one express the closure of f? (In Scala the answer is to write f _).

Today, in Flix, the code is written as:

def f(): Int32 = 21
def g: Int32 = f() + 42 // returns 63

which makes it clear when there is a function call.

Infix Type Application

In Flix, a function f can be called with the arguments x and y in three ways: In standard prefix-style f(x, y), in infix-style x `f` y, and in postfix-style x.f(y). The latter is also sometimes referred to as universal function call syntax. I personally feel reasonably confident that all three styles are worth supporting. The postfix-style fits well for function calls such as a.length() where the length function feels closely associated with the receiver argument. The infix-style fits well with user-defined binary operations such as x `lub` y where lub is the least upper bound of x and y. And of course the prefix-style is the standard way to perform a function call.

Type constructors, such as Option and Result can be thought of a special type of functions. Hence, it makes sense that their syntax should mirror function applications. For example, we can write the type applications Option[Int32] and Result[Int32, Int32] mirroring the prefix style of regular function applications. Similarly, for a while, Flix supported infix and postfix type applications. That is, the former could also be expressed as: Int32.Option[] and Int32.Result[Int32], or even as Int32 `Result` Int32. Thankfully, those days are gone. Striving for such uniformity in every place does not seem worth it.

Unit Tests that Manually Construct Abstract Syntax Trees

The Flix compiler comes with more than 6,500 manually written unit tests. Each unit test is a Flix function that performs some computation, often with an expected result. The unit tests are expressed in Flix itself. For example:

@test
def testArrayStore01(): Unit = let x = [1]; x[0] = 42

In earlier versions of Flix such unit tests were expressed by manually constructing "small" abstract syntax tree fragments. For example, the above test would be expressed as something like:

Let(Var("x", ...), ArrayNew(...), ArrayStore(Var("x"), Int32(0), Int32(42)))

The problem with such tests is at least two-fold: (i) the tests turn out to be anything but small and (ii) maintenance becomes an absolute nightmare. I found that the surface syntax of Flix has remained relatively stable over time, but the abstract syntax trees have changed frequently, making maintenance of such test cases tedious and time consuming.

Bad Ideas that were Never Implemented

These ideas were fortunately never implemented in Flix.

The Itself Keyword

The idea was to introduce a special keyword that within a pattern match would refer to the match value. For example:

def foo(e: Exp): Exp = match e {
    // ... many lines ...
    case IfThenElse(e1, e2, e3) => itself // refers to the value of e.
}

The keyword itself refers to the value of the match expression, i.e. the value of e. The idea was that in very large and complicated pattern matches, with many local variables, the itself keyword could always be used to refer to the innermost match value. The thinking was that this would make it easier to avoid mistakes such as returning e0 instead of e or the like.

The problem with this idea is at least three-fold: (i) it seems like overkill for a very specific problem, (ii) it is not worth it on the complexity and strangeness budget, and finally (iii) it is still brittle in the presence of nested pattern matches.

Potential Design Flaws

It is debatable whether the following feature is a design flaw or not.

Built-in Syntax for Lists, Sets, and Maps

Flix has a principle that states that the standard library should not be "blessed". That is, the standard library should be independent of the Flix compiler and language. It should just be like any other library: A collection of Flix code.

Yet, despite this principle, Flix has special syntax for Lists, Sets and Maps:

1 :: 2 :: Nil
Set#{1, 2, 3}
Map#{1 -> 2, 3 -> 4}

which is built-in to the language. While technically these constructs are merely syntactic sugar for Cons, and calls to Set.empty, Set.insert, Map.empty and Map.insert there is no getting around the fact that this is a special kind of blessing of the standard library. In particular, it is not possible to define your own Foo#... syntax for anything.