rescript-compiler icon indicating copy to clipboard operation
rescript-compiler copied to clipboard

Inlined modules in functors can lead to (almost) impossible-to-fix type errors in the call site.

Open mvaled opened this issue 3 years ago • 0 comments

I have a small module to help me create deserializer from JSON while validating the structure received. Basically you define a module with describing your data-schema like this and use MakeDeserializer to create the parser.

The following is cut-down version of the functor and the module type:

module Deser = {
  module type Serializable = {
    type t
    let fields: array<string>
  }

  module MakeDeserializer = (Serializable: Serializable) => {
    type t = Serializable.t
    let fields = Serializable.fields

    external _toNativeType: 'a => t = "%identity"

    /// Parse a `Js.Json.t` into `result<t, string>`
    let fromJSON = (data: Js.Json.t): result<t, _> => {
      Ok("just to show, dont use this value"->_toNativeType)
    }
  }
}

Of the following three (equivalent) modules that use MakeDeserializer, only the last one compiles:

open Belt

module Fails = {
  module Parser = Deser.MakeDeserializer({
    type t = {
      id: string,
      verbose_name: string,
    }
    let fields = []
  })

  let parse = raw => raw->Parser.fromJSON->Result.map(res => res.verbose_name)  // <-- error
}

module AlsoFails = {
  include Deser.MakeDeserializer({
    type t = {
      id: string,
      verbose_name: string,
    }
    let fields = []
  })

  let parse = raw => raw->fromJSON->Result.map(res => res.verbose_name)  // <-- error
}

module Works = {
  module Parser = {
    module Defs = {
      type t = {
        id: string,
        verbose_name: string,
      }
      let fields = []
    }

    include Deser.MakeDeserializer(Defs)
  }

  let parse = raw => raw->Parser.fromJSON->Result.map(res => res.verbose_name) // <-- works
}

The error the two failing versions give is:

The record field verbose_name can't be found.
  
  If it's defined in another module or file, bring it into scope by:
  - Prefixing it with said module name: TheModule.verbose_name
  - Or specifying its type:
  let theValue: TheModule.theType = {verbose_name: VALUE}

You can see it live in the playground.

Trying to annotate the argument to Result.map doesn't help:

module Fails = {
  module Parser = Deser.MakeDeserializer({
    type t = {
      id: string,
      verbose_name: string,
    }
    let fields = []
  })

  let parse = raw => raw->Parser.fromJSON->Result.map((res: Parser.t) => res.verbose_name)  // <-- still fails
}

mvaled avatar Jun 03 '22 07:06 mvaled