Design Principles
We believe that the development of a programming language should follow a set of principles. That is, when a design decision is made there should exist some rationale for why that decision was made. By outlining these principles, as we develop Flix, we hope to keep ourselves honest and to communicate the kind of language Flix aspires to be.
Many of these ideas and principles come from languages that have inspired Flix, including Ada, Elm, F#, Go, Haskell, OCaml, Rust, and Scala.
Update: The Flix Principles has been published in a paper at Onward! '22. Read it here: The Principles of the Flix Programming Language.
Language Principles
- Type signatures are useful as documentation and to aid program understanding.
- Type signatures accurately assign blame for type errors.
- Type signatures enable parallel type checking.
List.map is def map[a, b](f: a -> b, xs: List[a]): List[b], we can use type parameter elision to write it simply as: def map(f: a -> b, xs: List[a]): List[b] omitting the type arguments.def, let, law,rel, but not for commonly established concepts: if ... else and match.- A function application is written as
f(a, b, c)whereas a type application is written asf[a, b, c]. - A function expression is written as
x -> x + 1whereas a function type is written asInt -> Int. - A tuple is written as
(true, 12345)whereas a tuple type is written as(Bool, Int).
- Monomorphization to avoid unnecessary boxing of primitives.
- Aggressive dead code elimination ("tree shaking") to remove unused functions.
- Inlining across namespaces.
- Whole-program analysis.
[0-99].in progress
main is the entry point of a program. No (user-defined) code is ever executed before main. No static initializers, no static fields. No class loaders. Main is always first. This makes it easy to reason about startup behavior.Option, List, Set, and Map, but for these it offers a comprehensive collection of functionality. For example, the standard library has more than 65 functions for working with lists. We want the standard library to offer a common set of abstractions which are usable by most programs, but not much else.null value. The null value is now widely considered a mistake and languages such as C#, Dart, Kotlin and Scala are scrambling to adopt mechanisms to ensure non-nullness. In Flix, we adopt the standard solution from functional languages which is to represent the absence of a value using the Option type. This solution is simple to understand, works well, and guarantees the absence of dreaded NullPointerExceptions.- No value is ever coerced to a boolean.
- No value is ever coerced to a string.
- Integers and floating-point are never truncated or promoted.
Map.filter and Map.filterWithKey, for functions that share similar functionality.Celsius instead of Int) or alternatively (ii) using record types which can be used to emulate the same functionality and works with higher-order functions.Compiler Message Principles
Compiler messages are the main interface between Flix and programmers. We should invest into it.
The rule states that 80% of the time a developer will need minimal information to understand a compiler message. Most likely the developer will already have seen the specific error message hundreds of times before. But 20% of the time, the developer will never have seen the message before and will need more information.
Flix compiler messages should accommodate both scenarios.
A compiler message consists of three components:
- Summary: A one sentence summary. The message shown on hover in Visual Studio Code.
- Message: A multi-line text that contains all relevant details, including the program symbol(s) and fragment(s) relevant for the message.
- Explanation: A description of why the problem occurs and what can be done to fix it.
A message should be crisp, concise, and clear. The language should be friendly or neutral. An error message should not blame the programmer. For example, we should prefer Unexpected foo over Illegal foo, since the latter implies that the programmer did something wrong.
The error message: Duplicate definition: 'foo' is better than the error message: The definition 'foo' is defined twice because in the former the programmer only has to scan the first word to understand what is wrong.
When relevant, a Flix compiler error should explain how Flix differs from other languages and explain how the specific problem can be solved in Flix.
Type Class Principles
A type class is conceptually a function from a type to a set of lawful operations (called signatures) on values of that type.
For example, the Eq type class takes a type and returns the eq and neq functions where eq must be reflexive, symmetric, and transitive, and neq must be the negation of eq.
Every type class must specify a collection of laws that instances of the type class must satisfy. If a type class does not specify any laws it is lawless.
Here are some examples of lawful and lawless type classes:
- Lawful:
Eq,Order,Functor,Foldable. - Lawless:
FromString,ToString,Add.
A type class is lawful if every signature of the class is used in at least one law.
Note: Laws are not checked by the compiler – that is an undecidable problem – but they may be used in a future SmallCheck / QuickCheck library.
A sub-class (i.e. a type class A that refines a type class B) must specify additional laws that its instances must satisfy.
For example, the Applicative type class extends the Functor type class with additional operations and laws.
- the type class declaration, or
- the type declaration of the instance
sealed in which case no further instances, other than those in the same namespace, can be defined. A sealed type class can be used when the programmer wants to maintain tight control over what instances should be permitted.The Flix compiler ensures that the selection of type class instances is always unambiguous.
In the future, we may allow a limited form of overlapping instances.
Every type class belongs to a namespace. Hence it is possible to define multiple operations with the same name, as long as they belong to type classes in different namespaces.
Every type class also defines a companion namespace which typically holds functions that are not part of the type class, but nevertheless are related to the functionality of the type class.
Type classes may provide default implementations of functions.
For example, the Foldable type class may provide default function implementations, e.g. count and length, based on the foldLeft and foldRight signatures defined in that class.
A default implementation can always be overriden by a specific type class instance. For example, to provide a more efficient version.
redef keyword. This ensures that there are no dangling overrides / redefinitions, i.e. functions definitions that do not match any signature of the type class.Library Principles
Array and MutSet support the map operation. Flix, being functional-first, reserves functional names for functional operations. Across the standard library map has the same name and the same type signature.Array.reverse(a) returns a new array with the elements of a in reverse order, whereas Array.reverse!(a) destructively re-orders the elements of a. Note: This principle applies to destructive operations that operate on data structures, not to impure functions in general, e.g. Console.printLine.Array.reverse and Array.reverse! share the same name. On the other hand, Array.transform! is called transform! and not map! because its type signature is dissimilar to map (i.e. map works on functions of type a -> b, but transform requires functions of type a -> a.)