I am a long time ReScript and OCaml user. I find working with Rescript incredibly unproductive because of constant context switches. It’s dictated by a single choice in tooling architecture. I don’t blame anyone for this terrible choice, you couldn’t know better while making this decision. I perceive it as a systemic failure in the way language and tooling development is ran and funded.
The tooling choice made Rescript a good language for bug fixing, minor refactorings, and linear coding and a pretty bad one for creative coding. I define “linear” and “creative” as:
linear: you precisely know what you want to do in code, what kind of modules you need to implement, what functions, what’s the overall structure
creative: you know what you want to build but you might not be aware of some details yet
In my experience a significant portion of building new functionalities in programs involves creative coding.
The problem
Assume 3 files: A.res, A.resi, B.res
In B.res I am implementing the core functionality I’m interested in at the time. It uses some stuff from module A. Turns out I need to slightly refactor A, I change some signatures in resi. What happens next:
In OCaml; carry on and finish implementing B before you have to jump back to A and fix the type error
In ReScript: Fix the error in A first, go back to B to carry on the real work.
Now multiply it by a couple of files. Madness.
It’s made even worse in Rescript by the behavior where B will compile until the point where A is referenced, then it will just show no errors until you fix A. Obviously there is a workaround, just put some Obj.magics in A, remove the references to A altogether etc.
All in all it makes Rescript incredibly bad to work with. I’m starting to understand my longtime friend and colleague, one of the smartest people I know, who’s really appalled by ReScript.
Hm, if you constantly have to change interface files, maybe your overall design is not so solid…? Good interfaces are supposed to be resilient against change (true for both FP and OOP). Then again, I’m not really a believer in encapsulation anymore, haha.
Maybe you can give us some example code?
I’m just using Neovim and build stuff in bash (watch mode), maybe I wouldn’t encounter the same issues as you. Dunno.
I’m also using neovim and run rescript in watch mode. When you write rescript pay attention to how much context switching it involves, you might notice that issue, too, you just might reflexively follow the compiler, which is ok, but IMO typically you want to write something starting from the “outermost”/most “top level” function, and only later focus on details. That’s the magic of interface files.
Example? Here’s a trivial one.
let x = 1.
let y = x + 1.2
let z = doSomethingInteresting(y)
Js.Console.log(z * B.multiplier)
where doSomethingInteresting is defined as float => float in the resi but float => int in res.
So, now let’s go through type errors:
First, line two, we have + instead of +.. Now, I’d expect to see an error on line 4 in OCaml (to replace * with *.) but it is not the case in Rescript. In Rescript, you don’t see any more errors here. You have to do a context switch, and fix the error in the implementation in A.res, before you get the errors on the next lines here. It’s because it very naively follows the DAG when compiling. That’s the issue number one.
I can actually live with only one type error per file at a time, it likely has a negative impact on performance (again, you might want to finish some function “down below” before finishing up the details upwards) but it doesn’t have the same compounding power * as the issue I described above. It might also have positive impact on productivity by constraining focus to one type error at a time, so mixed feelings about this behavior actually.
*: the problem gets worse the more modules are involved
I don’t see your problem as a flaw in the compiler, but more as a missing feature from your ide.
In vscode, I have a shortcut to go to the next error.
So when I got some, I just follow the compiler without leaving my keyboard.
In the example above the “next error” is not located in the file you’re currently editing. The next error follows the DAG which is how the compiler happens to think, not a human. As a human I want to finish working on this file before I context switch to the next one.
Also, I don’t want to be pedantic but in case somebody responds “it’s not the compiler, it’s the build system!” as some kind of enlightened thought; yes, it is the build system architecture, specifically IIRC the way ninja rules are structured, but in rescript it’s all opaque to the user so I allow myself to use the ‘compiler’ mental shortcut.
But you don’t have to write an .resi file, right? Everything can be public.
I think it’s an OCaml compiler thing, that it aborts at first error it finds.
Further more, it’s not 100% uncommon to think “global type-inference was a mistake” in the OCaml community, haha. Personally, when coding OCaml, I make almost all types explicit, just to get proper and more helpful error messages. At least always in function signatures.
Arguing about the magnitude of the problem is pointless and I won’t engage.
The problem exists, it’s real, it’s extremely acute in my experience. Seasoned OCaml developers might (or might not) feel the same but despite the fact that rescript and ocaml are so alike from the type system standpoint, this detail makes writing programs a completely different experience.
The only thing I will respond to; “it’s an OCaml compiler thing”; Rescript tooling could behave the same way ocaml tooling does and no feature of OCaml makes it impossible. In fact, merlin/ocaml lsp allow for the workflow I’m striving for (and it’s part of why I really like OCaml)
Hm, I didn’t yet code enough in ReScript to see the difference to my OCaml workflow. Maybe I can return to this thread at a later point. Professionally I work with PHP, sooooo hard to imagine anything worse than that (despite using multiple tools to check for types and unused code in PHP).
I don’t have expertise here, but I suspect it comes down to deeper questions like whether ReScript is compiling stand alone modules or full programs, which might be driven by the goals of Rescript’s level of integration with npm, babel, and bundlers. I think there are a lot of js targetting languages that have low ambitions for general js tooling reuse such as scalajs, jsoo, elm, but few that try to do what ReScript does. I personally am more interested in Elm’s goals, but I can relate to Rescript’s goals.
I am an outsider in Elm at this point, haven’t used it in years, but my rough impression is: build tooling and ecosystem from the ground up (except for piping final output through parcel), treat code coming from npm as dangerous code that should be jailed away from other code, emphasize slow deliberate pace over churn, prefer beginner friendly over complexity when you have to make a choice, emphasize pure code and delegating impure actions to the framework. Rescript overlaps with some of these, but it has a very different answer when it comes to tooling and ecosystem. The current Rescript tooling and ecosystem answers might be driven by lack of resources or it may be a long term goal to have codebases that frequently mix javascript and rescript together.
Let’s not attack or belittle people for how they choose to write code, please.
Problems such as the one you’re describing @wokalski were discussed at some length recently:
As you can see I agree with you. It’s very unfortunate that the OCaml tooling (which still work if using reasonml syntax, even today) had to be thrown out with the change to ReScript.
I get the impression that these wheels will be reinvented at some point but I’m not sure when. There are some very smart people working on the IDE extension now.
I read it as kind advice from armchair experts. It’s the internet after all, nobody called me stupid for not being able to compile my codebase in my brain (yet).
Particularly @cristianoc is a static analysis guru. I’m curious how difficult it is to bring back those two functionalities (proceeding with the compilation of dependents in case of interface/implementation mismatch and what you’re describing above).
I imagine it’s quite difficult, but once the compiler is easier for the community to work on (which is well underway) hopefully something like this happens.