top | item 31775216

Ante: A low-level functional language

446 points| cheesestain | 3 years ago |antelang.org

217 comments

order
[+] jfecher|3 years ago|reply
Hello, author here! As the website says, the compiler itself is still in a very early state where basic things like functions, types, traits, inference, monomorphisation, and codegen are implemented. The fun stuff of algebraic effects, lifetime inference, and refinement types are not however. Though I can elaborate on implementation strategies of these for anyone curious. For example, algebraic effects in existing languages can be quite slow at runtime due to the use of continuations, the translation to a monad stack, and/or dynamically finding handlers at runtime. It is my plan to monomorphise them away and inline all these cases as in the following paper (1). With this, handlers are inlined into functions, continuations are normal closures, and continuations that aren't called multiple times are inlined.

(1) Zero-cost Effect Handlers by Staging: http://ps.informatik.uni-tuebingen.de/publications/schuster1...

[+] funny_falcon|3 years ago|reply
I'd really like to find "the low-level functional language" without "the fun stuff". Or at least it should have simple and boring subset that is usable without "the fun stuff".

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.

[+] charleskinbote|3 years ago|reply
Thanks for sharing!

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?

[+] b3morales|3 years ago|reply
Looks like a great start! Your attention to programmer ergonomics is admirable. Added to my weekend hacking reading list.
[+] rayiner|3 years ago|reply
Very neat project! I noticed that Ante doesn’t have explicit region type declarations. As I recall, existing algorithms implemented for ML can sometimes infer very large regions which causes memory usage to balloon. It looks like smart pointers are part of the plan to address that possibility, but I’d love to hear more about your thoughts on memory management.
[+] vanderZwan|3 years ago|reply
As a person named Job I'm a little confused by the second example on the website, but that's probably my own interpretation bias :p

Joking aside, looks cool, good luck with the project!

[+] PowerBallZed|3 years ago|reply
I don't see a LICENSE file anywhere in the repo or the website. Nobody is allowed to use Ante?
[+] vanderZwan|3 years ago|reply
As a person named Job I'm a littly confused by the second example on the website, but that's probably my own interpretation bias :p

Joking aside, looks cool, good luck with the project!

[+] ObiWanFrijoles|3 years ago|reply
This is a really awesome project and I hope it goes far!
[+] Syzygies|3 years ago|reply
I avidly follow descendants of Scheme and Haskell (I figured out for example how to build Idris native on an M1 Mac), and I plan to follow Ante. I just spent $5k of lunch money on an M1 Ultra Mac Studio, to use as a math compute server. How do I keep it busy? Haskell. My "Hello world" parallel benchmark, using reverse search to enumerate the 66,960,965,307 atomic lattices on six atoms, took 14 hours in 2009. It now takes four minutes.

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!

[+] JoshCole|3 years ago|reply
Also have a computer with a ridiculous number of cores (32) that I want to keep busy.

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.

[+] cultofmetatron|3 years ago|reply
try elixir! parallelism is child's play on the platform. plus its functional without drowning you in type theory.
[+] Escapado|3 years ago|reply
The syntax looks a little funky to me but it's still quite interesting. Is there a reason why you would make fn(1) and fn 1 equivalent? For me personally it makes readability worse and looks strange when chaining functions like in their last example on their landing page.

On mobile horizontal scrolling through the code snippets will trigger switching to the next snippet on my phone.

[+] tazjin|3 years ago|reply
> Is there a reason why you would make fn(1) and fn 1 equivalent?

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!
[+] eatonphil|3 years ago|reply
Those two function calls are the same thing in OCaml and Standard ML.
[+] curryhoward|3 years ago|reply
> Is there a reason why you would make fn(1) and fn 1 equivalent?

For the same reason you'd make 1 + 2 the same as 1 + (2). Parentheses can be used to group arbitrary subexpressions.

[+] deltaonefour|3 years ago|reply
Haskell does this. Basically languages that are following the ML style have this syntax including Haskell. You must not have experience with this family of languages at it is very common and a huge part of functional programming.

A good number of "functional programmers" only have experience with JavaScript these days.

[+] xico|3 years ago|reply
It seems the parenthesis are only used to indicate the priority, like in Haskell, so you wouldn't really use them for `fn (1)`.
[+] louthy|3 years ago|reply
> Is there a reason why you would make fn(1) and fn 1 equivalent?

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).

[+] dthul|3 years ago|reply
Looks very cool! Can somebody enlighten me what's happening in the Algebraic Effects example? Specifically this part:

    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?
[+] jfecher|3 years ago|reply
Author here, a good way to understand algebraic effects is as "resumeable exceptions." In this case the expected_value handler says to run `f ()` and whenever that computation "throws" a `flip ()` effect to handle it by resuming the computation with the value true returned for flip. The continuation continues as normal, subsequent uses of flip are also handled until the computation finishs. Then we evaluate the rest of `+ resume false) / 2.0` and resume the computation (a second time!) in the same place, this time with the value false. Finally, we add both values returned and divide by two to get the expected value.

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.

[+] GrumpySloth|3 years ago|reply
Explanation based on my familiarity with Koka:

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.

[+] jkarni|3 years ago|reply
I don't fully understand it yet, but I think it resumes twice, adds the results, and divides by two.

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.

