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.
“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.
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.
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.
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.
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.