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

670 Views Asked by At

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?

1

There are 1 best solutions below

4
glennsl On BEST ANSWER

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.