Why can't/doesn't ReScript infer the method I want to use?

Consider the following:

module Cat =
{
    type t = { meow: bool }
    let makeSound = t => t.meow ? "meow" : "purr"
}

module Dog =
{
    type t = { bark: bool }
    let makeSound = t => t.bark ? "bark" : "whine"
}

open Cat
open Dog

/* Compiler correctly infers the following types */
let cat = { meow: true }
let dog = { bark: true }

/* Compiler has no idea what I want to do */
let a = cat->makeSound
let b = dog->makeSound

I’m a JS/TS dev that wants the best of both worlds ie. type safety without verbosity, so I’m curious to know why exactly I am forced to always explicitly prepend function calls with the module from which they come when at least in scenarios like this, it ‘should’ be easily inferable from the type of the parameter and the methods in scope. Obviously I’m a beginner, but it would be nice to know why this sort of ergonomic is not available to me so it doesn’t become a big bugbear. This is a particular concern for the popular primitive/array/list/etc functions which I’m of course accustomed to using without explicitly defining where map/filter/etc come from.

I understand my confusion around this is probably a lack of understanding regarding some sort of fundamental nominal type system concept or something, but nonetheless that’s what I’m curious about. I want to be able to make my program work without having to type Belt.Array.map etc dozens of times over and over again in all my files if I can avoid it.

There are some trade off here.
E.g, it is not always clear that the type of cat is already known. So the typing of cat->xx need some ad-hoc rules

“Type classes” don’t exist in the language. The work in OCaml (ReScript is based on an old version of OCaml) to support this is called “modular implicits” but that is still far from making it in OCaml (and even if it would make it in OCaml it would probably not make it in ReScript because ReScript became it’s own language). It would be nice to have but as a production user I didn’t miss this feature to much in practice, it keeps things simple and clear.

Note you don’t need Type classes for the method resolving. You can go with the golang approach

1 Like

ReScript’s type system is very simple (and fast). If you open Cat, it brings everything from Cat into scope. Then if you open Dog, it brings everything from Dog into scope, and Dog.makeSound ‘shadows’ or ‘hides’ Cat.makeSound. Now makeSound refers to Dog.makeSound, simple as that. There’s no (potentially time-consuming) examination about the type of the argument it’s being called with. It simply has no awareness (at this point) that there was a Cat.makeSound, it only knows that makeSound works for Dog.t.

That’s why you need to be careful about opening modules willy-nilly. The compiler even warns about it:

[W] Line 14, column 0:
this open statement shadows the value identifier makeSound (which is later used)

In practice, you should set almost all warnings as compile errors to watch out for issues like this.

2 Likes

I’ve written some programs in F# which is very similar to ReScript. I’m happy to type the module name first. It provides intellisense in VS Code so I can easily pick which function in the module I want to use. It also makes the code easy to understand when looking at it - you’re doing some kind of operation on an Array or Map or Set and without the module name it may not be as clear. “Map.make” is easier to grok than just “make”. If it gets cumbersome you can do an “open” in a limited scope.

3 Likes

Ah ok, would be great to have anything like modular implicits in the language long term, whatever the implementation might be! :eyes:

Just to also respond to this–typically, this can be avoided pretty easily. Use your judgment about how much to bring in scope and where.

E.g., if you have a relatively short module (file) and it is rather heavily using Belt.Array operations, and there is little chance of conflict with other modules, then I’d say do open Belt.Array at the top of the module and bring everything inside it into scope.

If you’re using it heavily but might have conflicts, then you can try opening it inside a limited scope (delimited by braces) so that it doesn’t ‘pollute’ the scope of the entire module.

If you can’t open at all because of potential conflicts, then you can always alias the module to a short name e.g. module BA = Belt.Array and use that convenient short prefix, BA.map etc.

2 Likes

This is what I wanted! Just having more information around how to tackle these sorts of unfamiliar problems is really useful. As a side note I’m still pretty confused as to how to manage the different Array modules available… I’ve sort of resorted to making my own Array module in an attempt to make a unified interface. I have no idea what I’m doing.

Try to stick to the modules listed by the ReScript API page as much as possible: API | ReScript API

So e.g. Belt.Array and so on. You normally would not need to make your own Array module as that would be a pretty advanced use case.

2 Likes