Once I finally found some code examples, all I saw was JavaScript with print instead of console.log. Doesn’t AssemblyScript already compile Typescript to web assembly?
I think a hello world example is insufficient. I would like to see some more code examples, especially some that are useful in the real world for building websites. It doesn't have to be a fully functional hacker news clone in Grain, but it should definitely be a little bit more than hello world.
It isn't only insufficient by far, it's the only complete code example. There's as good as nothing else. The chapter on control structures isn't even there. There are some cryptic remarks on mutation, but that's it.
An hello world type example that just prints to the console is pretty useless. An example of how to get something on screen is needed. The docs for the dom part of the standard library is just a "coming soon" page.
The way recursive functions are defined seems strange [1].
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`.
No, in this case I think you're mistaken. "Rec" means whatever is the definition of the variable can reference the variable itself.
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.
let vs. let rec is in ML and Scheme and some other languages. But the compiler could transform the let rec into a let, and e.g. Haskell does it so the programmer does not have to.
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.
Ahh, this makes sense. Thank you for the link. I was thinking it was more of modifier to switch on tail call optimisation (or something) rather than a scoping modifier.
It should be "statically typed" not "strongly typed". While I'm a big fan of statically typed languages which good type inference, the fact that the authors mix these terms up is not a good sign to me.
Variable shadowing is orthogonal to static typing. You can do the exact same thing in OCaml or Haskell - a is being redefined so it can be given any type you want.
For unrelated reasons, I wonder how strong Rust's runtime types are. That is, assuming you manage to compile something with undefined behavior, will the program crash immediately?
It won't necessarily crash immediately. Inside unsafe blocks you can invoke undefined behavior by overwriting arbitrary memory locations. The effects of reading that modified memory (including crashes) can become visible much later, long after the unsafe block.
That answer is technically correct but too shallow.
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.
> I could then call <variable reassignment> in Python also variable shadowing
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.
I believe the difference is that Rust implicitly creates a new scope when a variable of type X is (re)assigned with a different type. So you in rust you can write (python syntax)
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.
Here's an example of how the behaviour between Rust and Python differ because of scoping:
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.
Usually statically typed implies strongly typed (there are rare exceptions). Also, when you look at their first page, they focus on type-safety and that the language prevent runtime errors.
In combination it looks very much like they mixed it up.
It kind of reminds me of what ReasonML/ReScript look like. They bolt a new frontend (syntax) on the OCaml compiler, giving it a C/C++/Java/JS like feel (curly braces, parens around function args).
So it's an ocaml-ish language targeting webassembly. It looks it has the potential to be decent but it seems like it's at a very early stage of development and not really usable yet.
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
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.
The pitch on their home page is "puts academic language features to work". But one of those academic languages, Haskell, has a GHC-based compiler to Webassembly (asterius), and it's not clear why someone would prefer Grain if "academic language features" are what they want.
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.
> 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 aren't smart enough to put code examples on their homepage, yet they did spend effort on making it look professional, then you can assume it's either vaporware or corporate nonsense and dismiss it out right.
If they don't respect your time and attention now, extrapolate.
Given that Grail has a quite ML-esque syntax and is also quite new, how does it compare to something like OCaml with JS_of_OCaml? Why would I use one over the other?
Imo tuples are a nice feature for the same reason lambdas are. Sometimes, you just want an anonymous way to bundle a few values in a local context. For instance:
match (a, b) with:
(4, 2) => "Hello"
(1, 0) => "World"
(_, _) => "nope"
The trouble is that tuples are not restricted to local context.
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.
The issue with that is that making tuples a local-only construct is not trivial. A run-of-the-mill tuple is a data type that fits in the type system quite well, it's not costly to create. But a tuple that can only be used locally requires to make a special case in the compiler, and to check it everywhere.
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.
Is there any good writing on overuse of tuples? Up until this thread, I just always thought that was a pet peeve of mine.
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).
The industry is balkanized on it. Functional languages which tend to model closed systems, with strict types, and hence easier ad-hoc refactoring swear by tuples. OOP platforms which deal with the "outside world" a lot and so need stable API that can evolve without BC breaks avoid tuples.
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.
> Functional languages which tend to model closed systems [...] OOP platforms which deal with the "outside world" a lot
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.
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).
You seem to believe that, because tuples are available in a language, they will be used everywhere. And indeed, that wouldn't be great.
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 wish tuples didn't exist in interfaces, unfortunately every function and method argument list is one. /s
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).
> but the question is why even have that opportunity
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.
Making the names part of the type solves this, like structural object types in TypeScript. I’ve come to the conclusion myself that, with the right language syntax supporting it, positional arguments and tuples should be limited to two, maybe three entries, and in the vast majority of cases should be replaced with named/keyword arguments and records even when they are that small (with unnamed, ordered entries being used in cases where there aren’t better names than “first” and “second” or “a” and “b”).
Tuples are the data type representing "top of the stack", so as a core data type it's great; tuples + arrays + syntactic sugar are the building blocks that give you all the other data types.
Though I will continue to claim that quotient types should belong in the list but aren't - even in Cubical Agda that problem's not really solved! If you don't have an easily-computed canonical form for your equivalence classes, you're doomed in almost any language.
Thank you for the term "quotient types", that's the first time I'm hearing that!!!
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? :)
Sadly I don't have "my own programming language", and even if I did, I think this is an unsolved problem in language design :P Interesting question, though; I'll think about it.
>Grain aims to modernize innovative features from functional and academic programming languages and bring them to the masses.
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...
Compared to Javascript, almost anything else is academic. The kind of pattern matching featured in Grain, Haskell or Python 3.10, in particular, isn't mainstream despite being really old.
This sorely needs a page describing its features and overall design. I spent maybe 10 minutes reading documentation and gave up.
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.
I'm curious, how does it differ (or plan to differ) from Reason?
It seems that the language implements a _subset_ of reasonml features with slightly different syntax. The sources even contain some files from OCaml that are translated to Reason.
Maybe the doc isn't complete or I miss something, what about the module system, type inference, polymorphic variants?..
80 comments
[ 4.1 ms ] story [ 134 ms ] threadthat'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`.
[1] https://grain-lang.org/docs/guide/functions
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.
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)
I could then call
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: One could argue there is a difference with method signatures 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.
in Rust:
in Python: 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.In combination it looks very much like they mixed it up.
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
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.
Ultimately all objects/structs/records are at their core tuples. But we refer to their attributes by name for a good reason.
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 never heard about a real-world problem associated with fizz-buzz, besides teaching children to count. Do you have some documentation about that?
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? :)
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.
Benefit: a larger language, Drawback: a language not designed explicitly for the purpose.
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...
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.