[+] jasonhansel|3 years ago|reply
This is cool! I really like the syntax for algebraic effects, which would be great for mocking. I also like the way that "impls" can be passed either explicitly or implicitly; the lack of global coherence seems like a reasonable tradeoff. Have you thought about using existential types to support dynamic dispatch (like Rust's "trait objects")?

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."

[+] myco_logic|3 years ago|reply
This looks really lovely, I look forward to following the maturation of Ante in the future. I've often thought that the niche of a general purpose low-level FP language was a promising space. The only other langs that fit in there are ATS[1], which is notoriously complex/difficult-to-learn, and Futhark[2], which is more GPGPU/scientific-computing specific.

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:

https://github.com/carp-lang/Carp

[+] jfecher|3 years ago|reply
Author here, thank you for your interest! There are definitely a lot of small design decisions that add up in language design, from using `|` for match cases to avoid double-indentation, to the use of pairs over tuples, to how methods are resolved and chained, it is nice to have others appreciate the small things sometimes :).

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.

[+] aconst|3 years ago|reply
Hello, I just wanted to point out that if I had not read comments here and had not looked for a second example linked to "Job", I would not have realized there were dots under the first example. You may want to make them more visible ^^;
[+] DeathArrow|3 years ago|reply
Very interesting stuff! I didn't even thought is possible to implement a low level functional languages, I always thought a functional language requires a ton of abstractions.
[+] coryfklein|3 years ago|reply
Very interesting to see you essentially replace Tuples with Cons! I'll be following that development for sure. Coming from Scala, Tuples are entirely how you represent Product and then that has tons of downstream implications on case classes and constructors. This leads to situations like Scalaz creating NonEmptyList to get Tuple-like behavior out of List. So your approach has the potential to unify these similar but different types and I find that fascinating!
[+] thurn|3 years ago|reply
I'm confused how data structures work in this language, and there's no documentation about it as far as I can tell. Is this going to be a Rust-style vectors+iterators system? It it going to use purely-functional concatenative structures? The first example you show invokes `map` and `sum` over an Array, but what is this code actually doing? Making a copy of the data in the array? Creating a mapping iterator?
[+] HelloNurse|3 years ago|reply
How does string interpolation work? In what context, exactly, are the placeholders checked and/or evaluated? How are missing or incompatible placeholder values handled? The semantics aren't obvious (for instance, how do you deal with string inputs, which would contain placeholders that cannot be checked in advance?).
[+] jfecher|3 years ago|reply
String interpolation works by expanding to the concatenation of several strings. So a string like "the ${foo}." is expanded to "the " ++ foo ++ ".". There are some things I'd like to change about the current design. Namely it should probably defer to a StringBuilder of sorts, and it should possibly do it lazily so that interpolation can be used in log calls without worry of whether logging is enabled.

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)).

[+] markoutso|3 years ago|reply
How is this low level exactly?
[+] shirogane86x|3 years ago|reply
quoting the github readme:

> 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.

[+] maze-le|3 years ago|reply
Pointers and manual memory management apparently.
[+] whoomp12342|3 years ago|reply
low is relative :-D my thought exactly.

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.

[+] roelschroeven|3 years ago|reply
It's not, or only partially. "To try to bridge the gap between high and low level languages, ante adapts a high-level approach by default, maintaining the ability to drop into low-level code when needed." says the website.
[+] ncmncm|3 years ago|reply
This looks like it could become a very interesting language. Unusually for a new language, I spotted zero red flags.

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?

[+] sargun|3 years ago|reply
Is there a reason not to force a linear typing system (at first) -- a la mercury? To simplify the lifetime analysis? And then in later versions that can be relaxed to affine typing, and then subsequently normal lifetimes?
[+] jfecher|3 years ago|reply
Lifetime inference originates from region inference which is actually completely unrelated to linear/affine/uniqueness typing. Uniqueness typing can definitely complement lifetime inference, but isn't really generalizeable to it.
[+] VladimirGolovin|3 years ago|reply
Can someone please explain what is an "uninterpreted function", in the context of Ante's refinement types? The Wikipedia article didn't help much (https://en.wikipedia.org/wiki/Uninterpreted_function).
[+] jfecher|3 years ago|reply
Hi! An uninterpreted function is one where the only thing we can assume about it is that `(x = y) => (f x = f y)`. That is if we give it the same input, we should get the same result out. In the context of refinement types this can be used for, among other things, tagging values with some predicate. For example, if we know `good_index array 3` and `x = 3` then we know `good_index array x`.
[+] ynoxinul|3 years ago|reply
Looks interesting. How are closures implemented? Is it possible to use Ante without heap? Are function arguments passed by value?
[+] djoldman|3 years ago|reply
dotproduct [1, 2, 3] [4, 5, 6] //=> 22

????????

[+] Garlef|3 years ago|reply
Looks great!

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)

[+] jfecher|3 years ago|reply
(1): No support, some monad or early-error-return sugar used to be considered but effects cover most of the usecases of monads and are easier to use so I plan on emphasizing them. As for arrows, I have to say here that I've actually never used them in haskell so I don't know how useful they'd be.

(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.

[+] Existenceblinks|3 years ago|reply
Noob question: how many PL books I have to read to be able to design a programming language like this?

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?