reasonbucklescriptrescript

How would I write a generic function to handle multiple record types in ReScript?


Given the following contrived example, is it possible to write a get function that can handle any record with an a property?

type type_one = {a: int}
type type_two = {a: int, b: int}

let example_one = {a: 1}
let example_two = {a: 1, b: 2}

let get = record => record.a

Js.log(get(example_one)) // notice the error here
Js.log(get(example_two))

ReScript Playground

If not, is this possible with an object? Or, what would be the best way to handle this situation?


Solution

  • It's not. Because records are nominally (as opposed to structurally) typed, there is no way to specify "any record with an a field". Therefore get will be inferred to have the last type the compiler saw with an a field, which is type_two.

    There is however the object type, which is structural with subtyping, allowing this:

    type type_one = {"a": int}
    type type_two = {"a": int, "b": int}
    
    let example_one = {"a": 1}
    let example_two = {"a": 1, "b": 2}
    
    let get = (record) => record["a"]
    
    
    Js.log(get(example_one)) // notice no error here
    Js.log(get(example_two))
    

    But beware that there are some trade-offs with using objects instead of records, like not being able to destructure them in patterns.

    Also, as a side note, another way this can be accomplished in some languages is through ad hoc polymorphism, by explicitly defining a common interface and implementations attached to specific types (called type classes in Haskell, traits in Rust). Rescript, and OCaml, unfortunately does not currently support this either, although there is a proposal for OCaml in the form of modular implicits. You can however still define a common interface and implementations using modules, and pass them explicitly:

    type type_one = {a: int}
    type type_two = {a: int, b: int}
    
    let example_one = {a: 1}
    let example_two = {a: 1, b: 2}
    
    module type S = {
      type t
      let getA: t => int
    }
    
    module T1 = {
      type t = type_one
      let getA = (record: t) => record.a
    }
    
    module T2 = {
      type t = type_two
      let getA = (record: t) => record.a
    }
    
    let get = (type a, module(T: S with type t = a), record: a) => T.getA(record)
    
    Js.log(get(module(T1), example_one)) // notice no error here
    Js.log(get(module(T2), example_two))
    

    A bit verbose for this use case probably, but this does come in handy sometimes.