Confusing type error with module functors

Hello all!

I’m attempting to build a small runtime using a simple action/reducer pattern but I’m getting a confusing type error: The parameter cannot be eliminated in the result type. Please bind the argument to a module identifier.. I’m unsure how to interpret the error, though it’s something to do with the action type and its usage in higher order functions.

The actual module is bigger than this, but here is the smallest reproducible example:

Link to playground

module Make = (
  M: {
    type action
    type state
    let reduce: (action, state) => state
  },
) => {
  type action = M.action
  type state = M.state
  let reduce = M.reduce

  type effects = ref<array<state => state>>

  let useEffect = (effects: effects, fn) => {
    effects.contents->Js.Array2.push(state => reduce(fn(state), state))->ignore
  }
}

module App = Make({
  type action = SetX(float) | SetY(float)
  type state = {x: float, y: float}

  let reduce = (action: action, state: state) => {
    switch action {
    | SetX(x) => {...state, x: x}
    | SetY(y) => {...state, y: y}
    }
  }
})

2 Likes

Not sure what causes this, I hope someone could explain.

But if you do what the error message asks “Please bind the argument to a module identifier”, it will compile: Playground

Ah, of course, it’s the module that needs binding, I was under the impression that perhaps a function or a type needed to be bound to the module. Thank you for this!

Still, I would be interested to learn what causes this quirk, as it does strike me as odd that an anonymous module causes a type error.

I googled the error, seems to exist in OCaml too. Even the creator of OCaml, Xavier Leroy, had a comment on a github issue about it. Something about types and lexical order…?

The module doesn’t actually need to be bound, although binding the module is one way to fix it.

This issue is that every new type needs a name with a path, for example: Module1.Module2.type1. If you create a new type inside of an anonymous module, and then export that module (via a functor in this case), then the type checker doesn’t have a valid path for that type anymore.

Defining the types outside of the anonymous module, and then aliasing them inside the module, will also fix the problem:

type action = SetX(float) | SetY(float)
type state = {x: float, y: float}

module App = Make({
  type action = action
  type state = state
  let reduce = (action: action, state: state) => {
    switch action {
    | SetX(x) => {...state, x: x}
    | SetY(y) => {...state, y: y}
    }
  }
})

Another way to solve the problem is to simply not export the type implementations by hiding them with a module signature. This will compile:

module type Arg = {
  type action
  type state
  let reduce: (action, state) => state
}

module type S = {
  type action
  type state
  type effects = ref<array<state => state>>
  let reduce: (action, state) => state
  let useEffect: (effects, state => action) => unit
}

module Make = 
  (M: Arg)
  : (S with type action = M.action and type state = M.state) 
  => { /* implementation goes here */ }

The downside to this is that the type constructors won’t be available outside of the module, which probably isn’t what you want in this case.

9 Likes