Flix – Next-generation reliable, concise, functional-first programming language
flix.dev
flix.dev
Seems to me that Flix would further blur the line between logic and functional programming (using what looks like some Prolog-esque search and datalog integration), amazing!
If I was designing a new modern language, I would make the default types structural records and variants and use a single "new type" form for creating nominal types, when necessary. Compositional datatypes are much easier to achieve with structural records/variants (e.g. define a new enum set based on another). It is also trivial to make a structural type nominal, but not to go the other way. But having structural records as something special alongside nominal ADTs (like Flix, Elm and Purescript), does not exploit the duality and does not give a modern rethink that IMHO is needed.
class ExpSYM a where
add :: a Int → a Int → a Int
lit :: Int → a Int
class MulSYM a where
mul :: a Int → a Int → a Int
Now suppose we extend with booleans and so on. We can freely combine addition and multiplication. As we add new data variants, old code need not be recompiled. We get the openness of OOP's class extensions with the flexibility of ADTs. ex1 :: (ExpSYM a, MulSYM a) => a Int
ex1 = add (lit 3) (mul (lit 4) (lit 5))
Not enough space here but you can also pattern match over final terms and interpret them in different ways (e.g. CBN vs CBV interpreters).Then as you go through your compiler pass you can see through the types that "capabilities" get reduced more and more until you reach some base language (sounds like free monads doesn't it?).
Tangentially if I were to design an FP language I would use tagless final for everything, and deforest as much as I can (if possible) to reduce overhead. How this affects other aspects (e.g. automatically translating pattern matching) I have not looked into, but it's interesting nonetheless.
getDates
:: Exp
-> Set Date
getDates = cata alg where
alg :: ExpF (Set Date) -> Set Date
alg (EDate i) = S.singleton i
alg e = fold e
substitute
:: Map VarId (ExpF Exp)
-> Exp
-> Exp
substitute env = cata alg where
alg :: ExpF Exp -> Exp
alg (EVar i)
| Just e <-M.lookup i env = Fix e
alg e = Fix eAs for matching remaining terms, I don't know the answer to that, would be interesting to investigate.
Yeah, having many typeclasses involved could be problematic. What sort of issues are you thinking of? One possibility is that so many constraints are involved no concrete type can instantiate a final term.
No this is not correct. Oleg provides a solution to double negation by creating a catamorphism that folds to a function. It's still a catamorphism. And folding to a function is not something the average Java programmer is likely to understand easily.
Edit: I'm not quite as negative as I sound, but given the headline (or the CTA on the homepage for that matter), I am much less than whelmed. This seems fine I guess, but just another "check all the boxes" language that doesn't really bring much of anything obvious new to the table.
Full disclosure: I have contributed a small amount of code to the standard library of Flix.
Personally I find the inclusion of Datalog as a first-class citizen in an otherwise functional language to be a pretty powerful feature that I personally haven't really seen done before.
It's neat, but IIUC, it needing to be a language feature (versus being included as a library) emerges from the lack of macro support. That's one of my issues with non-macro languages, I have to wait for the language owners to add this stuff.
EDIT: I hope to explore a macro system for Flix in the future. So we are definitely on the same side of the issue; just wanted to point out that the current system is more powerful.
Flix aims to occupy a unique point in the design space. It combines features from programming languages like Go, Haskell, OCaml, and Scala in a way that no other language does. It has:
- a sound type system (like Haskell and OCaml)
- a complete local type inference algorithm (like Haskell and OCaml)
- eager evaluation (like OCaml and Scala)
- CSP style concurrency (like Go)
- type classes (like Haskell)
- full tail call elimination (like OCaml and Haskell)
If you look at the "Innovations" page you will find that Flix has (at least) two features found in no other programming language: a polymorphic type and effect system and first-class Datalog constraints.
(I am one of the authors of Flix).
I think the effect system is cool. How does Flix differ from Eff or Unison in this matter?
The effect system in Flix is focused on a single effect: purity. It is very fine-grained (compared to e.g. systems based on rows) and it supports effect inference and effect polymorphism. Eff and Unison have richer effect systems - based on algebraic effects - which allow re-interpretation of effects. However these effects system struggle more with effect inference. For example, I believe that in Eff you only have a restricted form of effect polymorphism / the use of higher-order functions is more limited.
(I am one of the authors of Flix)
That does sound powerful! To be honest I have no idea what the implications of that are - but I guess that's why it's research!
Re: effect system.
The reason that I think algebraic effects are so cool is that they allow for trackable, side-effectful, and powerful abstractions.
To me it sounds like Flix's Pure/Impure effects offer a fine-grained form of taint tracking. Why do you think I should be excited about this? For example, why not Haskell's monad solution?
I hope I don't come off as confrontational, I'm genuinely interested :-).
edit: I'm asking because, from what I understand, the DOT calculus puts the Scala type system on sound foundations.
It's like if Scala, Java and Haskell had a one night stand in the center of Chernobyl.
I'm stealing the "had a one night stand in the middle of Chernobyl", that's hilarious!
This language looks like a perfect fit.
Could this be ported to other "hosting" languages/VMs? In any case, I think interoperability with foreign code needs a different solution and it has to have a way to fully deal with OOP constructs.
Built-in Datalog is indeed a surprising feature. Strongly reminded me that we may come to a universal data model someday just like we rely on universal computational model (Turing / von Neumann) nowadays. Under that model, we would not make distinctions between various sources of data and we would be able to efficiently query them in every structural dimension they offer.
(I am one of the authors of Flix)
> Can we just stop building new programming languages?
We build new programming languages and we do research on programming languages because we want to offer developers better tools to write programs. We want tools that make it simpler, safer, and faster to write correct programs. We want tools that increase productivity and that aid in program maintenance.
The situation is not unlike why we would want better airplanes, better houses, or better surgical instruments.
(I am one of the authors of Flix)
>We are planning to define division by zero as yielding zero
Is there a non mathematical reason for that choice?
- Division must be impure (because it can throw an exception, crash the program, etc.)
- Division must be partial - i.e. return Option[Int].
Both seem worse compared to defining division by zero as zero. Coq, Lean, and Pony do the same. https://github.com/ponylang/pony-tutorial/blob/master/conten...
When you write code that may divide by zero you need to check the result afterwards (and in truly defensive programming that is every time you divide numbers). 0 is not a great choice because it doesn't necessarily mean you divided by zero - in floating point, dividing by a sufficiently large number or when working with the status register set to flush denormals to zero then normal division can result in 0. For integer division, any fraction will result in 0.
No valid division can result in NaN or INT_MAX.
- introduce a non-zero number type, define division as Number/NonZeroNumber -> Number, and provide a simple, non-verbose way to convert a Number into NonZeroNumber (with default value in case of zero, and/or direct assignment after a non-zero-check)
- optionally introduce an unsafe division operator that takes two Numbers and returns Option[Number]
Recent | Bread-
News | and Butter
| Functional
2020-11-20 | Programming
We are on |
Twitter! Find | Flix supports
us at | basic building
@flixlang. | blocks of typed
| functional programs
(Android, recent Firefox)it'd probably be good to switch the divs to `class="col-xs-12 col-md-6"` or equivalent
"Flix can be thought of as a meta-programming language for Datalog."
So far so good! :)
> Separate pure and impure code
So IIUC, the effect system [2] means impurity is contagious. This seems reasonable to me, though I wonder what that means in practice. `Console.printLine` is impure [3], and presumably any debug logging would be too; the contagion might spread pretty far. I wonder if this is why seemingly pure functions like `Array.find` are flagged as impure [4].
> Principle of least surprise: ... and when there is no immediately obvious default, we should not have a default at all, but force the programmer to be explicit about his or her intention.
I like that they called this part out. I'd wager most code really doesn't have sane defaults, just the defaults that made sense to the developer at the time.
> Local type inference
I find that in languages with type signatures (without inference), folks lean on the type to convey information, and in dynamic languages like Clojure, folks lean on good naming. Based on my not-much experience with type inference, the code ends up with neither. Sure the compiler can figure out what it is, but the humans not so much.
> Uniform function call syntax: ... the function call length(xs) can also be written as xs.length()
I wonder what their motivation was for this; my guess is to appeal to both FP and OO devs. IMO, having multiple ways of writing the same thing is likely to lead to unnecessary style wars.
> Private by default: ... declarations are hidden by default (i.e. private) and cannot be accessed from outside of their namespace
I like this a lot, though I foresee running into issues with unit testing, and thereby arguments over at what level things should be tested.
> Bugs are not recoverable errors: ... For recoverable errors, we should enforce that they are checked and handled. For program bugs, we should terminate execution as quickly as possible
This seems reasonable to me, but I'm curious what counts as a "program bug", or how to know that some error is recoverable.
> No null value
Meh. I've think Option is just null with extra steps. NPEs arise because of method invocation, e.g., `x.foo()` while `x` is null. In FP languages like Clojure, NPEs don't really happen since `x` is just a value being passed to a function. With nil-punning and reasonable behavior of core functions (e.g., `(get m k)` yields nil when `m` is nil), an unexpected nil is really a type error.
> No dead or unreachable code / No unused variables
Perhaps it's from my REPL-driven, exploratory style of development, but I think having to constantly comment out code I'm ambivalent about just to get it to compile would be a bit annoying. I'd think something like this should be a compile option, but I think that runs afoul of their "One Language"/"no flags" principle.
> No variadic (varargs) functions
Having worked with enough Clojure, and seen enough bugs when using `apply` on vararg fns, I think this is a good choice. IIUC, their approach would replace, e.g., `(+ x y z ...)` with `(sum [x y z ...])`.
> No binary or octal literals: ... It is our understanding that these features are rarely used in practice.
Until you need to do some bit-twiddling; maybe they still have hex. I wonder what they considered the downside of including it.
[1] https://flix.dev/principles/
[2] https://flix.dev/innovations/
Source: i worked a bit with the language during Spring.
Edit: I just noticed that `List.toArray` -- which just returns a new array, so no concern over references or mutation -- is also marked as impure. This seemed wrong to me, but then I noticed that even `Array.new()` is marked impure. To my mind, a function that allocates a new mutable collection is itself not inherently impure.
That always has to be true, no? Reading from a mutable datastructure is impure in the same way that reading from standard input is.
> Edit: I just noticed that `List.toArray` -- which just returns a new array, so no concern over references or mutation -- is also marked as impure. This seemed wrong to me, but then I noticed that even `Array.new()` is marked impure. To my mind, a function that allocates a new mutable collection is itself not inherently impure.
Two different arrays with the same members are not generally equivalent. E.g. (x.toArray, x.toArray) is semantically something very different from {val y = x.toArray; (y, y)}.
If the function internally reads from stdin, I'd agree. But if a fn takes an input stream as an arg, and if given input streams that yield the same bytes the fn always yields the same result, then why consider it an impure fn? The input stream is just a fancy data structure for bytes.
> Two different arrays with the same members are not generally equivalent.
Derp, of course! And though I guess it would be possible to have the `Mut*` collections use value equality instead of reference equality, that'd probably conflict with the performance goals of the mutable variant.
Hmm. It's impossible to create a value that's equivalent to reading from the input stream, because reading from the input stream has operational effects. But that logic doesn't apply to an array if your access to the array is unobservable. (One could argue that reading from an array creates temporal relationships, but I don't think that really holds up). So I think you're right, and a function that accepts a mutable datastructure can still be pure (in a vacuous sense really, since a mutable datastructure can't ever be equivalent to a value - it's not even equivalent to itself at a different time), though given that that purity can't ever be useful to you I don't think it's particularly important.
> And though I guess it would be possible to have the `Mut*` collections use value equality instead of reference equality, that'd probably conflict with the performance goals of the mutable variant.
It's not about the implementation of .equals(), it's about semantic equivalence. Two different arrays with the same members behave quite differently than two references to the same array (as the program continues and other code mutates them), regardless of whether they compared equal at the start.
let x = 123; let y = (x, x)
If you substitute x into the pair you get the same result. So far, so good.
Now consider:
let x = [1, 2, 3]; x[0] = 5; let y = x[0]
If you substitute x into the body of y you get a different result! The meaning of x[0] changed underneath you. We are very conservative and say that anything which touches an array is impure. Is this the best solution? no- but it is a reasonable/sound start.
(I am one of the authors of Flix).
> But in a nutshell if something is pure it should support substitution.
Neat, hadn't thought about it like that before.
> If you substitute x into the body of y you get a different result!
Heh, I guess that depends on "which" x. I recognize Flix doesn't allow shadowing as a defensive choice, but with respect to substitution, wouldn't this be equivalent to something like:
let x = [1, 2, 3]
let x = insert(0, 5, x) # treating it like Map [1]
let y = get(0, x)
IOW, it's not clear to me why the `x[0] = 5` step would be skipped when considering substitution. Hmm, per the substitution principle, is `x[0] = 5` pure?> We are very conservative and say that anything which touches an array is impure.
Understandable given how difficult it is to reason about mutable things.
----
[1] Given the uniform function call syntax, I found it odd that the map is the last arg in the function. I originally assumed it would be m.insert(k, v) === insert(m, k, v)
Yeah, but those extra steps are worth it. I've written a fair bit of typescript and javascript, and I feel like I need to be always careful about nullable values in javascript. Like, I agree that Option is null with extra steps - but those extra steps make it harder for bugs to creep into my code.
In JS its all too easy to accidentally write getNullableVal().doStuff(). Or doStuffWith(nullableVal) and inside the function accidentally forget that the value can be null. In typescript the compiler forces me to consider the nullable case for nullable values, and as a result my code ends up better. And when I pass a nullable value into a method, its clear from the type signature whats going on. I still make bad assumptions sometimes and unwrap when I shouldn't, but when crashes happen they happen closer to the bug in the code.
My favorite syntax for all this is Swift, which understands nullable types at a language level. (Rather than at the type level in rust / haskell). It means the language can coerce X -> Just(X), so making function arguments nullable doesn't break callers. And let myVal: SomeType? / myVal!.foo() syntax seems strictly better than rust's let myVal: Option<SomeType> / myVal.unwrap().foo().
A decent IDE should make the type information available when you need it.
> This seems reasonable to me, but I'm curious what counts as a "program bug", or how to know that some error is recoverable.
I would assume recoverable errors would be explicitly represented in an Either-like style, and if you don't have a better way to handle them you can explicitly turn them into a program bug (exception/panic) by some unwrap()-like operation.
> an unexpected nil is really a type error.
Right, which is why you want "nullable x" to be distinct from "x" in the type system. The problem isn't the NPE per se, it's silently allowing the program to continue in an invalid state until the error happens far away from the problem that caused it, and that's just as much of a problem in Clojure.
And so either you create a special ad-hoc thing in your type system with a bunch of syntax and special cases in the language standard that misbehave when your users do something you didn't anticipate, or you can write a normal datatype to represent "nullable x" and everything will just work, and then maybe put some lightweight syntax sugar on top if you really want to.
A decent programming language shouldn't need an IDE to understand the code. What happens when you post a snippet online ? Or when you look at a diff in github ?
In any language you have to make a tradeoff between concision and explicitness. With type inference you could write code whose text is just as explicit as you'd do in an untyped language, and use the types solely to provide more detail. Or you could put no more detail than in an untyped language, but move some of that detail into types and make your code more concise. Most people find a point somewhere in the middle, and end up with code that's both more concise and more explicit (when you look at the types); of course that means the code is more "magic" if you're looking solely at the text.
There is definitely a balance to be found, but I believe most languages today (and, unfortunately, programmers) rely too much on an IDE even though there's a large enough part of our work happening outside of it.
Reviewing PRs is important, but you'd normally do it from a development machine with your normal tooling available; I don't think it's too big an imposition to say you should do it with an IDE, or else accept that you will have slightly less context available. (In some ways it's good to review that way, as it means you evaluate whether the code is still clear even with less information available).
> Same goes for sharing snippets with colleagues to explain/understand ehat is happening.
I hardly ever see that happen; it's usually much better to create a branch or worksheet.
> There is definitely a balance to be found, but I believe most languages today (and, unfortunately, programmers) rely too much on an IDE even though there's a large enough part of our work happening outside of it.
IMO it's just the opposite; there's a lot of good things we can do with the GUI tools that we realistically ~always have available. Programmers are rightly sceptical of "visual programming" tools that break VCS history, but a language that has an understandable textual representation enhanced by the GUI gets you the best of both worlds.
It is when you don't use it right. Although, you have the benefit of being explicit about it. Still, you're supposed to make your Option/Maybe type a monad and functor, so you can easily chain Option/Maybe-returning functions and transformations while hiding all the boilerplate null checks.
Better to concentrate on the plain "A" in "Option[A]" I think. WHen you don't have nulls, Option is what allows you to have plain types that don't use Option, that can't be "missing", which is enforced by the type system, and so doesn't need any "steps" at all to handle them as regards presence/absence of value.
I'm wondering how this is achieved given the type system and garbage collector. What types are unboxed and when?
(I am one of the authors of Flix).
Honestly, just for the language I'd pick Flix over Scala.
(I am one of the authors of Flix)
FP and JVM doesn't mean Scala.
That's not a bad thing at all.
Regardless, their principles [1] make the trade-offs clear:
- Developer productivity over runtime performance
- Correctness over performance
It seems that the situation is not as clear cut as your post suggests.
(I am one of the authors of Flix)
Yes. GraalVM CE 20.0.0 and native-image.
> …because they are short-lived.
https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
Because "general performance" is ill-defined.
Because tiny tiny programs aren't like other programs.
There are lots of different goals and tradeoffs...
Warm up takes a while but then it can be very fast. For long running servers this works quite well.