From 498e610b27816b09ab3ccf170b153db5e3de988d Mon Sep 17 00:00:00 2001 From: Magnus Madsen Date: Wed, 25 Mar 2026 07:43:40 +0100 Subject: [PATCH] refactor: remove legacy blog posts, keep link to blog.flix.dev Delete the five legacy blog post components and the blog/ folder. Simplify Blog.jsx to just show the redirect link to https://blog.flix.dev/. Remove nested routing from the blog route. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/App.jsx | 2 +- src/page/Blog.jsx | 38 +- src/page/blog/@Ideas.md | 15 - src/page/blog/DesignFlaws.jsx | 317 ---------- src/page/blog/Naming.jsx | 366 ----------- src/page/blog/PolymorphicEffects.jsx | 618 ------------------- src/page/blog/ProgrammingLanguageDefense.jsx | 339 ---------- src/page/blog/Redundancies.jsx | 492 --------------- 8 files changed, 11 insertions(+), 2176 deletions(-) delete mode 100644 src/page/blog/@Ideas.md delete mode 100644 src/page/blog/DesignFlaws.jsx delete mode 100644 src/page/blog/Naming.jsx delete mode 100644 src/page/blog/PolymorphicEffects.jsx delete mode 100644 src/page/blog/ProgrammingLanguageDefense.jsx delete mode 100644 src/page/blog/Redundancies.jsx diff --git a/src/App.jsx b/src/App.jsx index 569798b..277705e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -65,7 +65,7 @@ function App() { } /> } /> } /> - } /> + } /> } /> } /> diff --git a/src/page/Blog.jsx b/src/page/Blog.jsx index 0261d01..35c00ad 100644 --- a/src/page/Blog.jsx +++ b/src/page/Blog.jsx @@ -1,42 +1,24 @@ import { useEffect } from 'react'; import {Col, Container, Row} from "reactstrap"; -import {Route, Routes} from "react-router"; -import ProgrammingLanguageDefense from "./blog/ProgrammingLanguageDefense"; -import DesignFlaws from "./blog/DesignFlaws"; -import Naming from "./blog/Naming"; -import Redundancies from "./blog/Redundancies"; -import PolymorphicEffects from "./blog/PolymorphicEffects"; function Blog() { useEffect(() => { - if (!document.title) { - document.title = "Flix | Blog"; - } + document.title = "Flix | Blog"; }, []); return ( - - - -

Blog

+ + +

Blog

- The Flix Blog is now available at:
+ The Flix Blog is now available at:
- - -
- } /> - - } /> - } /> - } /> - } /> - } /> -
+ + +
); } diff --git a/src/page/blog/@Ideas.md b/src/page/blog/@Ideas.md deleted file mode 100644 index 4ba89f8..0000000 --- a/src/page/blog/@Ideas.md +++ /dev/null @@ -1,15 +0,0 @@ -# Ideas for Future Blog Posts - -- Introduction to Datalog -- Programming with Datalog in Flix -- Relational Nullability -- How we track compiler performance -- Currying: Challenges and Solutions -- Why we use the Java Virtual Machine -- "The State of Flix" -- There is no free lunch (simplicity vs. power) -- Sanity: Avoiding common PL pitfalls -- Design of Type Classes -- In Defense of PL Design -- Looking for contributors / Why you should join us now -- Why work on someone else's PL? diff --git a/src/page/blog/DesignFlaws.jsx b/src/page/blog/DesignFlaws.jsx deleted file mode 100644 index e1abae8..0000000 --- a/src/page/blog/DesignFlaws.jsx +++ /dev/null @@ -1,317 +0,0 @@ -import { useEffect } from 'react'; -import {Col, Container, Row} from "reactstrap"; -import InlineEditor from "../../util/InlineEditor"; - -function DesignFlaws() { - - useEffect(() => { - document.title = "Flix | Design Flaws in Flix"; - window.location.replace("https://blog.flix.dev/blog/design-flaws-in-flix/"); - }, []); - - return ( - - - - -

Design Flaws in Flix

- -

- Posted January 2020 by Magnus Madsen. -

- -

- 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 - and 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 Options.isSome. The problem with these - functions is that they are not really useful and they lead to brittle code. For - example, if Options.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. -

