My main gripes with rescript (constructive criticism)

i’ve been using rescript since 10.1 or something, quite a bit before async/await was even part of the language. today, i’m basically only using rescript if i can. it’s my favorite language no matter what i’m going to do (except multi-threaded computation, but it’s rare fo rme to do cpu-bound computation today). 99.9% of the time, rescript is a perfect fit if i can model the types properly (and that’s on me).

it has its warts, however, and i really don’t mind. there are, however a few things that i would love to see fixed:

  1. can’t turn off formatting for sections. this is a problem because i know when i need to take control of code alignment. aligning on comments and what have you is often a code smell, but after 30 years of programming, i’m quite confident i know where to draw that line. i would love to have some way of turning off the formatter for a block in a file instead of having to fight the formatter (and just giving up since there’s really no fight to be had except trying to restructure the entire code block)

  2. .resi-files. they’re great, so don’t get me wrong here, however for trivial things like keeping something private or just marking type t = whatever as opaque to the outside world, i don’t want to have to double the amount of source files in my project. keeping module internals private should be the default (i know there’s a thread on this). particularly, the opaque type t thing would be useful for me. we often change type ts in modules as we hammer out new functionality, and often get e.g. string interpolation errors because some type t wasn’t opaque and so type t = string would work fine in string interpolation, but should never have been used as such. this is a recurring albeit small issue for us. maybe opaque type t is possible today and i’ve missed something.

  3. complexity of error messages. this has improved greatly since v10, but it’s still not as good as i would like. the error messages are often exhaustingly long and i find myself more or less reverting to “random” code changes until i get a shorter message that i can bother to read. it works, but i’m sure this area can be improved.

  4. minor: i never get used to .res for source files. this is ingrained in me as resource files, but yeah, this is an artifact from the 90’s i guess. it would likely be even weirder for me to see resx for jsx rescript files.

  5. i still haven’t found a way to export only the modules i want to export from libs/projects. i saw someone mentioning an exports attribute in rescript.json but it seems to not work (at least not the way i would expect it to). our libs have tons of internal code that is basically just infra for the lib itself that shouldnt be exposed, but there seems to be no way to prevent that…

  6. the stdlib is still lacking heavily. it needs a ton of work and many things are still unclear (part of the stdlib is just rescript impl of js counterpartss, part of it lives under the same “namespace” but has nothing to do with js, part of it is mixed). i think a direction should be decided on. for me, i would love to see a rough mirroring of js counterparts, but also a huge growth beyond that. i think this is part of the maturing of the language, it needs to be ready to “go its own way” and not just be a syntactical typed layer on top of js. there’s so much we can do if we allow it to grow further in this direction. (just a few things to give you an idea of what i have in mind, why don’t we have Option.getOrElse that takes a thunk, or Array.zip, or Promise.mapAsync, or even Array.forEachAsync since async/await is now native to the language and so on). we also still have Js. but i think the idea is alerady to remove this? still, confusing.

  7. type inference. there are situations where the compiler should trivially be able to infer a type but just won’t. here’s one such example:

module M = {
  type t = Foo
}

let f = (): promise<M.t> => Promise.resolve(Foo)

it’s clear that f can never return anything other than Foo (through Promise.resolve), but the compiler is unable to infer it. i suspect this is due to compiler internals and it might be complex to implement, but to me as a developer, the type is easy to infer because it couldn’t ever be anything else. yes, we can say M.t in this example but shouldn’t have to because we already said promise<M.t> and beyond that, we might have a switch with differently nested Foo’s and we’d have to say M.Foo in each one depending on how their nesting looks, and it’s less fun when your module names are Dsl_Lang_Libs_Stdlib_SeqOps as is the case in one of our codebases (which by the way is related to the exports problem i mention above).

we could even do:

module M = {
  type t = Foo
}

module N = {
  type t = Foo
}

let f = (): promise<M.t> => Promise.resolve(N.Foo)

at which point the compiler immediately tells you:

Type Errors
[E] Line 9, column 28:

This has type: RescriptCore.Promise.t<N.t> (defined as promise<N.t>)
  But it's expected to have type: promise<M.t>
  
  The incompatible parts:
    N.t vs M.t

so it knows exactly what type to expect, but still won’t infer it. what?

.

please take the above as an attempt at constructive feedback, rescript is the best language i’ve come across for almost any problem i encounter. i rarely or never do frontend work personally but rescript still “outergonomics” almost any other language out there while retaining sound types.

This is a lot, I will address only #1 for now:

We want to keep the formatter non-configurable. We’d rather relax some rules so please if there are any particular problems just create an issue. One thing I still need to do is to implement smart formatting for arrays and JSX like we have with records and pipe chains: [ANN] Smart linebreaks for pipe chains

Would that help you?

thanks,

not really, i often have stuff like

@module("x") extern f1: t1 => t2 = "abc"

@module("x") extern f2: t1 => t2 = "abc"

@module("x") extern f3: t1 => t2 = "abc"

@module("x") extern f4: t1 => t2 = "abc"

and the formatter absolutely mangles it into something like:

@module("x") extern f1: t1 => t2 =
  "abc"

@module("x")
extern f2: t1 => t2 = "abc"

@module("x")
extern f3: t1 => t2 =
  "abc"

@module("x") extern f4: t1 => t2 = "abc"

because the type names/extern names are of quite varying length. even though they are “too long” here, i know it would be easier to read if they were all kept on a single line because it’s a repated pattern. in files with 30-40 externs, this becomes an absolute mess.

1 Like

Care to open an issue for that?

