How to work through interop more efficiently + array mutability wtf?

Hi,

I just recently discovered ReScript - I absolutely love the idea of it. I’ve been through more languages than I can recall, but Ocaml isn’t one of them. For me, ReScript is a mix of Rust and Groovy, sort of.

It resonates with me as I’ve been looking for a more rigorous, concise and efficient language with accompanying toolkit for the Node.js domain.

There are a few things that I either do not understand or have some issues with:

  1. Array mutability
    I don’t understand why arrays are mutable in ReScript. Can someone explain the rationale? I mean, it would make sense for me if they were mutable if marked as such, but otherwise - why not default to immutability both when it comes to assignment but also in terms of array contents?

  2. JS Interop
    Right now, whenever I pull some JS lib into my projects, I have to wade through tons of information. Not because I don’t know the lib API - but because I haven’t memorized each exact type. Just do do a simple app with Mongoose, for example - setting up the schema, connecting to the database, using it properly and inserting or retrieving documents - I have to set up so much typing.
    .
    I understand this is true for TypeScript as well - however there is a significant difference, and that is that I don’t have to setup the typing. I can use it without typing, which makes my life so much more easy. I know there’s some method called connection, I know there is a connection with some readyState on it, I know there’s a findOne method - but I don’t recall their exact signatures. But I recall them to the point where I can use them in JS without ever looking at the documentation. Forcing me to type everything means killing my efficiency by 90% or so. Am I doing something wrong?

  3. Interop verbosity
    I was looking to do some React with ReScript. The experience is, most of the time, better than JSX/TSX. I can probably live with React.string everywhere (although I would prefer to not have it, but I don’t know what the plans on this are). What I can’t live with is seeing something like React.ReactForm.Event.target(e)[“value”] instead of e.target.value. It’s just not feasible for me - it’s not slightly overly verbose, it’s WAY, WAY too verbose for me to live with. I could probably live with e->eventTargetValue (which I know how I can define easily as a layer in between). Am I thinking about this wrong, or what I am missing? I think I would like some kind of “relaxed interop mode” - but maybe that I was %raw is for?
    .
    If this cannot be addressed, I think it might become a sort of friction for the community in terms of adoption.

PS. Thank you for an absolutely amazing toolchain and language. The speed of the compiler and the simplicity of setting everything up, and the fact that you can go file-by-file when migrating makes it so much more easier to use.

1 Like

Actually as I am going along, ReScript unfortunately becomes more and more unsuable to me. I must be doing something wrong. I’m spending literally 90% of the time trying to figure out how to type out bindings for JS libs that I know by-heart how to use, but don’t know the typings of.

I think I urgently need help on this to progress - I can’t see myself doing this much more. I’ve literally spent two hours trying to set up MongoDB typings/bindings.

Bindings are required to tell the compiler what you’re using and prevent mistake (if your binding is correct of course)

Its not that complicated, just bind functions your using, not the whole lib, and if you really know your lib by-heart, are confident enough and don’t want to waste more time with binding, you could just wrap your js/ts code in a simple function.

ex:

// mongoose-setup.js
import mongoose from "mongoose";

main().catch((err) => console.log(err));

export async function main() {
  await mongoose.connect("mongodb://localhost:27017/test");
}

then

// App.res
@module("./mongoose-setup.js")
external main: unit => unit = "main"

Welcome to ReScript and the ReScript forum!

1. Mutable arrays:

This is for convenience and easier JavaScript interop. If you care about safety, use Belt.Array instead of Js.Array, because all its methods create new arrays. Personally, I always use Belt’s methods and never waste a thought about me mutating an array. Also, they would get less usable when you really need the performance and use an imperative for loop (and Array.push).

2. JS interop:

Learning to write bindings is the hardest part of ReScript. I suggest you have a look at some projects, most of them keep their bindings in a bindings folder. To quickly find out what a decorator does, the syntax lookup is a handy tool. There is also the bindings-cookbook. And I found some MongoDB bindings you can at least use for inspiration.

3. Interop verbosity:

I agree this is somewhat tedious. I guess I just got used to this verbosity. But there are escape hatches, which are useful for quick prototyping. Note that you bail out of the type system (!), which is not recommended. You can wrap your event in Obj.magic and then access arbitrary values like so:

let onClick = event => {
  Obj.magic(event)["target"]["value"]
}

Again, in my view it is still better to just get used to this verbosity. Personally, I use a ReactUtils file, where I put some helper functions. I then open ReactUtils almost everywhere, since it also contains a shorter version of React.string. Last but not least, you can also use a module alias:

module RFE = React.ReactForm.Event
RFE.target(e)["value"]

Conclusion

