Grain: A strongly-typed functional programming language for the modern web
grain-lang.org
grain-lang.org
What are these features?
This could be things like "pattern matching and sum types", or it could be things like "effect systems and dependent types". Personally I don't think you can reasonably call pattern matching an "innovative feature" from an "academic programming language", but as far as I can tell that's what they mean...
Ultimately all objects/structs/records are at their core tuples. But we refer to their attributes by name for a good reason.
match (a, b) with:
(4, 2) => "Hello"
(1, 0) => "World"
(_, _) => "nope" match (n % 3, n % 5) with:
(0, 0) => print "FizzBuzz"
(1, 0) => print "Fizz"
(0, 1) => print "Buzz"
(_, _) => print nI never heard about a real-world problem associated with fizz-buzz, besides teaching children to count. Do you have some documentation about that?
JS has made a few steps in allowing us to have objects as compact as a tuple, i.e. {foo, bar, baz}.
I think we need more effort in that direction. But keep fields addressable by name at all times. In fact, ordered arguments in functions have the same problem (they're a tuple). Unfortunately that ship has sailed.
Besides, you may end up with divergences in your community about how local a tuple should be allowed to be. Can it be passed to a lambda? Can you map over it in a local context? etc. Overall, deciding where tuples should be used is an easy choice for the developer, and a very hard choice for the language creator.
Internally I've tried to make the case that nobody understands the reason a tuple exists except the person who wrote it, and a discriminating union, record, or variant is always a better choice if you're optimizing for the common case (that is, readability).
Notice also web APIs virtually never use tuples (everything is objects with named fields both in complex input and virtually all output).
A language that targets the browser is going to be inherently open. It has to deal with browser DOM, it has to deal with service APIs, it also has to potentially interact with JS libraries.
It'll be an uphill struggle to apply Ocaml style in it.
The open vs closed systems logic simply doesn't have any kind of relation with whether one "deals with the outside world", it's about knowing what you're about to receive. And Java, which you quoted earlier, is very much geared for closed systems. On the other hand, Prolog, a language that hardly qualifies as OOP is built to work on open systems.
Closed systems language deal with the outside world pretty gracefully, by parsing their inputs.
> A language that targets the browser is going to be inherently open.
There's been a flurry of closed-system languages for the web in the last decade (e.g. Elm, Fable, etc.), and they work just fine.
As functional languages often do. /s
Tuples have coupling order. That's a fact. So if you have tuple [a, b, c, d], then you can maybe extend this by adding ...e, f] at the end, if your parser can ignore extraneous items.
But even if your parser can do that, you still can't remove b without BC breaks, because then the rest get shifted, and you have a mess.
This is not a problem when the tuple exists only in the confines of some singular unit of code, that gets refactored together. But if it escapes, you have a problem. Because over time, you'll want to change this tuple, and you can't refactor the entire world at once (which is more often than not the issue of functional programming and why it can't scale well).
From my mileage in this industry, though, the cases you describe simply don't happen. As you said, tuples aren't used in interfaces, because developers know better than to do that.
> which is more often than not the issue of functional programming and why it can't scale well
I don't know what you've done with FP, but in your last few posts, you've been making claims about it that simply don't match my experience. There's not much to discuss without specifics, though, so maybe you could expand about the cases where you had issues with it?
I appreciate your point that people try and avoid tuples where not appropriate, but the question is why even have that opportunity, when we can nudge the community to use equivalent "named attribute tuples".
Regarding scale, FP scales great for certain types of problems. But it's not great at modeling or encouraging boundaries. Imagine doing FP where all your types cease to exist for part of your codebase, but also you need to interact with the code where the types are. Even more, imagine if those two modules may fail independently (i.e. so you lose one of them). Anyway I think I'm becoming too abstract.
Basically the FP world needs to be more like Erlang. Erlang is functional but keeps units bounded (in processes).
I have provided a valuable use case above, and creating a structure allowing convenient use cases while preventing inconvenient ones would be extremely costly.
> But it's not great at modeling or encouraging boundaries.
What features are you missing specifically? You cited the need to "keep units bounded", but most FP langs provide the ability to contain implementation details within modules, by not exporting them.
> Imagine doing FP where all your types cease to exist for part of your codebase, but also you need to interact with the code where the types are. Even more, imagine if those two modules may fail independently (i.e. so you lose one of them).
I've had to deal with antiquated DBs containing unreliable data using flaky drivers, and while working with legacy can be painful sometimes, the FP paradigm caused no specific trouble.
I agree that this would be an amazing thing to have, but I've never seen those anywhere in practice (not even in Idris). My gut-feeling tells me that it might not work in combination with composability in the sense that there might never be a proper global "equal" that everyone can agree one.
I'm curious - besides the ones mentioned, are there any other fundamental type concepts that you would build into your own programming language? :)
In combination it looks very much like they mixed it up.
fn main() {
let a = String::from("Hallo");
let a = 3;
}I could then call
a = "hallo"
// can call String methods, runtime checked
a = 3
// can call int methods, runtime checked
in Python also variable shadowing. But in this case you assume a is the same but the type changes, and call it dynamic typing, while in the Rust example you assume the two a's are not the same variable: let a = String::from("Hallo");
// can call String methods, compiler checked
let a = 3
// can call i methods, compiler checked
One could argue there is a difference with method signatures def f( a )
a = "hallo"
f(a)
a = 3
f(a)
which does work in Python ("dynamic") but not in Rust ("static").But then the actual usage of "a" in the method effectively puts it on the same level as interfaces in Rust and blurs further with static languages like Typescript without nominal types.
You could, but you would be wrong. Shadowing is associated with opening a new scope. Reassignment modifies a location in an existing scope. Python does the latter.
a = "hello"
{
// new scope
a = 3
}
// a == "hello"
and is there a difference between Rust and Python?in Rust:
fn main() {
let a = 3;
let f = | | { a };
let a = "b";
println!("{}, {}", f(), a);
}
in Python: a = 3
def f ():
return a
a = "b"
print("{}, {}".format(f(), a))
The rust code outputs "3, b" while the python code outputs "b, b". The python code _assigns_ to the variable a, whereas the rust code creates a new scope and redefines a, shadowing it in the parent scope. a = 3
a = "x"
and both of these variables will exist at the same time with different times and potentially referable to from the outside. In python that cannot work, because there are no types. That's why in python the behavior is to always overwrite the existing variable, or create a new one in a local scope _if and only if_ this scope is somehow specified, i.e. by declaring a function.You can read more about what things the programmer has to manually avoid while writing code in "unsafe" code blocks: https://doc.rust-lang.org/reference/behavior-considered-unde...
EDIT: this means that if you stick to safe rust, the type system ensures you cannot have data races (among other things)
The language seems to draw lots of inspiration from Rust. Not sure how polished their type system is, though.
The pitch is also a bit unclear to me about which users are being targeted. In my experience, small languages that don't focus on a single area have a hard time, and aside from "modern functional language features" (and modern functional languages are a dime a dozen these days), there doesn't seem to be lots of specific about the target space.
On the other hand, it is nice to see that there is LSP support provided for the language.
There are a bunch of languages that compile to webassembly, including Rust, Golang, Python (via Pyodide), and Kotlin, so that by itself isn't a great competitive differentiator either.
If you've ever tried actually doing something with webassemly, you know that none of these actually work that well and should be considered pre-alpha software. (And often abandoned or unsupported.)
Realistically at this point your only choice for webassembly is emscripten, but the code quality and documentation quality of that project is abysmal.
Grain makes sense from that point of view; something that compiles to webassembly and isn't a giant pile of unvetted legacy hacks doesn't exist right now.
If they don't respect your time and attention now, extrapolate.
Benefit: a larger language, Drawback: a language not designed explicitly for the purpose.
Reading between the lines, it’s statically-typed, compiles to assembly, and I haven’t seen a type annotation yet, but I’ve seen a “let rec,” so maybe it’s similar to OCaml? Or even Ocaml in disguise again? But perhaps they decided to obscure that for marketing reasons, as if adopters of hot-off-the-press programming languages are just programmers looking for something nondescriptly “modern”, rather than programming language enthusiasts.
This language has polymorphism and mutation, like OCaml. I wonder how they avoid the "value restriction" problem that in OCaml forces you to write type annotations in some places: https://ocaml.org/manual/polymorphism.html
that's my main gripe with TS: it's bolted on nature.
that's why I prefer a clean slate (like Elm, Rust, F#/Fable, ReasonML/ReScript, PureScript, or this "Grain" project)
The are declared using `let rec foo = ()=> ...` syntax. So the modifier is on the variable declaration rather than the function definition. Since recursiveness is a property of the function this seems like the wrong way round. JavaScript gets this right with `async`.
In other words, the default behaviour is that the scope, during variable definition, does not include the variable being defined. With "rec" the variable being defined is added to the scope in the definition of the variable itself.
Coming from lambda calculus, this makes sense: functions themselves cannot be recursive. What makes them recursive is taking their own definition as a parameter, I.e. getting their name in their scope.
It's not exactly a trivial algorithm as it involves finding strongly connected components from the dependency graph and then topologically sorting that.
I implemented this for a toy programming language compiler project, it was a fun one. Luckily I had SCC and topological sort from Haskell's Data.Graph module.
And some stuff which might normally be in a standard library.
Didn't read anything about concurrency, etc.
I was looking for a clue if this was a wrapper for Typescript to eventually run on the V8 Javascript engine.