Ante: A low-level functional language
antelang.org
antelang.org
(1) Zero-cost Effect Handlers by Staging: http://ps.informatik.uni-tuebingen.de/publications/schuster1...
The dot product example gave me pause because map2 seems to be the same as zipWith. Does that exist in Ante? Without context I might have thought map2 was going to act as bimap. Take that for what you think it's worth :)
Also I might be having a brain fart -- but isn't the dot product in your example equal to 32?
And yes, the dot product should be 32, thank you :)
As you have the normal map function for Functors (using Haskell):
> :t fmap
fmap :: Functor f => (a -> b) -> f a -> f b
you can have bimap for Bifunctors: > :t bimap
bimap :: Bifunctor p => (a -> b) -> (c -> d) -> p a c -> p b d
which specialised to pairs is: > :t bimap @(,)
bimap @(,) :: (a -> b) -> (c -> d) -> (a, c) -> (b, d)Joking aside, looks cool, good luck with the project!
Joking aside, looks cool, good luck with the project!
Something like Caml Light - precursor to Ocaml - but with translation to C instead of bytecode, so it will be fast and will have comfortable integration with C libraries.
I like the lack of GC! What is the method for always incremental compilation? The benefits are obvious, but isn't it problematic to have two authoritative representations of the same source? It would be fantastic if you get that to work well!
C is great, and it would be cool to see something that is similarly close to the instruction set but has functions as values
Of course there are many programming patterns that are in more acceptable functional programming languages than C. Whether a programming language is considered functional is not the same as which patterns are supported in the language.
For the record, there is not one accepted definition, but we can get close by saying that FP languages are those based on lambda calculus as their semantics core. And the primary mechanism in lambda calculus is variable capture (as done in closures).
C is based on the Von-Neumann model and has absolutely nothing to do with the lambda calculus. No reasonable PL expert considers it functional.
[0] https://www.semanticscholar.org/paper/Functional-C-Hartel-Mu...
[1] https://ris.utwente.nl/ws/portalfiles/portal/5128727/book.pd...
(edit) add download link and background.
It's a good representation of the PDP-11 and similar-era computers. It's also a good abstraction for modern microcontrollers.
C's view of the world is also a really poor fit for today's larger CPUs. Your computer has to jump through a ton of hoops to make itself seem C-like. C has no concept of vectorization, speculative execution, branch prediction, multiple cores, caches, MMUs, etc.
For basically any desktop/laptop/smartphone CPU, the C runtime is more like a little VM than an accurate model of the hardware underneath.
OK, but vectorisation is exposed by the instruction set, as instructions[1]
> speculative execution, branch prediction, ... caches, MMUs
How would you expose speculative execution & branch prediction? Cache behaviour is well understood so what's wrong with it? What would you do to expose MMUs to the programmer, and what would they do with it/how would it help?
[1] but aren't there libraries that expose it just fine in a portable way?
But they are thoroughly ingrained into the technology, and will not easily be rooted out.
The more expressive your language is, the less the runtime and hardware needs to guess about.
This is a very deep and mature assessment. I have high expectations for the future of Ante. Higher than Rust, in particular, provided it supports some analog of destructors.
Even though I’ll never release it, it was a rewarding theoretical and practical exercise.
As an aside, I really love how detailed and example-ful Ante’s site is. It looks like a ‘fun’ language and I hope the creator keeps working on it.
Really, perhaps your language is The Next One.
Then there are people who will criticize just about anything for the sake of doing it. Just ignore those people and make a day more interesting for the people who share your passions.
I would love to imagine I could be the next Andrew Kelly (creator of Zig), I don’t know that I can actually be a language founder.
Nevertheless I think there is space for experimental/research languages. Just don't burden yourself thinking in terms of "this is the next great thing" and it will be fun, and perhaps your work and the ideas underlying it will even leave a mark, even if that doesn't necessarily mean that the particular incarnation of that particular language will ever reach adoption.
We not only stand on the shoulder of giants. We all stand on the shoulders of billions of dwarfs.
You will only know if you try. Things that are worth it take courage me thinks.
> I'd really like to find "the low-level functional language" without "the fun stuff"
But current stable Ocaml has neither of the "fun stuff" mentioned and compiles to native code. So isn't that exactly what you want?
It doesn't even need lifetime analysis because automatic garbage collection be praised.
And algebraic effects are awesome. Sure they are not mainstream yet but conceptional there is a strong parallel to the Common Lisp condition system which is quite established. Not sure why you wouldn't want to have them. Also it is still a long way until we will see them used in user facing stuff in Ocaml.
I'm not Ocaml implementation expert, but I suppose Ocaml exception handling and garbage collector could be tricky to aware about when one extend or embed Ocaml.
To be honestly, my fellow did Ocaml embedding once. He made able to load natively-compiled plugins written in Ocaml into C based software. And it worked. I didn't dig into details, though. There was at least one bug regarding garbage collection (and it happens that I fixed it).
The only gain from emiting C code is portability to weird architecture that would be covered by C compilers but not the OCaml one; which is arguably a pretty niche use-case.
An ideal solution to me needs to solve these problems. Since there is already a large body of research trying to address this on the static side and failing, I believe it needs to be solved with runtime checks. The specifics of which I'm still exploring but its worth mentioning these would only be necessary to tighten existing lifetimes so one can envision annotations or compiler options to elide these if desired. Lifetime inference in MLKit (and I believe ante as well) tends to speed things up by turning more dynamic allocations into stack allocations, so there is some room there for runtime checks without making the result more expensive than the version with dynamic allocation I believe.
Seriously, this looks promising and I'm very interested to see where it goes.
These days I program in Rust and find Rust's approach to explicit regions to be a workable compromise, though reference-heavy types can get pretty ugly and hard to work with (and I'm pretty sure that variance of lifetimes is confusing to everybody who isn't a PLT theorist and some who are).
The approach I personally find most interesting is the Lobster language. There (and I'm probably oversimplifying) the semantics are reference counting, but you so analysis to remove a huge fraction of RC operations. I believe the Perceus work is similar.
I'm happy to chat anytime. Recent work has been using somewhat exotic types provided by Rust (associated types, existentials, lots of inference through product types) to represent UI. So far I've basically been using what Rust gives me, but it's interesting to imagine what changes to the language/type system might buy you. For example, there are a few places in the code where there are downcasts, but I suspect that with a sufficiently strong type system you could prove those downcasts are infallible.
:eyeroll:
>However, without a license, the default copyright laws apply, meaning that you retain all rights to your source code and no one may reproduce, distribute, or create derivative works from your work.
I'd love to start again with a new, small language. I love Haskell, but it's decades old, and one struggles to work with it without being torn apart by the gravitational tides of the black hole at the center of its galaxy. All jokes aside, Haskell really does reward a PhD in category theory, or the self-taught equivalent later.
Functional languages make parallelism easier, and Haskell has put particular effort into this: Adding a dozen lines to a program can keep the 16 performance cores of my Mac Studio running full tilt. I have yet to read any account of how someone else's favorite language makes parallelism easy, that is aware of the comparison with Haskell. If I thought there was a reasonable alternative, I'd jump at trying it.
Rust appeals because it replaces GC with modern control of allocation lifetimes. I love how Ante will also explore this. I use cyclic data structures; how one handles this functionally, and how one handles this using reference counting, is exactly the same problem.
Parallelism should be the first consideration, designing any new language in 2022. Parallelism is the reason I keep using Haskell, despite newer alternatives. Any language that runs on a single core is a toy. This is a plea!
I've found Clojure to dominate the ease of parallelism. For problems that are actually stupidly parallel the number of changes to your code is often measured in letters rather than lines. For example, you might change map to pmap or you might change reduce to reducers/reduce and change nothing else but now be fully leveraging all your cores. For problems that aren't stupidly parallel I feel like Clojure shines even more. Most languages didn't implement software transactional memory and encourage it as the default way to work with things that vary over time, but Clojure did. On account of that you can have a not massively parallel algorithm that would be full of tricky lock code in another language and still end up leveraging all your cores by doing something as simple as map or pmap, but not run into horrible issues.
On mobile horizontal scrolling through the code snippets will trigger switching to the next snippet on my phone.
If your standard function call convention is just `f x`, but you also support precedence operators, then `f (x)` automatically becomes possible.
It reminds me of an old Lisp joke, though:
f x -- too mathematical!
(f x) -- too many parenthesis!
f(x) -- just right!:) yet we are very happy to use the same syntax in shell scripts or the shell itself.
f $(g x)
To pass the output of g x to f. grep "hello ($(cat patterns.txt| tr '\n' '|'|grep ')$^)" <(ssh other.host "cat ~/file.txt") |tee >(grep 'a' >As.txt) >(grep 'b' > Bs.txt)
[yes, gratuitous use of cat, sue me]In shell, f x y z, x y z are all augments to f. Doesn't matter if f takes one argument, all are passed to the function.
With many functional languages this gets very confusing. IE what ie what does `f f x` do? In shell I know for sure.
In the example f f x, it might be easy to parse. But in f x y z, any of x y z might be functions.
In Ruby however it is a bit more ugly
def f x
x + 1
end
puts f f 1
> 3OCaml parses a b c as ((a b) c). In case the compiler can determine that a is a function taking 2 arguments, it will optimise the code, so that it’s effectively a(b, c). But in general, that’s not possible, especially in the case where the compiler determines that a is a function with a single argument (in which case, it’s return value must be another function, which is in turn called with c) or when a is a first-class function (e.g. passed as an argument)
xargs -0 -n1 bash -c 'mv $1 ${1//.js/.ts}' --
Everything to the right of xargs is a string argument passed to xargs.
and the nice thing about (f x) is that the parenthesis group f with x; so you have the whole call inside the (). Consistent and simple to understand.
vs i.e. print(f"a string"), where it isn't even clear that the "f" is a function call.
Hence the joke. It's one of those jokes that earns a loud sigh from me, rather than a chuckle.
That's the joke
A lisp paren can do both jobs: expression and scopes.
Using different symbols for different purposes makes sense, it helps humans to parse correctly faster.
Mainly, you don't look at the parenthesis when reading; you look at that let and your eyes rely on indentation for structure.
Also, the ending paren doesn't have anything to help you see what it is on its own.
)))) is like a ground symbol in a schematic:
(+5V
(+10V
(-10V
(INPUT3 ...))))
----- local "ground" for all the above
(different circuit)As a side note, I believe that different people fundamentally have different programming languages that objectively suit them best, due to differences in their respective psychology and way of thinking and perceiving. It’s interesting to discuss trade-offs, but in the end there is no single truth about which language is better overall — it depends on the task and on the person. There’s no “one size fits all”. What’s valuable is to understand why a certain syntax or language might work better for certain people.
( can mean: function call, part of an expression to be evaluated first, regex capture group
{ can mean: scope [of ... loop, if, lambda, function etc.], object definition, string interpolation code i.e. ${..}, class definition
There are probably other things I am not thinking of.
The one that trips me in JS is code like x.map(v => {...v, v2}) breaks because the compiler sees the { as the beginning of a scope, not the object definition I intended.
The working solution is x.map(v => ({...v, v2}))
But x.map(v => v+1) is allowed.
I don't think the compiler could figure out what you meant because an object definition might be a lambda function body. For example { myvar } can be both an object definition, and a function that returns myvar.
Java, one pair of parens:
int x = 1;
int y = x + 1;
System.out.println(y);
Clojure, six pairs of brackets: (let [x 1 y (+ x 1)] ((. (. System out) println) y))The drawback is that they are put on the same level, whereas in most people’s minds the function is a fundamentally different thing from the argument(s). The “f(x)” syntax reflects that asymmetry.
Of course, theoretically you could also view the function as a parameter of the computation and/or the arguments as specifying an operation (in particular if those are also functions), but for most concrete function invocation that's not generally the mental model. E.g. in "sin(x)" one usually has in mind to compute the sine, and x is the input for that computation. One doesn't think "I want to do x, and `sin` is the input of that operation I want to do". One also doesn't think "I want to do computation, and `sin` and `x` are inputs for that computation". It's why you may have mentally a sine graph ranging over different x values, but you don't imagine an x graph ranging over different functions you could apply to x.
Incidentally, that’s also why we use syntax highlighting. One could, of course, use syntax highlighting instead of symbols to indicate the difference between function and arguments (between operation and operands), but that would interfere with the use of syntax highlighting for token categories (e.g. for literals of different types).
The negative reactions that a lot of people have toward Lisp's parentheses are not because of the call function syntax but because parentheses are used everywhere else in Lisp's syntax.
x fFor the same reason you'd make 1 + 2 the same as 1 + (2). Parentheses can be used to group arbitrary subexpressions.
You can design a language which uses `e1 e2` to represent any binary operation you like, but I'd argue that function application is more common than string concatenation, so it's more deserving of that syntax. Plus, it plays nicely with currying.
f g = composition of f then g
x.f = f applied to x
x.f g h = in regular notation h(g(f(x))
Though slightly different precedence rules may be preferable.I think the k in awk considered juxtaposition-as-string-concatenation to have been a mistake by the way.
Some other reasonable choices may be:
- disallowed syntax
- multiplication (which, for matrices, is a special case of function composition and application)
- inner join which can be seen a bit like function composition but for relations instead of functions
- sequencing (ie instead of ‘;’)
If functions are curried, then I suppose the syntax for `f x y` would be `y.(x.f)`, which maybe you could write as `y.x.f` if the associativity worked as such. But that means you have to provide your arguments in reverse order?
If functions are not curried, do you write `(x, y).f`?
1 and (1) are isomorphic. A single term tuple can be converted to the single term, and vice versa. Having an implicit conversion doesn't seem too crazy.
The biggest issue I suspect would be confusion about the most idiomatic way, or a mix of styles in real-world code bases, that causes confusion or inconsistencies (increases cognitive load for the reader).
(1) - the number 1
1 - the number 1
(1,) - a tuple with one item, which is the number 1
1, - a tuple with one item, which is the number 1
Where brackets are used in some languages to construct tuples, you generally need special syntax to represent the case for 1-tuples, like Python where "(1)" is equivalent to "1" but "(1,)" is a 1-tuple containing a single 1 value.
Also in most FP semantics, x and the 1-tuple containing x are not equivalent so the mathematical isomorphism doesn't hold. The tuple itself could be undefined (bottom/unterminating or null, if the language has such a concept), or could contain an undefined value or could be completely well-defined. These three cases are not represented in the semantics of the unbundled value.
A good number of "functional programmers" only have experience with JavaScript these days.
handle f ()
| flip () -> (resume true + resume false) / 2.0
Does `handle f ()` call `calculation` and the `| ...` part "injects" the `flip` effect?
I am also quite confused by the part following `| flip ()`. It somehow returns true or false with a probability of 50%? And why does this give you the expected value of the whole calculation function in the end?This expression matches on the effect flip() and handles it with the code on the right hand side of -> which becomes the value of the function invoking the effect. In this case the handler runs the continuation for both possible values (resume true and resume false, i.e. the function was called once, but it will return twice, with true and false substituted in the place of flip() in the place which used this effect) and returns their mean as the average.
I.e. each time calculation calls flip(), the effect handler will continue the function for both true and false and then take the mean of those two results. Comments explain that flip() should simulate a coin flip, which generally gives equal probability (1/2) to true and false. Each flip() in calculation was simulated with equal number of true and false values, so it fits the expected distribution. Each time taking the mean of those values is just an expansion of the integral calculating the expected value, as per definition of expected value.
Note that there is some recursion here. While the flip() handler is executing for the first invokation of flip(), another instance of the handler will execute for second and third invokations (inside resume true). I.e. it calculates the expected value by taking the mean of the expected values of each branch created by a flip. When a branch doesn't have any more flips inside, its expected value is taken to be the constant value that this branch returns.
the `| flip ()` is pattern matching on that effect term, I believe. So essentially: when you see `flip ()` inside anything in the argument of `expected_value`, capture the continuation there, run that argument once with `true` and once with `false`, add the results, divide by two.
This way of computing expected value works because for each flip we essentially have a case-split: the value can be true or false with a 50-50 chance, so the expected value of the whole thing is is 0.5 * the result of the true branch + 0.5 * the result of the false branch!
This is more of a fun use of effects than a practical one. If you're still curious about effects, check out the full page on them here: https://antelang.org/docs/language/#algebraic-effects which includes some actually useful effects like State, Generators, looping constructs, parsers, etc.
That said, I'm a bit skeptical of the utility of refinement types (and dependent types in general). They greatly increase a language's complexity (by obscuring the type/value distinction and making type inference and checking more complicated). I'm not sure the benefits are worth it, particularly because of the so-called "specification problem."
Your concern on refinement types is definitely valid and is something I've been thinking about. They are certainly useful in some cases like array indices or passing around certain predicates but whether these are useful enough to offset the implementation cost and brain tax is an open question.
We've got Rust, which is essentially a C-style lang that steals all kinds of goodies from the ML family; it's nice to see Ante as a kind of inverse to this: i.e. an ML style lang that borrows some of the nice bits from traditional imperative langs (and hopefully maintains their performance characteristics).
Looking through the language tour there already seem to be a plethora of very sensible/ergonomic design decisions here. The 'loop' and 'recur' keywords are a great feature, making a nice little functional nod towards while loops. As a long-time Haskell user the explicit currying initially turned me off, but after seeing a couple examples I can see how it's actually a very reasonable solution, and moreover the ability to curry out of order is really nice instead of having to use 'flip' or similar combinators (as a side note, the explicit currying reminds me a bit of APL's α and ω arguments in dfns, a feature I'd love to see pop up more). The paired tuples also seem like they'd be a pleasure to use; certainly a bit more flexible than tuples in other ML style langs. Making '.' the pipeline operator is also a smart bit of syntax, and I can see it being very accessible to OO programmers in that it looks (and acts) like the method chaining they're familiar with. Refinement Types seem like a good alternative to full on dependent typing (ATS has an analogous (and more general) proof system for ensuring things like array indices are valid (an essential feature in a low level FP lang), but it involves threading those proofs through your program which seems much more clunky than what's presented here).
Overall I'm really excited to see where Ante goes, it seems like a very pragmatic functional language, something the world definitely needs more of...
[1]: http://www.ats-lang.org/Home.html
[2]: https://futhark-lang.org/
EDIT: In regards to low level FP langs there's also Carp, Erik Svedäng's nifty little statically typed Lisp. It's like Scheme with a Rust-y memory model:
I'll avoid posting it here but if you do want to follow ante's development the best place is on its discord which is linked on the github page.
Might be my 2 cents, but i think Rust can hit a very sweet spot for functionally-leaning low-level effective programming.
I'm not sure I agree. "Straightforward" FP, e.g. a bunch of functions defining local vars and calling other functions, can be pretty hard to make low-level (it pretty much assumes a GC, and indirected closures; although tech like linear types can help).
However, the more abstract, point-free, pattern-heavy FP seems like a reasonable fit (e.g. pipes/conduit)
What am I missing?
At a slightly higher level, if memory is explicit, and that would include input and output buffers and the like, then it is possible to make functions exist inside a ‘memory monad’, similar to Haskell’s state monad. Then any and all operations involving the memory used can be made referentially transparent.
Now, the real question is would anyone want to program in such a style? I know I wouldn’t mind, it’s while I broke down and designed a personal programming language for my daily use. But it’s a matter of taste and tolerance.
For example, ATS is ostensibly an ML-like language; but its "low level" examples look more like "C with stricter types" (e.g. its Cairo examples)
That is how Lisp Machines and Xerox PARC workstation OSes used to be built, or Mirage OS for a more recent example.
And I do consider Lisp functional, since when I reached university, the options were Lisp dialects, Mirada was fresh, Standard ML and Caml Light were rather new.
There was yet to appear the mentality that OCaml / Haskell === FP.
Generally I think FP has a bit of a misguided bad rep when it comes to performance. What makes code slow is indirection that a compiler cannot reason about and work that doesn't need to be done. There are FP techniques that can help a compiler or CPU to generate/transform efficient code and ones that can confuse them so to speak. Needless mutation can sometimes lead to bad code as well (from a compiler or CPU perspective), because it solidifies control. Functional programming is fundamentally a constraint, computers, interpreters and so on like constraints, because they can then go about shuffling around stuff without breaking those constraints if that makes sense.
[1] https://en.wikipedia.org/wiki/Tim_Sweeney_(game_developer)
I'd like to remove iterators in favor of Generators (implemented with Algebraic effects) in the future though since they're much easier to define. Actually switching over is on hold until effects are implemented and speed tests are done to ensure they're compareable speed to iterators (which they should hopefully be due to monomorphisation of effects + handlers).
These are all checked at compile-time though, since interpolation can only be done inside string literals we can check for all uses of ${expr} inside literals and ensure e.g. that expr is convertable to a string. Since these are only in string literals, all placeholders are known in advance so there are none that cannot be checked at compile-time. (A string like "foo \${" ++ "foo}" must both escape the interpolation begin ${ and is concatenated to the actual string "foo ${foo}" with no interpolation. Otherwise it would be very confusing having interpolation happening in unintended circumstances (and would make string operations less efficient by having to check for this)).
I don't think limiting string interpolation to enhanced string literals in code (leaving out generic strings) in order to allow static checking is a practically acceptable restriction. For example, logging frameworks meant for long-running application tend to specify message templates in configuration files or databases, possibly hot-reloading them at runtime.
I'd like to keep this orthogonal to IO or other specific use cases so that it is applicable to any use case users may have rather than tailored to existing ones. You're also correct there is no real logging framework or anything of the kind except print really. Ante is still in a quite early state and there aren't really any substantial libraries to speak of. As-is, interpolation is really just some nice sugar to make some common operations more ergonomic, like f-strings in python.
I think applications that need their own dynamic interpolation requirements should define their own interpolation to handle their specific needs. It would be unacceptable from a performance and predictability standpoint for example to support the example of loading files into a string and have them interpolated at runtime automatically for all file loads.
> In general, ante is low-level (no GC, values aren't boxed by default) while also trying to be as readable as possible by encouraging high-level approaches that can be optimized with low-level details later on.
IMO the only low level language is assembly. Everything else is some form of abstraction. C/C++ and the likes I tend to call lower, since in 2022 it is closer to the hardware, and then sugar languages like python, c#, js, and the likes I call high level.
Consider what happens when you implement a x86 emulator in python: you're using a high level language to implement a machine using a particular substrate (a simulation inside another machine). This simulated x86 CPU executed machine code and you'd call that machine code to be the "native" or "lowest level" language with respect to that particular machine.
You can see how that choice of machine language bears no relationship with the language used to implement the underlying machine.
I did do a quick ^f for "destructor" and "drop"...
The intro mentions memory management, and inferring lifetimes. How do I make something happen at end of lifetime?
Function arguments are passed by value currently, though I may explore pass by move and other options in the future.
I hope for ante to be usable without a heap, though the `ref` type automatically handling lifetimes makes this more difficult. These refs compile to an equivalent of destination-passing in C, but can require dynamic allocation if a function creates a ref and another function calls the first in a loop. I also plan on having an Allocate effect for whenever a function can allocate, so that it is trivial to handle this with your own handle that can do anything. This would be an easier alternative to zig's approach of "pass the allocator everywhere manually," but there are still things I need to iron out, like how it interacts with the lifetime inference issues above, so its still in the design phase.
????????
A few questions (hopefully the author still reads it):
* Any plan for support of arrow/monad comprehensions?
* Semi-related: When it comes to generators it might be worth to consider making them clonable (see https://github.com/pelotom/burrido)
(2): Thanks for the resource, I'll look it over :). I remember seeing a vaguely related note that multicore OCaml used to provide a `Obj.clone_continuation` function but no longer does. Of course, ante's algebraic effects design is rather different so that may not apply anyway. It may depend on the Generator whether it is cloneable or not. Some generators are just functions that can be arbitrarily cloned, but others are closures which may have restrictions in whether their environment is cloneable. Ante's clone story in general needs more work.
The "7 programming languages in 7 weeks" doesn't seem to be enough. Could anyone recommend PL (1-3) books that nail all of these concepts?
You don't touch on it, but there are some more difficult cases with lifetime inference as well. Namely branching the compiler must decide whether or not to extend a refs lifetime. This and a lack of granularity in container types are known problems with region inference (they lead to more memory than necessary being used by assuming the longest lifetimes), and are things I hope to tackle.
The other examples are way too confusing for me, except the first one.
As for the examples, they definitely showcase things that aren't present in most languages and can thus be confusing. That is on purpose though, since ante for me is a language to experiment with novel features I find interesting. The second example on lifetime inference for example can be thought of as like rust's lifetimes but completely inferred and instead of issuing errors for "x does not live long enough" it will instead automatically extend the lifetime of x. So my hope is it is easier to use at the loss of some control (control can be regained by using other pointer types like `Ptr a` for a raw pointer used to implement other things on top of).
Not a huge fan of the whitespace delineated syntax though. I like me some curly braces.
Other than that though, it is super interesting!
Generally, the entire site has a really nice design, at least on desktop. One of the prettiest I've seen.
Also, black text on white background is easier to read.
And don't make your web design to complex for Firefox reader mode to work: if you do, that's a sign you're doing it wrong.
The mechanism should be made clear in the introduction as browsing the documentation doesn't make it clear to me.
The specific design is still in question though. For one, an effect like Allocate will be almost ubiquitous throughout each function so care is needed to not clog up signatures too much. There's a few potential solutions here if you're interested. From including Allocate within the row-type of a larger effect like `IO = can Allocate, Read, Write, ...` to encouraging effect inference on functions versus manual annotation.
The author said somewhere else in the thread they want to switch to generators implemented with effects eventually.
However for things like "sorting" this can never fully be achieved. You cannot sort a generator. Every iterator counterpart needs a list counterpart or there will be literally certain computations that are impossible.
I chose it mainly because I like the style and I haven't found it to be an issue in practice yet, especially with flexible rules for line continuations. The biggest detriment to me is the lack of auto-formatters for indentation.
Edit: I'll add to this a point I haven't seen discussed before about functional languages using the `f a b` syntax specifically. Without significant whitespace you cannot really have semicolon ellision with this syntax since almost any line may be interpreted as a function application of the next line. E.g. `a = b + c` and `foo 32` as subsequent lines would be interpreted as `a = b + (c foo 32)`, hence why ocaml requires semicolons. For some people semicolons are fine, but they're just noise to me :)
There's more detail on the specific significant whitespace scheme used on the website here: https://antelang.org/docs/language/#significant-whitespace
Guido van Rossum basically said parsing them is hard, and he doesn't like lambdas in the first place, so it won't happen. [0]
Similar to "tail call optimization encourages dirty FP nerds" logic. Terrible decisions IMHO.
[0]: https://www.artima.com/weblogs/viewpost.jsp?thread=147358