i’m not sure i’m communicating it well. it’s because some of hte lines will be beyond 100 chars and some won’t, and the formatter ruthlessly breaks the longer ones. it’s by design and that’s why i’m asking for a “let me format here because i know better”-switch, like black (for python) has # fmt: off and # fmt: on

Just put your example with the description in an issue. Whether we fix that with a formatting directive or just relax the formatter rules can be further discussed in the issue.

1 Like

2. .resi files

Private-by-default is maybe a thing we can tackle when the old build system is gone. Maybe. I still love to prototype without having to do all imports and generate the interface after the implementation is done.

One possibly dumb idea of mine is a hybrid solution where there is everything public by default still but adding the first pub would add the value to the interface behind the scenes and everything becomes private. Could be too confusing though.

3. Error messages

@zth is fixing them one by one. Some are harder to tackle but we try to add more and more context in order to make them more accurate. This is actually one of the most important issues and still needs to be improved (and it will) both for humans and AI.

4. .res file extension

We won’t change the file extension. The exception are technical issues like if we really want to differentiate between jsx files and pure rescript files (I don’t!) or legal issues.

It’s a bit unfortunate that resource files on GitHub are detected as ReScript now but it is also a kind of viral marketing so I would not bother to improve that.

5. Exports

I think you mean the public field in “sources”.

{
  "sources": {
    "dir": "src",
    "public": ["MyMainModule"]
  }
}

a quick check at the source code revealed that it’s not implemented in the new build system. I am not sure though if there is anything else in the works that supersedes it but will check it with the rewatch guys.

As a workaround maybe use rescript legacy build in CI and rescript build for development?

6. Stdlib

The goal is to only have one stdlib in the compiler that strikes a balance between being close to the JS runtime but extends it everywhere JS is still lacking. The (literal) Js namespace is deprecated and will be removed in a future release. The Belt namespace will become an external library at some point. We care about it not being too bloated. If you really think there are fundamental parts missing please submit a PR. Personally I recently added some async methods to Result. I am sure you can do that too :wink:

7. Type inference

module M = {
  type t = Foo
}

let test: M.t = Foo

This works but I think it is a bit of a hack. So I am unsure about this, but give it a try and open another issue?

Remark

Don’t worry, we’ll always take this as constructive criticism. You have good ideas and attention to detail, exactly the type of user that brings us all forward. So keep 'em coming! :+1:

1 Like

About 5. you can use the public parameter inside sources of rescript.json (see docs):

...
  "sources": [
    {
      "dir": "src",
      "public": [ "MyPublicModule"]
    },
  ],
...

About 7. adding type annotations is kind of a smell in rescript, a more idiomatic way to write the function would just be:

module M = {
  type t = Foo
}

let f = () => Promise.resolve(M.Foo)
1 Like

thank you for your thoughtful reply

  1. we all have different ways to model or approach problems. i tend to agree with you, i don’t want us to get rid for resi files, but again, usually we only need opaque type t and nothing more, and a resi file for each res file in a projects with ~200 res files would suck to put it mildly.

  2. great to hear. interesting point re. ai!

  3. i understand that, i still had to get it off my chest :wink:

  4. tsnobip addresses tihs as well, good to see that there’s still a way to do it!

  5. i’ll gladly contribute code once i can get my head around github again, i stopped using it 10 years ago and generally dislike it, but i’ll look into it. do you generally accept PRs if they make sense or do you want to see RFCs first?

  6. yes, my point is that rescript won’t infer type parameters of return types (i think)

thanks

For error messages, please make a habit of opening an issue whenever you find something that could be improved. If you have suggestions as well, that’s great. And tag me if you want.

Error messages are a priority, especially now with AI (we’ve seen massive improvements in AI performance just with the work we’ve done so far for error messages).

2 Likes

RFCs for some Stdlib additions are overkill. Just don’t add all of them in one PR so that they can be discussed independently. One PR per Stdlib file would be nice.

If you have some bigger ideas (like how to implement a good private-by-default), just drop them on the forum first (we have a tag for that). Technically we also have a RFCs repository but we are not religious about putting everything there (and you dislike GitHub anyways).

For everything else (smaller feature requests or bug reports) as usual just create a GitHub issue.

2 Likes

i forgot to bring one point up and that is tracking internal mutability:

  1. i get that this might be a difficult line to draw given the coupling with js, but it’s not obvious that array is internally mutable. in the best of worlds, a->Array.push would fail unless you had something like let mut a = … but that still leaves the problem somewhat open (external vs internal mutability, also fields etc). we use a (not super-pleasant) suffix of Mut for all vars that we intentionally mutate (e.g., storeMut or concatedStrMut but this is not really enough (and we make mistakes lol). it’s just hard to track mutability, especially if we pass arrays into functions, you have to trust that whoever wrote the function either is sane enough to not mutate it without documenting it (turns out we have insane people in our team) and/or copy it if they do. are there any plans to address this? we even considered aliasing Array etc with Belt variants to enforce immutability.

I think there’s a plan to introduce an immutable array module in the stdlib for this.

right but it holds true for a slew of types, Array, Dict, TypedArray and probably more. maybe they will be addressed too? i would love for immutability to be the default in rescript. this would be a huge breaking change but maybe we could have Array be immutable in v13 and then add MutArray which is a mutable layer on top of js arrays (and same for Dict, etc.)

if it’s an issue you have with many types, maybe it’s something that could be better achieved with linting or by providing your own immuatable versions of these types.

1 Like

Yeah this has been discussed at length before, and ReScript’s stdlib is intended to stay close to JS. So it won’t be made immutable by default.

However, it’s easy enough to set up your own immutable versions and use them in your repo. All the necessary facilities exists for that. And it’s not ruled out that we might also ship other data structures and/or immutable versions of things in addition to the things that exist.