- - -
-
- ) -} - -export default DesignFlaws diff --git a/src/page/blog/Naming.jsx b/src/page/blog/Naming.jsx deleted file mode 100644 index 0459095..0000000 --- a/src/page/blog/Naming.jsx +++ /dev/null @@ -1,366 +0,0 @@ -import { useEffect } from 'react'; -import {Card, CardText, CardTitle, Col, Container, Row} from "reactstrap"; -import InlineEditor from "../../util/InlineEditor"; - -function Naming() { - - useEffect(() => { - document.title = "Flix | Naming Functional and Destructive Operations"; - window.location.replace("https://blog.flix.dev/blog/naming-functional-and-destructive-operations/"); - }, []); - - return ( - - - - -

Naming Functional and Destructive Operations

- -

- Posted April 2020 by Magnus Madsen. -

- -

- It has been said that there are only two hard problems in computer science: (i) naming, (ii) - cache invalidation, and (iii) off-by-one errors. In this blog post, I will explain a name - consistency issue that arises when a programming language wants to support both - functional and destructive operations. (A functional operation always returns new data, - whereas a destructive operation mutates existing data. For example, functionally reversing - an array returns a new array with its elements reversed, whereas destructively - reversing an array mutates the array in place.) -

- -

- Flix supports functional, imperative, and logic programming. Flix is intended to - be functional-first which simply means that if there is a trade-off between - having better functional- or imperative programming support, we tend to favor design choices - that support functional programming. For example, the Flix effect system separates pure - and impure functions mostly to the benefit of functional programming. -

- -

- Flix, being imperative, wants to support mutable data structures such as arrays, mutable - sets and maps. We have recently added support for all three. But let us for a moment - consider a simpler data structure: the humble list. -

- -

- We can map a function f: a -{">"} b over a list l to - obtain a new list of type List[b]: -

- - - {`def map(f: a -> b \\ ef, l: List[a]): List[b] \\ ef`} - - -

- (Here the ef denotes that the function is effect polymorphic, but that - is for another day.) -

- -

- We can also map a function over an option: -

- - - {`def map(f: a -> b \\ ef, o: Option[a]): Option[b] \\ ef`} - - -

- We can also map a function over an array: -

- - - {`def map(f: a -> b \\ ef, a: Array[a]): Array[b] \\ IO`} - - -

- This is good news: we can program with arrays in a functional-style. Mapping over an array - is certainly meaningful and useful. It might even be faster than mapping over a list! - Nevertheless, the main reason for having arrays (and mutable sets and maps) is to program - with them imperatively. We want to have operations that mutate their data. -

- -

- We want an operation that applies a function to every element of an array changing - it in place. What should such an operation be called? We cannot name - it map because that name is already taken by the functional version. - Let us simply call it mapInPlace for now: -

- - - {`def mapInPlace(f: a -> a \\ ef, a: Array[a]): Unit \\ IO`} - - -

- The signature of mapInPlace is different from the signature - of map in two important ways: - -

    -
  • The function returns Unit instead of returning an array.
  • -
  • The function takes an argument of type a -{">"} a rather than a function of - type a -{">"} b. -
  • -
-

- -

- The latter is required because the type of an array is fixed. An array of bytes cannot - be replaced by an array of strings. Consequently, mapInPlace must take a - less generic function of type a -{">"} a. -

- -

- We have seen that it is useful to have both functional and destructive functions such - as map and mapInPlace, but what should such functions be called? - Are they sufficiently similar that they should share similar names? What should be the - general rule for naming functional operations and their counter-part destructive operations? -

- -

- To answer these questions, we surveyed the Flix standard library to understand what - names are currently being used. The table below shows a small cross section of the results: -

- -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Functional OperationDestructive Equivalent
Array.mapArray.mapInPlace
Array.reverseArray.reverseInPlace
missingArray.sortByInPlace
Set.insertnot relevant – immutable
Set.unionnot relevant – immutable
missingMutSet.add
missingMutSet.addAll
MutSet.mapMutSet.transform
-

- -

- The table exposes the lack of any established naming convention. Let us consider some of the - many inconsistencies: For arrays, the functional and destructive operations are - named Array.map and Array.mapInPlace, but for mutable sets the - operations are named MutSet.map and MutSet.transform. - As another example, for immutable sets, we - have Set.insert and Set.union, but these - functional operations are missing on the mutable set. Moreover, the mutable version - of Set.union is called Set.addAll. - Finally, Array.sortByInPlace, what a name! -

- -

Exploring the Design Space

- -

- With these examples in mind, we tried to come up with a principled approach to naming. Our - exploration ended up with the following options: -

- - - Option I: Distinct names - - Proposal: We give distinct names to functional and destructive operations. For - example, we will have Array.map and Array.transform, - and MutSet.union and MutSet.addAll. We reserve the most - common names (e.g. map) for the functional operations. - - - Discussion: With distinct names there is little room for confusion, but it may be - difficult to come up with meaningful names. For example, what should the - destructive version of reverse be called? - - - - - Option II: Use similar names but with a prefix or suffix - - Proposal: We reuse names between functional and destructive operations. To - distinguish operations, we add a prefix or suffix to the name. For - example, reverseInPlace, inPlaceReverse, reverseMut, - or similar. - - - Discussion: The advantage of this approach is that names are immediately - consistent. The disadvantages are that: (i) it may be difficult to come up with a good - prefix or suffix word, (ii) some users may dislike the chosen prefix or suffix, and - (iii) it may be confusing that the signatures for two similarly named operations differ - not only in the return type, but also in the polymorphism of the arguments. - - - - - Option III: Use similar names but with a prefix or suffix symbol - - Proposal: Similar to the previous proposal, but instead we use a symbol. For - example: reverse!, reverse*, or the like. - - - Discussion: The same advantages and disadvantages of the previous proposal, but - with the difference that using a symbol may be more or less appealing to programmers. - - - - - Option IV: Use namespaces - - Proposal: We place all functional operations into one namespace and all - destructive operations into another. For example, we might - have Array.reverse and MutArray.reverse. - - - Discussion: While this solution appears simple, it has two downsides: (i) we now - have multiple functions named reverse with different semantics and (ii) we - get a plethora of namespaces for data structures that exist in both - immutable and mutable variants. For example, we might end up - with Set.map (functional map on an immutable - set), MutSet.Mut.map (destructive map on a mutable set), - and MutSet.Imm.map (functional map on a mutable set). - - - - - Option V: The Python approach: sort vs. sorted - - Proposal: In Python the sorted operation functionally returns a new - sorted list whereas the sort operation destructively sorts a list in place. - We use the same scheme - for reverse and reversed, map and mapped, - and so forth. - - - Discussion: An internet search reveals that many programmers are puzzled by the - Python naming scheme. Another disadvantage is that the common functional names, - e.g. map and reverse would be reserved for destructive - operations (unless we adopt the opposite convention of Python). - - - - - Option VI: Drop functional operations for mutable data - - Proposal: We drop support for functional operations on mutable data structures. - If the user wants to map a function over an array, mutable set, or mutable - map he or she must first convert it to an immutable data structure. For example, to - functionally reverse an array one would - write a.toList().reverse().toArray(). - - - Discussion: The "stick your head in the sand approach". The programmer must - explicitly convert back and forth between immutable and mutable data structures. - While such an approach side-steps the naming issue, it is verbose and slow - (because we have to copy collections back and forth). Deliberately leaving functionality - out of the standard library does not mean that programmers will not miss it; instead we - are just passing the problem onto them. - - - -

The Principles

- -

- We debated these options and slept on them for a few nights before we ultimately ended up - with the following hybrid principles: -

- - - Library: Mutable Data is Functional Data - - In Flix, every mutable data structure supports functional operations. - For example, mutable collections, such - as 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. - - - - - Library: Destructive Operations are Marked with '!' - - In Flix, every destructive operation is suffixed with an exclamation point. For - example, 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. - - - -

- As a side-note: Scheme has used ! to indicate destructive operations for a - long-time. -

- - - Library: Consistent Names of Functional and Destructive Operations - - In Flix, functional and destructive operations that share (i) similar behavior and (ii) - similar type signatures share similar names. For - example, 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.) - - - -

- We are in the process of refactoring the standard library to satisfy these new principles. -

- -

- Going forward, we are sensitive to at least four potential issues: - -

    -
  • Whether users come to like the aesthetics of names that end in exclamation point. -
  • -
  • If there is confusion about when exclamation points should be part of a name.
  • -
  • If there is confusion about when two operations should share the same name.
  • -
  • That Rust uses exclamation points for macro applications.
  • -
-

- -

- As Flix continues to mature, we will keep an eye on these issues. -

- -

- Until next time, happy hacking. -

- - -
-
- ); -} - -export default Naming diff --git a/src/page/blog/PolymorphicEffects.jsx b/src/page/blog/PolymorphicEffects.jsx deleted file mode 100644 index c8bd464..0000000 --- a/src/page/blog/PolymorphicEffects.jsx +++ /dev/null @@ -1,618 +0,0 @@ -import { useEffect } from 'react'; -import {Col, Container, Row} from "reactstrap"; -import InlineEditor from "../../util/InlineEditor"; - -function PolymorphicEffects() { - - useEffect(() => { - document.title = "Flix | Taming Impurity with Polymorphic Effects"; - window.location.replace("https://blog.flix.dev/blog/taming-impurity-with-polymorphic-effects/"); - }, []); - - return ( - - - - -

Taming Impurity with Polymorphic Effects

- -

- Posted May 2020 by Magnus Madsen. -

- -

- In the blog post Patterns of Bugs, Walter Bright, - the author of the D programming Language, writes about his - experience working at Boeing and their attitude towards failure: -

- -
-

- "[...] The best people have bad days and make mistakes, so the solution is to - change the process so the mistakes cannot happen or cannot propagate." -

- -

- "One simple example is an assembly that is bolted onto the frame with four bolts. The - obvious bolt pattern is a rectangle. Unfortunately, a rectangle pattern can be assembled - in two different ways, one of which is wrong. The solution is to offset one of the bolt - holes — then the assembly can only be bolted on in one orientation. The possible - mechanic's mistake is designed out of the system." -

- -

- "[...] Parts can only be assembled one way, the correct - way." -

-
- -

- (Emphasis mine). -

- -

- Bright continues to explain that these ideas are equally applicable to software: We should - build software such that it can only be assembled correctly. In this blog post, I will - discuss how this idea can be applied to the design of a type and effect system. In - particular, I will show how the Flix programming language and, by extension, its standard - library ensure that pure and impure functions are not assembled incorrectly. -

- -

Impure Functional Programming

- -

- A major selling point of functional programming is that it supports equational reasoning. - Informally, equational reasoning means that we can reason about programs by replacing an - expression by another one, provided they're both equal. For example, we can substitute - variables with the expressions they are bound to. -

- -

- For example, if we have the program fragment: -

- - - {`let x = 1 + 2; - (x, x)`} - - -

- We can substitute for x and understand this program as: -

- - - {`(1 + 2, 1 + 2)`} - - -

- Unfortunately, in the presence of side-effects, such reasoning breaks down. -

- -

- For example, the program fragment: -

- - - {`let x = Console.printLine("Hello World"); - (x, x)`} - - -

- is not equivalent to the program: -

- - - {`(Console.printLine("Hello World"), Console.printLine("Hello World"))`} - - - -

- Most contemporary functional programming languages, including Clojure, OCaml, and Scala, - forgo equational reasoning by allow arbitrary side-effects inside functions. To be clear, - it is still common to write purely functional programs in these languages and to reason - about them using equational reasoning. The major concern is that there is no language - support to guarantee when such reasoning is valid. Haskell is the only major programming - language that guarantees equational reasoning at the cost of a total and absolute ban on - side-effects. -

- -

- Flix aims to walk on the middle of the road: We want to support equational reasoning with - strong guarantees while still allowing side-effects. Our solution is a type and effect - system that cleanly separates pure and impure code. The idea of using an effect system - to separate pure and impure code is old, but our implementation, which supports type - inference and polymorphism, is new. -

- -

Pure and Impure Functions

- -

- Flix functions are pure by default. We can write a pure function: -

- - - {`def inc(x: Int): Int = x + 1`} - - -

- If we want to be explicit, but non-idiomatic, we can write: -

- - - {`def inc(x: Int): Int \\ {} = x + 1`} - - -

- where \ {} specifies that the inc function is pure. -

- -

- We can also write an impure function: -

- - - {`def sayHello(): Unit \\ IO = Console.printLine("Hello World!")`} - - -

- where \ IO specifies that the sayHello function is impure. -

- -

- The Flix type and effect system is sound, hence if we forget the \ IO annotation - on the sayHello function, the compiler will emit a type (or rather effect) error. -

- -

- The type and effect system cleanly separates pure and impure code. If an expression is pure - then it always evaluates to the same value and it cannot have side-effects. This is part - of what makes Flix functional-first: We can trust that pure functions behave like - mathematical functions. -

- -

- We have already seen that printing to the screen is impure. Other sources of impurity are - mutation of memory (e.g. writing to main memory, writing to the disk, writing to the - network, etc.). Reading from mutable memory is also impure because there is no guarantee - that we will get the same value if we read the same location twice. -

- -

- In Flix, the following operations are impure: -

- -
    -
  • Any use of channels (creating, sending, receiving, or selecting).
  • -
  • Any use of references (creating, accessing, or updating).
  • -
  • Any use of arrays (creating, accessing, updating, or slicing).
  • -
  • Any interaction with the Java world.
  • -
- -

Higher-Order Functions

- -

- We can use the type and effect system to restrict the purity (or impurity) of function - arguments that are passed to higher-order functions. This is useful for at least two - reasons: (i) it prevents leaky abstractions where the caller can observe implementation - details of the callee, and (ii) it can help avoid bugs in the sense of Walter Bright's - "Parts can only be assembled one way, the correct way." -

- -

- We will now look at several examples of how type signatures can control purity or impurity. -

- -

- We can enforce that the predicate f passed - to Set.exists is pure: -

- - - {`def exists(f: a -> Bool, xs: Set[a]): Bool = ...`} - - -

- The signature f: a -{">"} Bool denotes a pure function - from a to Bool. Passing an impure function - to exists is a compile-time type error. We want to enforce - that f is pure because the contract for exists makes no guarantees - about how f is called. The implementation of exists may - call f on the elements in xs in any order and any number of times. - This requirement is beneficial because its allows freedom in the implementation - of Set, including in the choice of the underlying data structure and in the - implementation of its operations. For example, we can implement sets using search trees or - with hash tables, and we can perform existential queries in parallel using - fork-join. If f was impure such implementation details would leak and be - observable by the client. Functions can only be assembled one way, the correct way. -

- -

- We can enforce that the function f passed to the - function List.foreach is impure: -

- - - {`def foreach(f: a -> Unit \\ IO, xs: List[a]): Unit \\ IO = ...`} - - -

- The signature f: a -{">"} Unit \ IO denotes an impure function - from b to Unit. Passing a pure function to foreach is - a compile-time type error. Given that f is impure and f is called - within foreach, it is itself impure. We enforce that - the f function is impure because it is pointless to apply - a pure function with a Unit return type to every element of a list. Functions - can only be assembled one way, the correct way. -

- -

- We can enforce that event listeners are impure: -

- - - {`def onMouseDn(f: MouseEvent -> Unit \\ IO): Unit \\ IO = ... -def onMouseUp(f: MouseEvent -> Unit \\ IO): Unit \\ IO = ...`} - - -

- Event listeners are always executed for their side-effect: it would be pointless to register - a pure function as an event listener. -

- -

- We can enforce that assertion and logging facilities are given pure functions: -

- - - {`def assert(f: Unit -> Bool): Unit = ... -def log(f: Unit -> String , l: LogLevel): Unit = ...`} - - -

- We want to support assertions and log statements that can be enabled and disabled at - run-time. For efficiency, it is critical that when assertions or logging is disabled, we do - not perform any computations that are redundant. We can achieve this by having the assert - and log functions take callbacks that are only invoked when required. A critical property of - these functions is that they must not influence the execution of the program. Otherwise, we - risk situations where enabling or disabling assertions or logging may impact the presence or - absence of a buggy execution. We can prevent such situations by requiring that the functions - passed to assert and log are pure. -

- -

- We can enforce that user-defined equality functions are pure. We want purity because the - programmer should not make any assumptions about how such functions are used. Moreover, most - collections (e.g. sets and maps) require that equality does not change over time to maintain - internal data structure invariants. Similar considerations apply to hash and comparator - functions. -

- -

- In the same spirit, we can enforce that one-shot comparator functions are pure: -

- - - {`def minBy(f: a -> b, l: List[a]): Option[a] = ... -def maxBy(f: a -> b, l: List[a]): Option[a] = ... -def sortBy(f: a -> Int32, l: List[a]): List[a] = ... -def groupBy(f: a -> k, l: List[a]): Map[k, List[a]] = ...`} - - -

- We can enforce that the next function passed - to List.unfoldWithIter is impure: -

- - - {`def unfoldWithIter(next: Unit -> Option[a] \\ IO): List[a] \\ IO`} - - -

- The unfoldWithIter function is a variant of the unfoldWith function where each - invocation of next changes some mutable state until the unfold completes. For - example, unfoldWithIter is frequently used to convert Java-style iterators into - lists. We want to enforce that next is impure because otherwise it is pointless - to use unfoldWithIter. If next is pure then it must always either - (i) return None which results in the empty list or (ii) - return Some(v) for a value v which would result in an infinite - execution. -

- -

- We can use purity to reject useless statement expressions. For example, the program: -

- - - {`def main(): Int = - List.map(x -> x + 1, 1 :: 2 :: Nil); - 123`} - - -

- is rejected with the compiler error: -

- - - {`-- Redundancy Error ------------------ foo.flix - - >> Useless expression: It has no side-effect(s) and its result is discarded. - - 2 | List.map(x -> x + 1, 1 :: 2 :: Nil); - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - useless expression.`} - - -

- Notice that the List.map(...) expression is pure because the function x - -> x + 1 is pure. -

- -

Polymorphic Effects

- -

- Flix supports effect polymorphism which means that the effect of a higher-order function can - depend on the effects of its function arguments. -

- -

- For example, here is the type signature of List.map: -

- - - {`def map(f: a -> b \\ ef, xs: List[a]): List[b] \\ ef = ...`} - - -

- The syntax f: a -{">"} b \ ef denotes a function - from a to b with latent effect ef. The signature of - the map function captures that its - effect ef depends on the effect of its argument f. - That is, if map is called with a pure function then its evaluation is pure, - whereas if it is called with an impure function then its evaluation is impure. The effect - signature is conservative (i.e. over-approximate). That is, - the map function is considered impure even in the special case when the list is - empty and its execution is actually pure. -

- -

- The type and effect system can express combinations of effects using boolean operations. - We can, for example, express that forward function composition >> is pure - if both its arguments are pure: -

- - - {` def >>(f: a -> b \\ ef1, g: b -> c \\ ef2): a -> c \\ { ef1, ef2 } = x -> g(f(x))`} - - -

- Here the function f has effect ef1 and g has - effect ef2. The returned function has effect ef1 and ef2, i.e. for it - to be pure both ef1 and ef2 must be pure. Otherwise it is impure. -

- -

Type Equivalences

- -

- Let us take a short detour. -

- -

- In a purely functional programming language, such as Haskell, mapping two - functions f and g over a list xs is equivalent to - mapping their composition over the list. That is: -

- - - {`map(f, map(g, xs)) == map(f >> g, xs)`} - - -

- We can use such an equation to (automatically) rewrite the program to one that executes more - efficiently because the code on the right only traverses the list once and avoids - allocation of an intermediate list. Haskell already has support for such rewrite rules built into the language. -

- -

- It would be desirable if we could express the same rewrite rules for programming languages - such as Clojure, OCaml, and Scala. Unfortunately, identities - such as the above - do not - hold in the presence of side-effects. For example, the program: -

- - - {`let f = x -> {Console.printLine(x); x}; -let g = y -> {Console.printLine(y); y}; -List.map(f, List.map(g, 1 :: 2 :: Nil))`} - - -

- prints 1, 2, 3, 1, 2, 3. But, if we apply the rewrite rule, the transformed - program now prints 1, 1, 2, 2, 3, 3! In the presence of side-effects we cannot - readily apply such rewrite rules. -

- -

- We can use the Flix type and effect to ensure that a rewrite rule like the above is only - applied when both f and g are pure! -

- -

- We can, in fact, go even further. If at most - one of f and g is impure then it is still safe to apply - the above rewrite rule. Furthermore, the Flix type and effect system is sufficiently - expressive to capture such a requirement! -

- -

- We can distill the essence of this point into the type signature: -

- - - {`def mapCompose(f: a -> b \\ e1, g: b -> c \\ {{(not e1) or e2}}, xs: List[a]): ... = ...`} - - -

- It is not important exactly what mapCompose does (or even if it makes sense). - What is important is that it has a function signature that requires two function - arguments f and g of which at most one may be impure. -

- -

- To understand why, let us look closely at the signature of mapCompose: -

- - - {`def mapCompose(f: a -> b \\ e1, g: b -> c \\ {{(not e1) or e2}}, xs: List[a]): ... = ...`} - - -

-

    -
  • - If e1 = T (i.e. f is pure) then (not e1) or e2 = F - or e2 = e2. In other words, g may be pure or impure. Its purity - is not constrained by the type signature. -
  • -
  • - If, on the other hand, e1 = F (i.e. f is impure) - then (not e1) or e2 = T or e2 = T . - In other words, g must be pure, otherwise there is a type error. -
  • -
-

- -

- If you think about it, the above is equivalent to the requirement that at most one - of f and g may be impure. -

- -

- Without going into detail, an interesting aspect of the type and effect system is - that we might as well have given mapCompose the equivalent (equi-most general) - type signature: -

- - - {`def mapCompose(f: a -> b \\ {{(not e1) or e2}}, g: b -> c \\ e1, xs: List[a]): ... = ...`} - - -

- where the effects of f and g are swapped. -

- -

Benign Impurity

- -

- It is not uncommon for functions to be internally impure but observationally pure. - That is, a function may use mutation and perform side-effects without it being observable - by the external world. We say that such side-effects are benign. Fortunately, we can - still treat such functions as pure with an explicit effect cast. -

- -

- For example, we can call a Java method (which may have arbitrary side-effects) but - explicitly mark it as pure with an effect cast: -

- - - {`/// -/// Returns the character at position \`i\` in the string \`s\`. -/// -def charAt(i: Int, s: String): Char = - import java.lang.String.charAt(Int32); - s.charAt(i) as \\ {}`} - - -

- We know that java.lang.String.charAt has is pure hence the cast is safe. -

- -

- An effect cast, like an ordinary cast, must be used with care. A cast is a mechanism - that allows the programmer to subvert the type (and effect) system. It is the - responsibility of the programmer to ensure that the cast is safe. Unlike type casts, an - effect cast cannot be checked at run-time with the consequence that an unsound effect cast - may silently lead to undefined behavior. -

- -

- Here is an example of a pure function that is implemented internally using mutation: -

- - - {`/// -/// Strip every indented line in string \`s\` by \`n\` spaces. \`n\` must be greater than \`0\`. -/// Note, tabs are counted as a single space. -/// -/// [...] -/// -def stripIndent(n: Int32, s: String): String = - if (n <= 0 or length(s) == 0) - s - else - stripIndentHelper(n, s) as \\ {} - -/// -/// Helper function for \`stripIndent\`. -/// -def stripIndentHelper(n: Int32, s: String): String \\ IO = - let sb = StringBuilder.new(); - let limit = Int32.min(n, length(s)); - let step = s1 -> { - let line = stripIndentDropWhiteSpace(s1, limit, 0); - StringBuilder.appendLine!(sb, line) - }; - List.foreach(step, lines(s)); - StringBuilder.toString(sb)`} - - -

- Internally, stripIndentHelper uses a mutable string builder. -

- -

Type Inference and Boolean Unification

- -

- The Flix type and effect system supports inference. Explicit type annotations are never - required locally within a function. As a design choice, we do require type signatures for - top-level definitions. Within a function, the programmer never has to worry about pure and - impure expressions; the compiler automatically infers whether an expression is pure, impure, - or effect polymorphic. The programmer only has to ensure that the declared type and effect - matches the type and effect of the function body. -

- -

- The details of the type and effect system are the subject of a forthcoming research paper - and will be made available in due time. -

- -

Closing Thoughts

- -

- The Flix type and effect system separates pure and impure code. The upshot is that a - functional programmer can trust that a pure function behaves like a mathematical function: - it returns the same result when given the same arguments. At the same time, we are still - allowed to write parts of the program in an impure, imperative style. Effect polymorphism - ensures that both pure and impure code can be used with higher-order functions. -

- -

- We can also use effects to control when higher-order functions require pure (or impure) - functions. We have seen several examples of such use cases, e.g. requiring - that Set.count takes a pure function or - that List.unfoldWithIter takes an impure function. Together, these restrictions - ensure that functions can only be assembled in one way, the correct way. -

- -

- Until next time, happy hacking. -

- - -
-
- ); -} - -export default PolymorphicEffects diff --git a/src/page/blog/ProgrammingLanguageDefense.jsx b/src/page/blog/ProgrammingLanguageDefense.jsx deleted file mode 100644 index b02012c..0000000 --- a/src/page/blog/ProgrammingLanguageDefense.jsx +++ /dev/null @@ -1,339 +0,0 @@ -import { useEffect } from 'react'; -import {Col, Container, Row} from "reactstrap"; -import {Link} from "react-router-dom"; - -function ProgrammingLanguageDefense() { - - useEffect(() => { - document.title = "Flix | In Defense of Programming Languages"; - window.location.replace("https://blog.flix.dev/blog/in-defense-of-programming-languages/"); - }, []); - - return ( - - - - -

In Defense of Programming Languages

- -

- Posted July 2021 by Magnus Madsen. Thanks to Matthew Lutze for discussion and - comments on an early draft. -

- -

- This blog post is written in defense of programming language enthusiasts; whether they are - compiler hackers, programming language hobbyists, industry professionals, or academics. -

- -

- In this blog post, I want to examine the discourse around programming languages and - especially how new programming languages are received. My hope is to improve communication - between programming languages designers and software developers. I understand that - we cannot all agree, but it would be fantastic if everyone could at least try to be - friendly, to be intellectually curious, and to give constructive feedback! -

- -

A Few Quotes from the Internet

- -

- Let me set the stage with a few quotes from social media tech sites (e.g. Reddit, - HackerNews, Twitter, etc.). I have lightly edited and anonymized the following quotes: -

- -
-

"Great! Yet-another-programming-language™. This is exactly what we - need; the gazillion of existing programming languages is not enough!"

-
Furious Panda via - Reddit -
-
- -
-

"This is – by far – the worst syntax I have ever seen in a functional - language!"

-
Irate Penguin via - Reddit -
-
- -
-

"The language is probably great from a technical point of view, but - unless Apple, Google, Mozilla, or Microsoft is on-board it is pointless."

-
Angry Beaver via - HackerNews -
-
- -
-

"How can anyone understand such weird syntax? I hate all these - symbols."

-
Bitter - Turtle via Reddit -
-
- -
-

"The examples all look horrible. The site looks horrible. This needs a - lot of work before it gets close to anything I would even consider using."

-
Enraged - Koala via Twitter -
-
- -

- While all of the above quotes are in response to news about the Flix programming language - (on whose website you are currently reading this blog post), depressingly similar comments - are frequently posted in response to news about other new programming languages. -

- -

- Why do people post such comments? And what can be done about it? -

- -

Where do such comments come from?

- -

- I think there are two reasons which are grounded in legitimate concerns: -

- -

-

    -
  • - Fatigue: I think there is a sense that there are new programming - languages coming out all the time. Paradoxically, I think there is both a dread of - having to keep up with ever-changing programming languages (and other technologies) - and simultaneously a sense that these new programming languages are all the same. -
  • - -
  • - Speech: Programming languages are the material with which we craft programs: - It is our way of "speaking" algorithmically. They are about what we say, how we say - it, and even what can be said. Like prose, what is beautiful and elegant is - in the eye of the beholder. It is not surprising then that when a new programming - language comes along and suggests a different form of expression that some may have - strong reactions to. -
  • -
-

- -

- Of course there are also internet trolls; but let us ignore them. -

- -

A Point-by-Point Rebuttal

- -

- I want to give a point-by-point rebuttal to the most common refrains heard whenever a new - programming language is proposed. -

- -
Do we really need new programming languages?
- -

- The Flix FAQ joking responds to this question with a rhetorical - question: Do we really need safer airplanes? Do we really need electric cars? Do we - really need more ergonomic chairs? -

- -

- I think it is a valid argument. We want better programming languages because we want to - offer software developers better tools to write their programs. You might say that existing - programming languages already have all the feature we need, but I think that there are - exciting developments; both brand new ideas and old research ideas that are making their - way into new programming languages: -

- -

-

    -
  • - Safety: region-based memory management, lifetimes, ownership types, linear - types, 2nd class values, and capabilities. -
  • -
  • - Expressiveness: union and intersection types, polymorphic effect systems, - algebraic effects, type-driven development, increasingly powerful type inference. -
  • -
  • - Development Experience: the Visual Studio Code ecosystem, the language server - protocol, GitHub code-spaces. -
  • -
-

- -

- I don't think we are anywhere near to the point where programming languages are as good as - they are ever going to get. On the contrary, I think we are still in the infancy of - programming language design. -

- -
All programming languages are the same
- -

- I strongly disagree. I think we are experiencing a period of programming language - fragmentation after a long period of consolidation and stagnation. For the last 15-years or - so, the industry has been dominated by C, C++, C# and Java. The market share of these - programming languages was always increasing and they were the default safe choice for new - software projects. -

- -

- Today that is no longer the case. The ecosystem of programming languages is much more - diverse (and stronger for it). We have Rust. We have Scala. We also have Go, Python, and - JavaScript. There is also Crystal, Elixir, Elm, and Nim (Oh, and Flix of course!) We are in - a period of fragmentation. After a decade of object-oriented ossification we are entering a - new and exciting period! -

- -

- If history repeats itself then at some point we will enter a new period of consolidation. - It is too early to speculate on which programming languages will be the winners, but I feel - confident that they will be much better than C, C++, C#, and Java! (Ok, maybe C++30 will be - one of them – that language changes as much as Haskell!) -

- -

- (Addendum: That said, it is true that many hobby programming languages look the same. But - there is a reason for that: if you want to learn about compilers it makes sense to start by - implementing a minimal functional or object-oriented programming language.) -

- -
New programming languages are too complicated!
- -

- That's the way of the world. -

- -

- What do you think an airline pilot from the 1950's would say if he or she entered the flight - deck of an Airbus A350? Sure, the principles of flying are the same, and indeed iteration - and recursion are not going anywhere. But we cannot expect everything to stay the same. All - those instruments are there for a reason and they make flying safer and more efficient. -

- -

- As another example, once universities start teaching Rust (and we will!) then programming - with ownership and lifetimes will become commonplace. As yet another example, today every - programmer can reasonably be expected to know about filter and map, - but that was certainly not the case 15 years ago! -

- -
A programming language cannot be successful unless a major tech company is behind it
- -

- Historically that has not been true. Neither PHP, Python, Ruby, Rust, or Scala had - major tech companies behind them. If industry support came, it came at a later time. -

- -

Ideas for Better Communication

- -

- With these points in mind, I want to suggest some ways to improve communication between - aspiring programming language designers and software developers: -

- -

- When presenting a new programming language (or ideas related to a new language): -

- -

-

    -
  • - Scope: State the intended scope of the project. Is it a hobby project - made for fun? Is it an open source project hoping to gain traction? Is it a research - prototype? Is it a commercially backed project? What is the intended use case? Is - there a "killer-app"? -
  • - -
  • - Implementation: What has been implemented? A compiler? An interpreter? Do you - have a standard library? How big is it? How many lines is the project? -
  • - -
  • - Novelty: What is new in the programming language? Are there some new takes - on old ideas? Is there something novel? How is the language an improvement compared - to existing languages? Does the language make you think in a new way about - programming? -
  • - -
  • - Resources: What resources are behind the programming language? Is it a hobby - project? An open source project? An academic project? Are you open to collaboration? - Do you have backing (from industry or otherwise)? -
  • - -
  • - Feedback: What kind of feedback are you looking for? What other people think? - Suggestions for improvements and related work? Constructive criticism about the - design? What it would take for someone to consider using it? -
  • - -
  • - Reality Check: Try to avoid grandiose or unsubstantiated claims: Do your - compiler really outperform modern state-of-the-art C compilers? Is your type system - really more expressive than Haskell or Idris? Is your language really safer than - Ada? -
  • -
-

- -

What about Flix?

- -

- The time has come to nail our colors to the flag: -

- -

-

    -
  • - Scope: We are building a real programming language intended for real-world - use. It is an open-source project lead by academic programming language researchers. -
  • - -
  • - Implementation: The Flix compiler project is ~137,000 lines of code. We have - a realistic compiler, a standard library (extensive, but still under development), a - Visual Studio Code extension (with auto-complete!), an online playground, online - documentation, and several published papers on the novel aspects of the language. -
  • - -
  • - Novelty: We have a whole page (Innovations) - that covers this, but briefly: a unique combination of features, combined with - first-class Datalog constraints and a polymorphic effect system. -
  • - -
  • - Resources: We are a group of programming language researchers from Aarhus - University and the University of Waterloo together with a small community of open - source contributors. Through our research we have funding for working on Flix. -
  • - -
  • - Feedback: We want to know what people think about Flix, how we can make Flix - better, and what it would take for someone to consider using it. -
  • - -
  • - Reality Check: We aim to under-promise and over-deliver. We do not promote - features before they exist. Our typical pipeline is: (Research) Idea → - Implementation → Documentation → Presentation to the World. Development is not - secret; everything is on GitHub. We just don't promote anything before it is ready. - We have exciting things in the pipeline, but you will have to wait a bit before - learning about them (or spoil yourself by diving into the GitHub issues!) -
  • -
-

- -

- Until next time, happy hacking. -

- - -
-
- ); -} - -export default ProgrammingLanguageDefense diff --git a/src/page/blog/Redundancies.jsx b/src/page/blog/Redundancies.jsx deleted file mode 100644 index 0f7f8d5..0000000 --- a/src/page/blog/Redundancies.jsx +++ /dev/null @@ -1,492 +0,0 @@ -import { useEffect } from 'react'; -import {Col, Container, Row} from "reactstrap"; -import InlineEditor from "../../util/InlineEditor"; - -function Redundancies() { - - useEffect(() => { - document.title = "Flix | Redundancies as Compile-Time Errors"; - window.location.replace("https://blog.flix.dev/blog/redundancies-as-compile-time-errors/"); - }, []); - - return ( - - - - -

Redundancies as Compile-Time Errors

- -

- Posted February 2020 by Magnus Madsen. -

- -

- As software developers, we strive to write correct and maintainable code. - Today, I want to share some code where I failed in these two goals. -

- -

- I will show you real-world code from the Flix compiler and ask you to determine what is - wrong with the code. Then, later, I will argue how programming languages can help avoid the - type of problems you will see. (Note to the reader: The Flix compiler is (currently) written - in Scala, so the code is in Scala, but the lessons learned are applied to the Flix - programming language. I hope that makes sense.) -

- -

- Let us begin our journey by looking at the following code fragment: -

- - - {`case Expression.ApplyClo(exp, args, tpe, loc) => - val e = visitExp(exp) - val as = args map visitExp - Expression.ApplyClo(e, as, tpe, loc) - -case Expression.ApplyDef(sym, args, tpe, loc) => - val as = args map visitExp - Expression.ApplyDef(sym, as, tpe, loc) - -case Expression.Unary(op, exp, tpe, loc) => - val e = visitExp(exp) - Expression.Unary(op, exp, tpe, loc) - -case Expression.Binary(op, exp1, exp2, tpe, loc) => - val e1 = visitExp(exp1) - val e2 = visitExp(exp2) - Expression.Binary(op, e1, e2, tpe, loc)`} - - -

- Do you see any issues? -

- -

- If not, look again. -

- -

- Ok, got it? -

- -

- The code has a subtle bug: In the case for Unary the local - variable e holds the result of the recursion on exp. But by - mistake the reconstruction of Unary uses exp and - not e as intended. The local variable e is unused. Consequently, - the specific transformations applied by visitExp under unary expressions are - silently discarded. This bug was in the Flix compiler for some time before it was - discovered. -

- -

- Let us continue our journey with the following code fragment: -

- - - {`case ResolvedAst.Expression.IfThenElse(exp1, exp2, exp3, tvar, evar, loc) => - for { - (tpe1, eff1) <- visitExp(exp1) - (tpe2, eff2) <- visitExp(exp2) - (tpe3, eff3) <- visitExp(exp3) - condType <- unifyTypM(mkBoolType(), tpe1, loc) - resultTyp <- unifyTypM(tvar, tpe2, tpe3, loc) - resultEff <- unifyEffM(evar, eff1, eff2, loc) - } yield (resultTyp, resultEff)`} - - -

- Do you see any issues? -

- -

- If not, look again. -

- -

- Ok, got it? -

- -

- The code has a similar bug: The local variable eff3 is not used, but it - should have been used to compute resultEff. While this bug never made it into - any release of Flix, it did cause a lot of head-scratching. -

- -

- Now we are getting the hang of things! What about this code fragment?: -

- - - {`/** - * Returns the disjunction of the two effects \`eff1\` and \`eff2\`. - */ -def mkOr(ef1f: Type, eff2: Type): Type = eff1 match { - case Type.Cst(TypeConstructor.Pure) => Pure - case Type.Cst(TypeConstructor.Impure) => eff2 - case _ => eff2 match { - case Type.Cst(TypeConstructor.Pure) => Pure - case Type.Cst(TypeConstructor.Impure) => eff1 - case _ => Type.Apply(Type.Apply(Type.Cst(TypeConstructor.Or), eff1), eff2) - } -}`} - - -

- Do you see any issues? -

- -

- I am sure you did. -

- -

- The bug is the following: The formal parameter to mkOr is - misspelled ef1f instead of eff1. But how does this even compile, - you ask? Well, unfortunately the mkOr function is nested inside another - function that just so happens to have an argument also named eff1! - Damn Murphy and his laws. The intention was for the formal parameters - of mkOr to shadow eff1 (and eff2), but because of the - misspelling, ef1f ended up as unused and eff1 (a completely - unrelated variable) was used instead. The issue was found during development, but not - before several hours of wasted work. Not that I am bitter or anything... -

- -

- We are almost at the end of our journey! But what about this beast: -

- - - {`/** - * Returns the result of looking up the given \`field\` on the given \`klass\`. - */ -def lookupNativeField(klass: String, field: String, loc: Location): ... = try { - // retrieve class object. - val clazz = Class.forName(klass) - - // retrieve the matching static fields. - val fields = clazz.getDeclaredFields.toList.filter { - case field => field.getName == field && - Modifier.isStatic(field.getModifiers) - } - - // match on the number of fields. - fields.size match { - case 0 => Err(NameError.UndefinedNativeField(klass, field, loc)) - case 1 => Ok(fields.head) - case _ => throw InternalCompilerException("Ambiguous native field?") - } -} catch { - case ex: ClassNotFoundException => - Err(NameError.UndefinedNativeClass(klass, loc)) -}`} - - -

- Do you see any issues? -

- -

- If not, look again. -

- -

- Ok, got it? -

- -

- Still nothing? -

- -

- Pause for dramatic effect. -

- -

- Morpheus: What if I told you... -

- -

- Morpheus: ... that the function has been maintained over a long period of time... -

- -

- Morpheus: But that there is no place where the function is called! -

- -

- I am sorry if that was unfair. But was it really? The Flix code base is more than 100,000 - lines of code, so it is hard to imagine that a single person can hold it in his or her head. -

- -

- As these examples demonstrate, and as has been demonstrated in the research literature (see - e.g. Xie and Engler 2002), - redundant or unused code is often buggy code. -

- -

- To overcome such issues, Flix is very strict about redundant and unused code. -

- -
Flix Treats Unused Code as Compile-Time Errors
- -

- The Flix compiler emits a compile-time error for the following redundancies: -

- -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TypeDescription
Unused DefA function is declared, but never used.
Unused EnumAn enum type is declared, but never used.
Unused Enum CaseA case (variant) of an enum is declared, but never used.
Unused Formal ParameterA formal parameter is declared, but never used.
Unused Type ParameterA function or enum declares a type parameter, but it is never used.
Unused Local VariableA function declares a local variable, but it is never used.
Shadowed Local VariableA local variable hides another local variable.
Unconditional RecursionA function unconditionally recurses on all control-flow paths.
Useless Expression StatementAn expression statement discards the result of a pure expression.
-

- -

- As the Flix language grows, we will continue to expand the list. -

- -

- Let us look at three concrete examples of such compile-time errors. -

- -
Example I: Unused Local Variable
- -

- Given the program fragment: -

- - - {`def main(): Bool = - let l1 = List.range(0, 10); - let l2 = List.intersperse(42, l1); - let l3 = List.range(0, 10); - let l4 = List.map(x -> x :: x :: Nil, l2); - let l5 = List.flatten(l4); - List.exists(x -> x == 0, l5)`} - - -

- The Flix compiler emits the compile-time error: -

- - - - {`-- Redundancy Error -------------------------------------------------- foo.flix - ->> Unused local variable 'l3'. The variable is not referenced within its scope. - -4 | let l3 = List.range(0, 10); - ^^ - unused local variable. - - -Possible fixes: - - (1) Use the local variable. - (2) Remove local variable declaration. - (3) Prefix the variable name with an underscore. - - -Compilation failed with 1 error(s).`} - - -

- The error message offers suggestions for how to fix the problem or alternatively how to make - the compiler shut up (by explicitly marking the variable as unused). -

- -

- Modern programming languages like Elm and Rust offer a similar feature. -

- -
Example II: Unused Enum Case
- -

- Given the enum declaration: -

- - - {`enum Color { - case Red, - case Green, - case Blue -}`} - - -

- If only Red and Green are used then we get the Flix compile-time - error: -

- - - {`-- Redundancy Error -------------------------------------------------- foo.flix - ->> Unused case 'Blue' in enum 'Color'. - -4 | case Blue - ^^^^ - unused tag. - -Possible fixes: - - (1) Use the case. - (2) Remove the case. - (3) Prefix the case with an underscore. - -Compilation failed with 1 error(s).`} - - -

- Again, programming languages like Elm and Rust offer a similar feature. -

- -
Example III: Useless Expression Statement
- -

- Given the program fragment: -

- - - {`def main(): Int = - List.map(x -> x + 1, 1 :: 2 :: Nil); - 123`} - - -

- The Flix compiler emits the compile-time error: -

- - - {`-- Redundancy Error -------------------------------------------------- foo.flix - ->> Useless expression: It has no side-effect(s) and its result is discarded. - -2 | List.map(x -> x + 1, 1 :: 2 :: Nil); - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - useless expression. - - -Possible fixes: - - (1) Use the result computed by the expression. - (2) Remove the expression statement. - (3) Introduce a let-binding with a wildcard name. - -Compilation failed with 1 error(s).`} - - - -

- The problem with the code is that the evaluation of List.map(x -{">"} x + 1, 1 :: 2 :: - Nil) has no side-effect(s) and its result is discarded. -

- -

- Another classic instance of this problem is when someone calls - e.g. checkPermission(...) and expects it to throw an exception if the user has - insufficient permissions, but in fact, the function simply returns a boolean which is then - discarded. -

- -

- But this is not your Grandma's average compile-time error. At the time of writing, I - know of no other programming language that offers a similar warning or error with - the same precision as Flix. If you do, please drop me a line on Gitter. (Before someone - rushes to suggest must_use and friends, please consider whether they work in - the presence of polymorphism as outlined below). -

- -

- The key challenge is to (automatically) determine whether an expression is pure - (side-effect free) in the presence of polymorphism. Specifically, the call - to List.map is pure because the function argument x -{">"} x + - 1 is pure. In other words, the purity of List.map depends on the purity - of its argument: it is effect polymorphic. The combination of type inference, - fine-grained effect inference, and effect polymorphism is a strong cocktail that I plan to - cover in a future blog post. -

- -

- Note: The above is fully implemented in master, but has not yet been "released". -

- -
Closing Thoughts
- -

- I hope that I have convinced you that unused code is a threat to correct and maintainable - code. However, it is a threat that can be neutralized by better programming language design - and with minor changes to development practices. Moreover, I believe that a compiler that - reports redundant or unused code can help programmers – whether inexperienced or - seasoned – avoid stupid mistakes that waste time during development. -

- -

- A reasonable concern is whether working with a compiler that rejects programs with unused - code is too cumbersome or annoying. In my experience, the answer is no. After - a small learning period, whenever you want to introduce a new code fragment that will not - immediately be used, you simple remember to prefix it with an underscore, and then later you - come back and remove the underscore when you are ready to use it. -

- -

- While there might be a short adjustment period, the upside is huge: The compiler - provides an iron-clad guarantee that all my code is used. Moreover, whenever I refactor some - code, I am immediately informed if some code fragment becomes unused. I think such long-term - maintainability concerns are significantly more important than a little bit of extra work - during initial development. -

- -

- Until next time, happy hacking. -

- - -
-
- ); -} - -export default Redundancies