why does wrapping functions in iife cause weak types?

100 views Asked by At

I'm trying to figure out a way to hide certain helper functions and related stuff from the user of a module and thought using an IIFE would work, yet it fails because a type variable cannot be generalized?

I think I've boiled it down to the most basic scenario with the following code:

module TestA = {
  let y = 0;
  let x = (type a, numbers: list(a)): option(a) => None;
};

module TestB = {
  let x =
    (
      () => {
        let y = 0;
        (type a, numbers: list(a)): option(a) => None;
      }
    )();
};

In TestB the compiler complains with

  41 │ };
  42 │ 
  43 │ module TestB = {
  44 │   let x =
   . │ ...
  50 │     )();
  51 │ };
  52 │ 
  53 │ module Number = {

  The type of this module contains type variables that cannot be generalized:
  { let x: list('_a) => option('_a); }

  This happens when the type system senses there's a mutation/side-effect,
  in combination with a polymorphic value.
  Using or annotating that value usually solves it. More info:
  https://realworldocaml.org/v1/en/html/imperative-programming-1.html#side-effects-and-weak-polymorphism

why is that? And how could I approach the problem of hiding y from the user of the module?

P.s.: When reformatting the return type annotation in TestB gets put behind the None like so: (type a, numbers: list(a)) => (None: option(a)). Why here and not in module TestA? As far as I understood it this just "tags" the returned value, so I don't see a difference here?

1

There are 1 answers

0
Yawar On BEST ANSWER

You hit what is called the value restriction. In short, the compiler thinks there may be potential side effects happening inside the closure that might unsafely change the return type of the function. So it can't safely decide on a return type.

Fortunately, ReasonML has an easy, idiomatic replacement for IIFEs–brace-delimited scopes:

module TestA = {
  let x = {
    let y = 0;
    numbers => None;
  };
};

This scope hides y inside the definition of x.

The more general way to hide items in a module is by giving the module a signature that simply does not list the hidden item(s). In this case it would look like:

module TestA: {
  let x: list('a) => option('a);
} = {
  let y = 0;
  let x = numbers => None;
};

Another way would be to use an 'interface file', e.g.:

// TestA.rei
let x: list('a) => option('a);

// TestA.re
let y = 0;
let x = numbers => None;