Parametric polymorphism (discussion)

I don’t see any problems here. In the first case the compiler should use Belt.List.forEach and in the second case, Belt.Array.forEach. There’s no ambiguity:

open Belt.List
open Belt.Array

let items1 = list{1, 2, 3}
items1->forEach(x => Js.log(x)) // Why not using the Belt.List version?

let items2 = [1, 2, 3]
items2->forEach(x => Js.log(x))

however the previous code throws this error:

This has type: list<int>
  Somewhere wanted: array<'a>
2 Likes

A module has a specific scope, and the “last definition” of a type or binding will shadow the previously declared entities.

To demonstrate:

open Belt.List

// from here on, we see `forEach: (list<'a> => unit`) => unit`
let items1 = list{1, 2, 3}
items1->forEach(x => Js.log(x))

open Belt.Array
// from here on, we see `forEach: (array<'a> => unit`) => unit`
// so this doesn't work anymore
items1->forEach(x => Js.log(x))

You always need to think about the “latest definitions” that happened in the module scope, that’s why it is common to scope your open statement to the closest expression. E.g.

let items1 = list{1,2,3}
// Open Belt.List only applies to the scope of the function
let someFunc = () => {
  open Belt.List
  items1->map(x => x + 1)->forEach(x => Js.log(x))
}
5 Likes

I understand but I think that somehow is redundant and verbose. It makes no sense that the compiler is capable to guess infer almost any type but it can’t figure out which function I’m referring to. It’s a bit disappointing.

The compiler doesn’t ‘guess’ the types. It infers them using a formally (mathematically) defined algorithm. What types are inferred depends on the typing environment, i.e. the types and values that are currently in scope.

In this specific case, the typing environment is impacted exactly in the way that Patrick described. Keeping an eye on scoping is an important part of ReScript’s modular style of programming.

I highly recommend watching the lecture videos of this course: https://www.coursera.org/learn/programming-languages --to get a good grasp of how type inference works.

4 Likes

FWIW, something similar does happen with a few specific built-in functions. For example, compare, ==, >, and others will compile to type-specific functions when the types are known, and to generic polymorphic functions when they aren’t. (The polymorphic versions of these generally aren’t considered safe to use, though.)

These are special cases in the compiler, though, and they can’t be extended. If you define a custom type, you’ll always need to explicitly use your custom equality function, e.g. MyModule.eq(a, b).

I have no idea how theoretically possible it would be to make that type of inference happen for everything, but it doesn’t seem feasible to me. Assuming it was possible to define a bunch of forEach values in the same scope without shadowing, it would still be too ambiguous as to which one should be used. Each value could have radically incompatible signatures, or even identical signatures.

But because ReScript requires that every expression have an explicit type anyway, this shouldn’t really be an issue. You know that items1 is a list, and you know that items2 is an array, so you can just write the correct forEach function in either expression. If you’re worried about the code verbosity, you can just alias the modules like module L = Belt.List; L.forEach(items1, f); Asking the compiler to pick a module for you isn’t really an advantage, since both you and the compiler should be in agreement about which one is needed.

It would still be cool if at least we got a suggestion of modules / methods of the possible operations in the scope after typing ->. Dunno how feasible that is.

1 Like

The ability to infer this by the compiler is actually a feature that is being worked on in OCaml and the name is “modular implicits”. Eduardo, a member in our community has an early version of this working (modular explicits), this early version still needs annotation, the next step is to have it fully inferred. However this can take a while before it is in a stable version of OCaml, and ReScript is not interested in staying up to date with OCaml, so don’t expect this to arrive soon :slight_smile:

4 Likes

I think it’s a real shame that ReScript lost the “open with .” functionality from Reason, that makes this “scoped opening” quite a bit easier to get used to.

2 Likes

What is the ‘open with .’ functionality?

EDIT, oh, you mean expression-level open i.e. Module.(foo bar).

1 Like

Yeah sorry, didn’t know what the official name was!

AFAIK it’s being planned in the near future.

1 Like