Show HN: Fuse – statically typed functional programming language
Posted by the_unproven 1 day ago
Hi HN! I've been working on the fuse programming language, it's a statically typed purely functional language with higher-kinder types and ad-hoc polymorphism. It compiles to the GRIN whole-program optimizer, producing LLVM-generated native code.
Fuse supports ADTs, Generics, Type Methods, Traits, Pattern matching etc. all in a functional style with no mutations.
I’ve been developing the language for 5 years, with code written in Scala. I’ve started coding the language from the base of System F that was implemented as part of the book: Types and Programming Languages (tapl). And then extending with concepts such as Bidirectional Type Checking with Higher-Rank Polymorphism.
I’ve mainly drawn inspiration from Rust, Haskell, Scala and Python (in terms of syntax). It all started because I wanted a language that has Rust-like concepts such as: ADT, Traits, Impl block syntax, etc. but have the pure functional semantics.
I'd would love feedback on the language design and its general usage.
Comments
Comment by codebje 1 day ago
It looks like a tidy little functional language - a small, easily grasped syntax surface and generally clear semantics. That you've got it to the point that it can compile and run proper programs is a great achievement for a solo dev project!
The string type in the standard library isn't Unicode-aware, might be worth just noting that. Unicode support can be a big undertaking, but considering whether you'll add it later or not might affect your library design now.
I don't really understand why you have an IO monad. The language isn't pure - `.exec()` means any function can perform IO actions no matter its type signature - so what's IO really for?
Do `impl` additions export? What happens when two libraries add the same function name with different signatures (or just bodies!) to a type's `impl` ?
Is currying automatic? It doesn't seem to be, but, eg, the `sum(x: i32, y: i32)` function theoretically could be called as `sum(5)` to create a closure, but this isn't a documented feature if so.
The website's font is using ligatures, not unicode operator symbols - I'd personally find it much clearer to use a non-ligature font to show what's really there, but that's immaterial to the language.
Comment by the_unproven 1 day ago
Yeah GRIN is a great project, it took a lot of debugging and analysis to make it compile 100% especially with monomorphization involved.
I'll look into Unicode support, makes total sense. Didn't scope it in initially. I can fix the site ligatures too, that's a fair remark.
> I don't really understand why you have an IO monad. The language isn't pure - `.exec()` means any function can perform IO actions no matter its type signature - so what's IO really for?
That's a fair point, I still left a place for `.exec()` to happen as un-handled side-effect. But the preference is with using the IO monad as the stdlib is built around it, with `main() -> IO[i32]` as a type signature. As languages evolves I'm planning to build a runtime around IO execution, and build more constraints for handling strict side-effects. However for this initial stage of the language, I left it as a really simple solution.
> Do `impl` additions export? What happens when two libraries add the same function name with different signatures (or just bodies!) to a type's `impl` ?
For now the language doesn't support modules (libraries), I'm planning on adding it. At the moment it's a bit of undefined behavior, as overloading would occur with latest `impl` definition.
> Is currying automatic? It doesn't seem to be, but, eg, the `sum(x: i32, y: i32)` function theoretically could be called as `sum(5)` to create a closure, but this isn't a documented feature if so.
In the type-system it is automatic, and it successfully passes type checker as it's entirely built on top of lambda calculus. But there's an issue with codegen right now. I can def look into it and document it.
Comment by nxobject 1 day ago
Comment by Twey 1 day ago
trait Functor[A]:
fun map[B](self, f: A -> B) -> Self[B];
This looks a little wacky to me. I see that you can write HKTs in their η-long form and refer to them unapplied (`Functor`). But I don't understand how I would use this syntax to attach something to the trait that _doesn't_ depend on `A`. For (a silly) example, trait SizedFunctor[A]: Functor[A]:
type Size;
fun size(self) -> Size;
How do I know that `List[A]::Size` is the same type as `List[B]::Size`?Relatedly, I want to read `Self` in there as ‘the thing that implements `Functor[A]`’ (e.g. List[A]`), but that makes `Self[B]`, instantiated, mean `List[A][B]`, which I think should be a kind error.
Comment by the_unproven 1 day ago
fun map[Self: Functor, A, B](self: Self[A], f: A -> B) -> Self[B];
Since `Self` is the unapplied constructor, `Self[B]` just means `Functor[B]` e.g. `List[B]` not `List[A][B]`.The example you've shown with `SizedFunctor` is not currently supported, as support for associated types is not yet implemented. I got it on the roadmap tho!
Comment by wavemode 1 day ago
trait ConvertTo[T]:
fun convert(self) -> T;
Seems to create a single trait ConvertTo, for a generic type with a [T] argument, rather than allowing one to define separate implementations for ConvertTo[i32], ConvertTo[String], etc.Comment by Twey 13 hours ago
I think what both I and the sibling comment are getting at is that there is a difference between `Functor : (Self : * → *) → Class` and `Functor : (Self : * → *) → (A : *) → Class`/preapplied `Functor : (Self : *) → Class` and the syntax seems to merge the two (using syntax for the latter that is automatically abstracted to the former). But it's not clear to me that you can do that without losing the ability to express some things. The associated type is a pointed example because the unwanted dependence breaks type equality, but consider also an associated function that should _not_ be parameterized by `A`.
Comment by the_unproven 25 minutes ago
You would want for type variable to not be attached directly to a type class on its definition? But still treated as a container type. Something like:
trait Functor:
fun fmap[A, B](f: A -> B, c: Self[A]) -> Self[B];
...
impl Functor for List[A]:
fun fmap[A, B](f: A -> B, l: List[A]) -> List[B]
List::fold(l, Nil[B], (t, h) => Cons(f(h), t))
...
The above would compile, but the Functor wouldn't be treated of a higher kind in the type-system. I'll try to work a flexible solution, thanks for the great callout!Comment by thesz 23 hours ago
From what I understand, GRIN does some parts of supercompilation [1] during optimization process. Supercompilation can prove equivalence of functional programs [2] modulo termination. So you can have something interesting and useful in almost no time. ;)
[1] https://themonadreader.wordpress.com/wp-content/uploads/2014/04/super-final.pdf
[2] https://www.researchgate.net/publication/225252220_Proving_the_Equivalence_of_Higher-Order_Terms_by_Means_of_Supercompilation
It appears that Fuse does not have user-defined operators. Am I right? If so, it is a major obstacle in creating embedded languages.Comment by codebje 18 hours ago
As I understand supercompilation, it's an extension of partial evaluation - optimisation is done on a graph of possible execution traces. The downsides should be obvious: execution traces rapidly grow massive, compilation resources grow superlinearly, and there are many cases in which the result is worse than the original.
What value would Fuse get from equivalence of terms, do you think?
Comment by thesz 10 hours ago
GRIN, if I am not mistaken, performs partial evaluation. For example, it constrains, for each eval site, a set of tags and set of heaps allocations an eval site can receive. This is close to a partial evaluation step of a supercompilation. GRIN does not perform unification, though, it is not described in the original thesis, but data flow graph matching would be close to unification, reducing code size.
> The downsides should be obvious: execution traces rapidly grow massive, compilation resources grow superlinearly, and there are many cases in which the result is worse than the original.
This can be constrained. Supercompilation usually gets ran to a fixed point, where no partial evaluation steps can be performed that are not unifiable with previously encountered evaluation steps. But supercompilation can be stopped at any point.
I believe you can read on that in Simon Peyton-Jones works, I am unable to find a link to that paper right now, I have troubles with the internet connection.
EDIT: here it is: https://simon.peytonjones.org/improving-supercompilation/
EDIT: Note "tag-bags," it rhymes with the tag sets of GRIN.
> What value would Fuse get from equivalence of terms, do you think?
I think that equivalence of terms is an efficient way to verify properties of programs. Myself, I am looking at consensus protocol implementation verification.
Comment by codebje 8 hours ago
Equivalence of terms is an efficient tool for verification; I suspect that isn't really in the set of goals for Fuse, though.
Comment by norir 1 day ago
How expressive is fuse? How long are equivalent programs written in fuse/rust/scala/haskell?
Can you show me a bug that the fuse compiler catches but some or all of the competition doesn't?
Comment by deepsun 1 day ago
I know jShell has been here like 10 years, but I'm not sure it's convenient to quickly write to a file, query a url, etc.
.ksh for Kotlin? Typescript (through Deno)? Lua?
Comment by Vedor 23 hours ago
Lua isn't statically typed, and while there is Teal – a statically typed variant of Lua – it requires separate build step.
Comment by Philpax 15 hours ago
Comment by Vedor 9 hours ago
Can you tell me how well it works in practice?
Comment by Philpax 5 minutes ago
You may be interested in https://lute.luau.org/, which is a node.js-style runtime for the language.
Comment by thesz 1 day ago
Comment by n_plus_1_acc 13 hours ago
Comment by rienbdj 21 hours ago
Comment by dehrmann 23 hours ago
Comment by helix278 1 day ago
Comment by dwb 1 day ago
Comment by smuffinator 1 day ago
Comment by mrkeen 1 day ago
They can't even represent Fuse's Functor example, i.e.
f: A -> B, x: F[A]
so maybe they don't get any points for syntax.Comment by lilbigdoot 14 hours ago
Comment by nee_oo_ru 1 day ago
Comment by lilbigdoot 14 hours ago
Comment by the_unproven 5 hours ago
Comment by Panzerschrek 14 hours ago
Comment by the_unproven 4 hours ago
Comment by mixmix 22 hours ago
Comment by peterbower 21 hours ago
Comment by strong-self 1 day ago
Comment by the_unproven 1 day ago
For example I’ve this config in helix:
[[language]]
name = "fuse"
scope = "source.fuse"
file-types = ["fuse"]
injection-regex = "fuse"
comment-token = "#"
indent = { tab-width = 2, unit = " " }
auto-format = true
formatter = { command = "fusefmt" }
[[grammar]]
name = "fuse"
source = { git = "https://github.com/stevanmilic/tree-sitter-fuse", rev = "eb5698f4867a4192064e54a92be280f4d2130e03" }Comment by toplinesoftsys 1 day ago
Comment by qsera 1 day ago
Comment by the_unproven 1 day ago
Comment by adastra22 1 day ago
Comment by qsera 7 hours ago
Comment by faangguyindia 1 day ago