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.
the_unproven 1 days ago [-]
First of all thanks for all the feedback and looking into it, appreciate it!
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.
nxobject 1 days ago [-]
I’d really encourage you with Unicode support, as frustrating as of a side track it may be - I’ve had fun dogfooding my hobby languages by writing small web servers and CLI utilities. Even if it just generates part of your shell prompt!
Twey 1 days ago [-]
Love to see a real-world example of GRIN!
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.
the_unproven 1 days ago [-]
`Self` isn't the applied type (`List[A]`), rather it's the type constructor of kind `* -> *` constrained by `Functor`. In the map example it gets desugared into:
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!
wavemode 1 days ago [-]
How do you define a trait that is itself generic? Like:
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.
Twey 14 hours ago [-]
Right, I got the notion — but syntactically I expect `Self` to refer to the thing named at the top of the block, which is a `Functor[A]`.
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`.
the_unproven 60 minutes ago [-]
Fair point, I don't disagree with the statement that `Self` can be limiting as the trait is defined for `Functor[A]`. Thus imposing limitations on type system.
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!
thesz 1 days ago [-]
Nice to see a pretty advanced language at frontpage of HN!
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. ;)
It appears that Fuse does not have user-defined operators. Am I right? If so, it is a major obstacle in creating embedded languages.
the_unproven 18 minutes ago [-]
This is great, I haven't been introduced in the notion of supercompiliation. Reading the papers you've listed and going through GRIN's paper [1] it ticks the boxes in terms of laziness and graph reduction. To keep the implementation of Fuse simple I've decided on using strict evaluation of GRIN programs instead of laziness, with my assumption that it would harder to debug/reason on the program. However, this was one of my next improvements: switching to a lazy evaluation with similar semantics to Haskell programs.
> It appears that Fuse does not have user-defined operators. Am I right?
Not yet, but I left this mechanism completely open. As operators are defined as type classes with their signs as method definitions.
Can you expand on your understanding of GRIN doing parts of supercompilation? As I understand it, GRIN doesn't do any supercompilation; it's a structural transformation optimiser built for functional languages, analyzing program flow across function calls for the whole program at once.
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?
thesz 11 hours ago [-]
> Can you expand on your understanding of GRIN doing parts of supercompilation?
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: 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.
codebje 9 hours ago [-]
Thanks for the link to SPJ's notes, I'll read that tomorrow.
Equivalence of terms is an efficient tool for verification; I suspect that isn't really in the set of goals for Fuse, though.
norir 1 days ago [-]
I would suggest adding objective metrics. How fast is this compiler? How long is the longest fuse program? How long does it take to compile? How fast at runtime is the fuse implementation of several benchmark programs compared to semantically equivalent programs written in other languages?
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?
deepsun 1 days ago [-]
Off topic: what is the most convenient statically typed language that don't require compilation/transpilation? To run instead of bash or Python, but types are mandatory (not just hints like python).
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?
Vedor 1 days ago [-]
OCaml has both compilator and interpreter.
Lua isn't statically typed, and while there is Teal – a statically typed variant of Lua – it requires separate build step.
Oh, nice, I didn't know - I always thought that Luau has only type annotations and is not statically typed per se. But yeah, looked into the documentation and you can set !strict mode per script, which will cause interpreter to assert the types. Looks promising.
Can you tell me how well it works in practice?
Philpax 40 minutes ago [-]
I've only used it for small scripts and can't say how well the type system actually functions/how it scales, but what I've seen so far has pleased me. (They have a _comptime_-equivalent! https://luau.org/types/type-functions/)
You may be interested in https://lute.luau.org/, which is a node.js-style runtime for the language.
F# is good because it can easily fetch and import third party dependencies in a script.
dehrmann 1 days ago [-]
A nit, but fusefs is a well-known thing, so you'll face some naming confusion.
helix278 1 days ago [-]
Superficially, I find the syntax very untuitive to read. Most languages opt to use <> for typevariables. Is there a specific reason you chose to deviate from that and use []?
dwb 1 days ago [-]
I actually can't think of any functional languages that use < > for type variables (I'm sure there's one or two, but it can't be common)
helix278 1 days ago [-]
Depends on what you mean by functional language. I was considering main-stream languages that support some form of functional programming. So Rust, F#, C#, Kotlin, Swift, Typescript, etc
smt88 1 days ago [-]
F# does, I think
smuffinator 1 days ago [-]
This is only true for C-style languages? Flix uses square brackets, as does Effekt, and Nim, as well as Python. Probably many more I'm missing. Gleam even uses normal parens.
mrkeen 1 days ago [-]
I'm not sure most languages get a vote here.
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.
lilbigdoot 15 hours ago [-]
Scala and a few others use `[]` (the compiler itself is written in Scala)
nee_oo_ru 1 days ago [-]
Congrats on the release! Really cool to see a working GRIN backend. I'm definitely bookmarking this to give it a spin as soon as I get some spare time.
lilbigdoot 15 hours ago [-]
Just curious, what made you choose GRIN? I haven't heard of it but it looks neat!
the_unproven 6 hours ago [-]
As I was deciding on the compiler backend, I stumbled upon on it in r/ProgrammingLanguages on reddit. I liked the syntax itself and the fact I can compile the language in the IR of a mini functional language; with a lot of benefits in terms of optimizations. Especially as (strict) pure functional language are notoriously slower than imperative language because of lack of mutations. Allowing me to have a higher-order functional language that has zero cost abstractions.
Panzerschrek 16 hours ago [-]
Can I write a game using OpenGL in this language? Does its functional purity allow this?
the_unproven 5 hours ago [-]
Technically yes, although there's no support for FFI yet. Purity would allow it as long as side-effects are wrapped into IO type.
mixmix 23 hours ago [-]
Looks clean! In impl blocks, how do you tell apart generic and concrete types? Is it possible for Foo to implement From[Bar] or Into[Baz]?
strong-self 1 days ago [-]
found tree-sitter-fuse in the org, updated yesterday. is editor support next, like an lsp over the scala typechecker?
the_unproven 1 days ago [-]
Yeah the LSP support is next, my goal is to implement the language server in the fuse itself. At the moment there’s a simple formatter implementation fusefmt: https://github.com/fuselang/fuse/blob/master/examples/fusefm..., you can compile it with fuse and hook-it with your editor.
What would you say is its key value proposition compared to other languages?
toplinesoftsys 1 days ago [-]
Great project - clean and simple syntax, pure functional language, uses GRIN framework.
qsera 1 days ago [-]
I wish you took Haskell's syntax as such. Why did you mix rust and possibly other stuff with it?
the_unproven 1 days ago [-]
Haskell is a great language with a really advanced type-system, although I found its syntax hard to read at times especially as I was exploring the language at first. On the other hand I really liked how Rust syntax was defined in terms of ADTs, Traits & Methods Impls, with type signatures required for functions. Hence I wished for a similar functional language that has such write-style and type concepts, but stripping away the borrow checker, mutations, etc.
adastra22 1 days ago [-]
Just as general feedback, I think your experience matches up with more software developers than the GP. Haskell is really obtuse and unreadable for those who are not accustomed to it.
qsera 8 hours ago [-]
ELM being a lot more popular than Haskell with mostly the same syntax would indicate that it is not the syntax, that people find hard with Haskell.
faangguyindia 1 days ago [-]
i loved haskell but cross platform haskell deployment is still pain
Rendered at 21:11:18 GMT+0000 (Coordinated Universal Time) with Vercel.
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.
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.
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.
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!
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`.
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:
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!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. ;)
It appears that Fuse does not have user-defined operators. Am I right? If so, it is a major obstacle in creating embedded languages.> It appears that Fuse does not have user-defined operators. Am I right?
Not yet, but I left this mechanism completely open. As operators are defined as type classes with their signs as method definitions.
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?
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.
Equivalence of terms is an efficient tool for verification; I suspect that isn't really in the set of goals for Fuse, though.
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?
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?
Lua isn't statically typed, and while there is Teal – a statically typed variant of Lua – it requires separate build step.
Can you tell me how well it works in practice?
You may be interested in https://lute.luau.org/, which is a node.js-style runtime for the language.
They can't even represent Fuse's Functor example, i.e.
so maybe they don't get any points for syntax.For example I’ve this config in helix: