i’ve been around and used rescript for 2 years now, actually more than 2 years. i love it. i also dislike typescript, i actually never really though typescript was a very good idea and i’ve sort of been proven to be rigth, because the type system is so bad it has to be repeatedly monkey-patched to handle an infinite number of scenarios, make it terribly slow.
anyway, what frustrates me with rescript is that it never feels complete. it’s like an inverted-warts situation where so many features are missing. i don’t have guard statements forcing me to branch everything. ok, fine, i can do that (but it absolutely worsens readability, but whatever, i can work around it). then we have loops, where there are no break statements. so for a tokenizer, for example, you first have to peek as the loop condition, and then assign inside the loop and actually eat the token. this is getting problematic.
and of course i can have a mutable variable outside the loop, but then i have to do a while condition, eat the token from the tokenizer (or data chunk from whatever reader), update the condition and so on. and for that i have to alloc with ref() for no real reason.
so there’s no if-let, while-let, no return and no break. whoever says this is not a problem is just plain-out wrong. the language feels gimped and incomplete, despite being oh-so-beautiful (especially now with 12 where we can finally stop saying *. /. +. -. and so on). i love you guys, i love your work, but good lord can we just sprinkle a few of these very reasonable features for those of us wanting to get work done in this fantastic language?
thanks. i agree we’re making progress, but i think the team is too conservative with regard to adding new features. if-let was decided as not needed because you don’t really gain anything over switch. i can sort of agree, although it’s a stretch. but while-let and guard clauses are crucial, because it makes code way harder to both write and read if you don’t have them.
while let Some(data) = dataReader.read() {
// ...
}
vs
let done = ref(false) // also is a useless alloc unless rescript is optimizing that away, unsure
while !done.contents {
switch dataReader.read() {
| Some(data) => // ...
| None => done := true
}
}
it’s ridiculously convoluted just because we have neither while-let, if-let, break or return.
EDIT: also keep in mind that the above is just one trivial example, you’re going to have nested instances of the above in all sorts of different forms in any real-life code base with a few tens of thousands of lines in it.
I have been in a loop between rescript and scala.js
being simple like rescript it’s nice until more power it’s required
being complete like scala.js it’s good until it gets overwhelming
However think it’s possible to agree on sweet spot for evolution, instead of being stuck in 1999 like C does, can evolve like Rust in 2024, for example lots of people have been asking for traits which is very common feature in functional languages like Rust or Scala and can be verbosely emulated on rescript by using modile but it’s not supported as language construct
There are definitely things that can be improved in ReScript and we’re actually working hard to improve them (and we welcome contributions too by the way), one of them is building better primitives to iterate things, there has been some experiments ongoing around for of/in, but it’s not an easy task to come up with a simple yet powerful syntax/semantics.
But at the same time, this pain is not really a surprise because it comes from one of the main design principles of rescript where things are immutable by default, which brings a lot of benefits and you can’t really have a cake and eat it, can you? You’d likely not use a while loop in such cases in rescript because since things are immutable you wouldn’t be able to return something out of the read chunk unless you do allocate a variable for this (or only apply side effects of course). You’d likely just use a recursive function and if you happen to use this pattern often, you’d write a helper function for it:
module Reader: {
type t<'a> = unit => option<'a>
let reduce: (t<'a>, 'b, ('b, 'a) => 'b) => 'b
let forEach: (t<'a>, 'a => unit) => unit
} = {
type t<'a> = unit => option<'a>
let reduce = (t, init, f) => {
let rec aux = acc =>
switch t() {
| Some(data) => aux(f(acc, data))
| None => acc
}
aux(init)
}
let forEach = (t, f) => reduce(t, (), ((), a) => f(a))
}
Playground link
I’m not saying it’s better or worse than a while loop, it’s just more idiomatic in rescript and it’s hard to design a language that is great at everything and with any style of programming.
We’re definitely all ears when it comes to listening to our users’ feedback and improving the warts of rescript, so keep them coming!
I didn’t know about the ? operator in rust, it looks interesting but how do you do when you use it multiple times in the same function with expressions that return different error types?
I’m not a big fan of the use operator from gleam (even though I really like most of gleam’s design decisions), it reverses the flow of operations and makes it harder to scan the logic of a function imo.
I find OCaml’s let* nicer to read but wait until you start using it with complex types, it’s a real mess to understand the error messages. And sometimes it also makes it easier to forget about handling some error cases.
When dealing with such issues, the first thing that comes to my mind is that I don’t bump into this issue that often because you can often just break down the logic into smaller functions that you pipe into each other. And even if you happen to have some ugly and indented code, those escape hatches tend to feel cool at first but it makes it much harder to read your code after some time.
One thing you can do if you want to write code in a more direct way would be to use reanalyze’s exception tracking, it’d be more explicit, easier to read even if you come back to it later imo.
This is a very interesting discussion, so please don’t hesitate to chime in!
you can’t, afaik. they have to be able to share a common type.
i’m just now building the prototype for a bigger project and i decided we are going to explore resscript, but tbh without ? or any form of early returns, using results basically turns the entire project into an endless amount of nested switches. it’s just not feasible, so we might have to start using exceptions to signal errors instead. which isn’t really the route i had imagined.
also, please check out rust’s option and result and just compare the ergonomics with rescript. this should be a rather trivial thing to address because not much code is missing, but we don’t have result’s andThen or options getOrElse in rescript. these are by no means required, but improve the ergonomics of using them.
however, having no if-let (ok, switch can do this but it’s cumbersome), no while-let (again, can be done but quickly becomes convoluted), no break (convoluted), no early return (wreaks havoc on your code base) is becoming more and more difficult, and it makes me feel i constantly have to fight the language. i recall there’s some library (rescript-future or whatever) that sort of helps the situation a little bit, but it turns your functions into sections of maps and flatmaps instead so doesn’t really fix it.
the problem with having no early return is that you can’t “jump out” a few levels of your nested code and thus you have to keep digging yourself deeper and then handle all the return cases at the end of your function. this works if you fragment your functions into really small pieces, but it becomes very cumbersome. early returns are really useful for sanity checks, and combining sanity checks with actual function return results (which you have to do when you have no early returns, since you have to lump them together at the end of the function) makes your code MUCH more difficult to understand
this is a fact of life in rescript. you gotta be misinterpreting me, or i’m misinterpreting you. it’s the same with a switch statement today, each branch has to generate the same type output.
there is no “map” function for None states, meaning you cant do something like value->Option.getOrElse(slowSyncOp). the value provided to getOr has to be computed before putting it into getOr. see what I mean?
same with andThen for result. these are not necessary, but provide some nice ergonomics.
i’m pulling code from a commercial project here so it’s a bit adjusted and lacks context (and also i’m pushing the boundaries of what i’m allowed to share):
Things described here I’d like to import to ReScript someday. I’d like to say that I’m already exploring options, including the things mentioned here, especially direct-style control flow. I’m taking inspiration from Rust, but also from all sorts of other languages. We have to look at a lot of things to make it fit with the entire ReScript codebase.
I don’t think we’re conservative about grammar evolution. That’s actually why we became ReScript. We’re in the process. There’s no “complete”. We’re just being careful about prioritizing work because we’re volunteers with limited time. We can find ways to do it well. But we have to finish more important work before we propose it.
Brain dump here, this is my personal roadmap (v13? i’m not sure)
Early-return semantics, let-else guard
Iterators & better for loop, I’m now reviewing prior arts (my current pick is Zig)
Generators of course
Better interop for results ↔ exceptions, this is also inspired by Zig.
Disposing syntax works (e.g. finally, using, defer)
I know a lot of other fancy languages, even the languages people forget or don’t mention are on my radar too. But integrating them to work with ReScript codebase seamlessly doesn’t seem easy. I need to do my own research.