Use module type returned from functor in another file

I’ve created the following functor PairedRefs.Make which returns type BindRefsTo:

// PairedRefs.res

module type RefsPairings = {
  type t
  type view
  let view: t => view
}
module type AudioRefs = {
  type t = AudioElementTracking.AudioRefList.t
  let refs: t
}
// I'd like to use this type wherever I create modules from the Make functor
module type BindRefsTo = {
  type refsPairing<'a> = 'a
  type audioRefPair<'a> = (AudioElementTracking.AudioRef.t, refsPairing<'a>)
  type t<'a> = (audioRefPair<'a>, audioRefPair<'a>)
  type view<'a> = (audioRefPair<'a>, audioRefPair<'a>)
  let make: ((refsPairing<'a>, refsPairing<'a>)) => t<'a>
  let getByRef: ((('a, 'b), ('a, 'b)), 'a) => ('a, 'b)
  let view: 'a => 'a
}
module Make: (
  AudioRefs: AudioRefs,
) =>
BindRefsTo

When I try to leverage the type in another file, I get no errors when assigning the type, but I do get errors when using it:

// Stream.res

  // No error here, and on hover in VS Code `PairedRefs` shows the correct type
  module type PairedRefs = module type of PairedRefs.Make
  type t = {
    // This next line errors out: "The module or file PairedRefs can't be found."
    mutable streams: option<PairedRefs.t<option<Stream.t>>>,
  }

I’ve also tried include PairedRefs.BindRefsTo in the Stream.resi file, which didn’t error, but also didn’t help.

Edit: One other thing I tried was to just open the PairedRefs module in Stream.res and use the type BindRefsTo, and got the following results:

  • Using the unmodified code above, it made no difference. Code in Stream.res still couldn’t see the module type BindRefsTo
  • If however I changed the functor into a normal module and made BindRefsTo just the normal module type, code in Stream.res can see the module type just fine, which is strange!

I can forgo using a functor here just to keep moving, but I’d still be interested to know the reason for the inconsistency in module type visibility when dealing with regular module types vs. functor return types!

This error message seems incorrect to me, but there are a few other issues with the code that I can see.

module type PairedRefs = module type of PairedRefs.Make

One problem is that a module type is just a specification for a module. It doesn’t actually contain any types or values, only a real module does, so PairedRefs.t does not exist. A functor doesn’t contain any types or values either until it’s applied, so PairedRefs.Make.t does not exist either.

You need to apply a functor in order to use its types, example:

module X = Y.Make(Z)
type t = {foo: X.t}

However, your Make functor doesn’t seem to actually generate any new types, so why do you need to reference it in a type definition? It looks like your BindRefsTo.t<'a> type could be completely moved out of the BindRefsTo module type. If you statically define that type inside a regular module, you would be able to easily reference it anywhere. (Also, since its definition is just tuples, which are structural, you could redefine it in multiple places and the types would still be compatible.)

1 Like

Thanks for the reply!

One problem is that a module type is just a specification for a module. It doesn’t actually contain any types or values, only a real module does, so PairedRefs.t does not exist.

I think I forget this, thanks. It’s strangely easy for me to forget since the vast majority of the time a module type is sitting right next to its corresponding module.

You need to apply a functor in order to use its types, example:

module X = Y.Make(Z)
type t = {foo: X.t}

I’d like to unpack this a little to understand it better. I started off with this approach, but wasn’t clear on how to reference, to use your example, X.t's type further up in the file. Here’s a simplified version of what I was confused by:

...setup state record

type state = {
  refsFromNewModule: <what type goes here if from a newly generated module below?>,
}

let updateState = refsFromNewModule => {
  state := {
    refsFromNewModule: refsFromNewModule
  }
}

let createModuleFromFunctor = () => {
  module NewModule: {
     type t
     let test: t
  } = MyFunctor({
    type t = string
    let test: t = "this is a test"
  })
  // Since NewModule was generated here, how will anything outside of this function understand this type?
  updateState(NewModule.test)
}

However, your Make functor doesn’t seem to actually generate any new types, so why do you need to reference it in a type definition? It looks like your BindRefsTo.t<'a> type could be completely moved out of the BindRefsTo module type. If you statically define that type inside a regular module, you would be able to easily reference it anywhere. (Also, since its definition is just tuples, which are structural, you could redefine it in multiple places and the types would still be compatible.)

I think this is ultimately the answer for my situation :pray:, so I think I’m good. Just still generally curious how one handles passing around types for modules generated from functors if it had been a new type I was generating.

The short answer is that you can’t. Just like any type, you can’t reference it until after it’s been declared (in other words, until lower in the file). For your example, it seems like you would just use a polymorphic 'a variable, e.g. type state<'a> = { resFromNewModules: 'a}.

Generally, a functor will create new types every time it’s applied (creating new types is one of the main reasons to use functors, versus just a record of functions). Consider this toy example:

module type Arg = { type t }
module type S = {
  type t
  let empty: t
}
module MakeList = (M: Arg): (S with type t = list<M.t>) => {
  type t = list<M.t>
  let empty = list{}
}
module IntList = MakeList({ type t = int })
module StringList = MakeList({ type t = string })

IntList.t and StringList.t are incompatible types. There’s no concrete "type returned by MakeList" that exists, because the type will always be different for each functor application.

I would personally argue that creating new types is the only reason to use functors 99% of the time. And, except for very advanced and rare cases, you do not want to create new types inside of a function (e.g. apply a functor). Doing this is possible, and sometimes useful, but it will make your code very complicated and hard to maintain. I would try to solve your problem with a basic record type first, and then “upgrade” the record to a module/functor if you need their extra power.

To answer your question about passing around a type, though, the keywords you should search for is “first class module” and “locally abstract type.” I’m not sure if those are documented on the ReScript site yet, but they’re OCaml features that ReScript has. They are pretty advanced, and will definitely create some hard-to-debug type errors until you get the hang of them.

There are a few posts about them on this forum. Here’s one from a while ago:

1 Like

Okay, that all makes sense. I’m pretty certain I am thinking of functors as a sort of “factory function” rather than a module type generator. Maybe the better way for me to think of functor return types is as templates for a final TBD module type.

I am a little familiar with first class modules, but I could definitely stand to understand locally abstract types better. I’ll read up on them. Thanks!