652 karma · joined February 6, 2019
I think one of the main reasons that Haskell failed to solve this is actually the same reason that many lower level languages failed: it places too many requirements on data representation. In the case of C/C++ and even (to a lesser extent) modern languages like Rust and Swift, this is because they make promises about representation to the programmer that allow you to circumvent aspects of the language and still write correct code: be it transmutation, casting, field offsets, etc. In the case of Haskell, lists have an entirely arbitrary length and the language makes no effort to constrain this requirement in the type system, meaning that the compiler can only speculatively optimise a list into an unboxed array. In a language with dependent types, it should be possible to constrain the size of a list with the type system, allowing the optimiser to do its job without need for speculative whole-program analysis.
The other reason Haskell doesn't quite succeed is monomorphisation (or lack thereof). Haskell's support for first-class higher-ranked types means that the language can't feasibly make promises about monomorphisation, and as a result it needs to revert to boxing and dynamic dispatch far more than it really should. Conversely, Tao is designed to monomorphise in all cases from the start.
Rust demonstrates that functional programming (and in particular, programming with higher-order functions) is more than possible to optimise very well, and it does this by promising monomorphisation through the type system, allowing the compiler to aggressively perform static dispatch and inlining.
From what I've seen, GHC also fails to pick a lot of low-hanging fruit. The last time I checked (perhaps this has since changed) GHC often struggles with things like TCO and inlining in relatively simple cases as a byproduct of its design (all functions are dynamically dispatched by default, with inlining being a speculative optimisation).
I need to do a little more writing about exactly what ideas I have for Tao, but other languages of similar ilk demonstrate that Haskell is very far from the pinnacle of what is possible (for example, Koka's Perceus reference reuse: https://koka-lang.github.io/koka/doc/book.html#why-perceus).
For example, the typeclass `Eq` describes how values of a type might be compared for equality.
class Eq =
=> eq : Self -> Self -> Bool
The typeclass requires that types conforming to it provide a function, `eq`, that takes two instances of itself and returns a bool (indicating whether the instances are equal). And member of this typeclass might look like: for A, B member (A, B) of Eq where
A < Eq,
B < Eq,
=
=> eq = fn (a0, b0), (b0, b1) => a0 == a1 && b0 == b1
You can read this as "for any two types, A and B, a tuple of both can also be compared for equality provided both A and B can themselves be compared for equality"Typeclasses become useful because they allow you to constrain generic type parameters. For example, we can now write a function that determines whether a list contains a matching value:
fn contains A : A -> [A] -> Bool where
A < Eq,
=
| _, [] => False
\ k, [x .. xs] => x == k || xs->contains(k)
All of this is checked at compile-time by the compiler, ensuring that the function can only be called with types that can be compared for equality.It echoes how I feel when I write programs in languages with strong static type systems: that of the compiler almost fatalistically leading me towards a solution that naturally fits the problem space. A metaphor I like to use is that of an artist knocking away pieces of marble to reveal the statue that was always there beneath.
And, yes, it's great fun to work on! Everybody should give writing a compiler and designing a language a try!
In Tao, natural numbers don't support subtraction but you can decrement them by pattern-matching as in this example, allowing the compiler to 'check your workings' and statically prove that your program can never result in underflows.
In a similar vein, you can't index directly into a list (because indexing is a fallible operation). Instead, you must pattern-match on the list and explicitly handle the empty case (you can also write a utility function to do this for you like so)
fn nth : Nat -> [A] -> Maybe A =
| _, [] => None
| 0, [x ..] => Just x
\ n + 1, [.. tail] => tail->nth(n)
Used like: [1, 2, 3]->nth(1) # Evaluates to `Some 2`I don't want to put emphasis on "pushing the limits" because I'm still very new to language design and mostly self-taught. There are bigger and better languages pushing the envelope further than Tao!
That said, I've been experimenting with:
- Expressing totality and type inhabitance in the type system
- Effect systems (including user-defined effects, effect handling, parametric effects, lowering to monadic IO, etc.)
- Typeclass inference via typeclass variables (Tao can handle several cases that are ambiguous even in Rust, such as the following: https://github.com/zesterer/tao/blob/master/examples/debug.t...)
Because this is purely a personal project, I'm thankfully not constrained by such mundane concerns as "ergonomics" (unless such concerns relate to me while I'm working with it). I think Pythonic syntax is probably the way to go long-term though...
The language does support limited sugar for tuples without parentheses in some specific cases, such as `match` and `let`:
let x, y = 5, 4 in
...
match x, y in
| 4, 7 => None
\ _, y => Just y
However, these cases only incidentally lower to tuple pattern matching, and deliberately hide the fact that tuples are used internally.data Nat = Zero | Succ Nat
You could pattern match on this Nat, fetching the inner Nat, allowing the type system to prove that all cases are handled exhaustively. Arithmetic patterns just extend this notion to the built-in Nat type.
Full dependent typing is something I'd like to experiment with in time, but I'm not there yet.
I don't know whether I'll keep this syntax though. I'm increasingly wondering whether it's better to just bite the bullet and go all in with indentation sensitivity.
That said, the existing \ syntax can be quite nice to read: https://github.com/zesterer/tao/blob/master/lib/parse.tao#L5...
As mentioned in the README, I don't see Tao as a production-quality language (at least, for the foreseeable future). I'll leave that to the experts. Instead, I'm more interested in exploring the limits of new language design ideas (effect systems in particular). There are already a few interesting things Tao does that I've not seen elsewhere.
TL;DR: The best approach, in my view, is to detect whitespace in the lexer and emit fake delimiter tokens for the parser so that the parser-level syntax is still context-free.
- Ease of use + ergonomics
- Backtracking 'by default'
- High quality errors and error recovery
Nom is very specifically designed for parsing input not intended to be read by humans, and that focus is evident in the things is prioritises.