Thank you for this post. I think we long-time users get blind for this things. We also desperately need a simple platform where we can share (incomplete) bindings. Like github gists, but just for ReScript bindings, where you just copy them into your project (and can then tweak it to your liking).

7 Likes
  1. Re. mutable arrays:

Thank you, that makes sense to me. Basically stop using Js.* unless needed!?

  1. Re. JS interop

Thank you - I found those very bindings.

One thing that I am wondering is what style of bindings are preferred and in what context?

For example, I could do bindings using just type definitions and then annotate imported JS objects with them:

module Models = {
  type t = {
    @as("User")
    user: Model.t<User.t>,
  }
}

module Conn = {
  type t = {readyState: int, models: Models.t, someFunc: int => unit}
}

@module("mongoose") external connection: Conn.t = "connection"

I can then do connection.readyState or connection.models.user, etc. Is this bad practice?

To me, it looks more “ReScripty” (though I obviously lack the competence to assess that as of now) than doing @send, @external and so on all over the place (see bindings you sent).

Thoughts!?

  1. Re. interop verbosity

Thank you, I didn’t know about Obj.magic. I don’t think this is the right use case, though. Could something like @types be done for ReScript? So you would have npm i @rescript/react and @rescript-types/react, which would give you the proper bindings.

Right now, the main point of friction is (as you also point out) proper bindings. I miss the simplicity of untyped code (e.g., e.target.value) for things that I’ve done two million times and just know will work (until I encounter some bug that I have no idea how it got into the program… ;-)).

ReactUtils is a smart thing to do, but I think it increases the need for boilerplating everything you do again and again and again for every project. That’s sort of an issue for me that I need to figure out how to address. Though I also understand that you can’t have both - type safety and the simplicity of everything being untyped, it’s obviously a logical impossibility. Still, I would like to be able to do something like let value = e.target.value to get rid of the verbosity.

I think it’s unfortunate we have to introduce so much verbosity even in places where it definitely isn’t needed. That’s a ReScript wart from my perspective - but maybe it can be addressed with a @rescript-types kind of setup.

1 Like
  1. I recommend it for Applications, Belt has some added runtime, whereas JS functions are mostly what is already included in JavaScript. For libraries you may not want that extra bloat.
  2. Some ways to write bindings stem from the days where records did not compile to objects, but arrays. Interop was more of a hassle. Your suggestion is fine.
  3. I think it is one of the more valid uses of it, because the object access notation (["target"]) is not typesafe itself anyway. Both are escape hatches, that you don’t need when you tweak (or write) the bindings yourself. I think you now see the tradeoff between type-safety and convenience. Most installable binding libraries rather have typesafe and verbose bindings.
1 Like

I think this reaction is quite common, and I echo the thoughts of @fham . Bindings are difficult to get started with (we really need to improve the learning experience around them). However, once you do know how to do them, I’ve found the binding system brings quite a lot of nice perks that I personally value.

One example is that rolling your own API flavor of the underlying API is quite easy, and with little (zero?) cost. A given JS/TS API can be bound to in a number of ways using ReScript. It doesn’t have to be bound to look just like the underlying API. Maybe the API you’re binding to is a bit unintuitive? Or it contains a bunch of options and things you don’t want to have to care about. Rolling your own flavor is quite simple.

Let’s take a very minimal example. Imagine we’re reading files using Node fs.readFileSync. But, we’re only ever going to be reading with utf-8. So, while the JS API would be fs.readFileSync(somePath, "utf-8") we could easily do a binding that hides the second parameter, with no additional cost:

@val @module("fs") external readFile: (string, @as("utf-8") _) => string = "readFileSync"

let myFileContents = readFile("./whatever.txt")

(read about the @as decorator here: Bind to JS Function | ReScript Language Manual)

The example above “cleans up” readFileSync a bit for our specific use cases, by renaming the function name (don’t care about the Sync suffix), and inlining "utf-8" so that’s always passed to the function without us needing to care. We interact with the function like it’s called readFile and it only takes a string path argument. And the compiler puts the right thing in the generated JS, with no additional cost or abstraction on top.

7 Likes

ReScript does have an immutable basic data structure, list, but it compiles to nested objects so Array is what we recommend most people stick to. Unfortunately, as they compile to JS arrays, they’re mutable at some level no matter what you do.

The interop verbosity story is likely to improve over time. The main reason I see for bindings to avoid the e.target.value style is that it makes dealing with optional properties a huge pain.

ReScript v10 improves this, but it isn’t out yet and there’s a lot of existing bindings that can’t be changed without a breaking version update.

2 Likes

Writting bindings for a library written in js is very easy in rescript than other languages compiled to js, you even do not need to read the documentation, just typing external in the vscode, then you will get tips for externing the library.
image

1 Like