Pass type of Belt Map (String or Int) to functor

TLDR; How do you flexibly use either a Belt.MutableMap.Int<'t> or a Belt.MutableMap.String<'t> in a functor?

Hello. I’m attempting to rework a module into a functor since the only portions that change are:

  • Key: k
  • Val: v
  • The map module doing the heavy lifting: module Map (the sticking point)

Here’s what I’ve got so far:

module type MapBasedStore = {
  type v
  type k

  // **** Breaks in its current form, but really I want this to flexibly be either
  // Belt.MutableMap.Int<v> or Belt.MutableMap.String<v>
  module MapType: Belt.MutableMap.Int
}

module MakeMapStore = (Store: MapBasedStore) => {
  // **** Then this would get set dynamically
  module Map = Store.MapType

  type v = Store.v
  type k = Store.k
  type t = Map.t<v>

  // **** Nothing too important below here but including for context.

  // Saved state
  let tracked: t = Map.make()->observable

  // ...Map get, set functions related to I/O that don't change signatures based on Map type
}

Here’s what I’ve tried so far:

  • Using a variant and switch assignment but I don’t think this works for module assignments. Gave a non-descript error related to an extra curly brace way further down (there wasn’t one):
type mapType =
  | IntMap
  | StringMap

...
module Map = switch Store.mapType {
  | IntMap => Belt.MutableMap.Int
  | StringMap => Belt.MutableMap.String
}
  • Passing the module as a first-class module. For whatever reason, I only ever see examples of this used for destructuring assignments, and I got type related errors if I tried to just assign it directly without destructuring to the functions I needed (i.e- let a = module(Belt.MutableMap.Int with type t=v) // This is likely bad syntax but tried a few variations.... I even tried just going the destructuring route, so something like:
let {
  get,
  set,
  ....etc.
} = module(Belt.MutableMap.Int)

I still ran into type hurdles related to map values. I got closer with this path but I’m skeptical it’s the correct one, but maybe I’m wrong? Would appreciate any pointers!

Needs some more testing but I think I might have it. Fall back to using Belt.MutableMap (not Belt.MutableMap.Int) and supply the types to a comparable module used in the map’s creation in the functor:

module type MapBasedStore = {
  type v
  type k
}

module MakeMapStore = (Store: MapBasedStore) => {
  type v = Store.v
  type k = Store.k

  module Comparable = Belt.Id.MakeComparable({
    type t = k
    let cmp = Pervasives.compare
  })

  module Map = Belt.MutableMap
  type t = Map.t<k, v, Comparable.identity>

  // Saved state
  let tracked: t = Map.make(~id=module(Comparable))->observable
...
}

Using the polymorphic Belt.MutableMap is indeed probably more flexible. You can also solve the problem by passing your desired map module to the functor. Note that modules are structurally typed, so you’ll need to define the types and values in the map you’re using (in MapType here):

let observable = x => x
type whatever

module type MapBasedStore = {
  type v
  module MapType: {
    type key
    type t<'a>
    let make: unit => t<v>
    let get: (t<v>, key) => option<v>
    let set: (t<v>, key, v) => unit
    // etc
  }
}

module MakeMapStore = (Store: MapBasedStore) => {
  module Map = Store.MapType
  type v = Store.v
  type k = Map.key
  type t = Map.t<v>
  let tracked = Map.make()->observable
  let get = Map.get
  let set = Map.set
  // etc
}

module IntMapStore = MakeMapStore({
  type v = whatever // replace this with your desired v type
  module MapType = Belt.MutableMap.String
})

module StringMapStore = MakeMapStore({
  type v = whatever // replace this with your desired v type
  module MapType = Belt.MutableMap.Int
})

2 Likes

Ah, nice. Honestly this approach might end up making more sense. I’ll play around with both!

If you’re using the polymorphic approach, then you can bypass making a functor all together and just have the consuming code send their own Belt.Id.Comparable module. Here’s an example that uses a module interface to hide the implementation:

module MapStore: {
  type t<'k, 'v, 'id>
  let make: (~id: Belt.Id.comparable<'k, 'id>) => t<'k, 'v, 'id>
  let get: (t<'k, 'v, 'id>, 'k) => option<'v>
  let set: (t<'k, 'v, 'id>, 'k, 'v) => unit
  // etc
} = {
  type t<'k, 'v, 'id> = Belt.MutableMap.t<'k, 'v, 'id>
  let make = (~id) => Belt.MutableMap.make(~id)->observable
  let get = Belt.MutableMap.get
  let set = Belt.MutableMap.set
  // etc
}

In general, it’s better to avoid using functors unless they’re necessary. When you do need to use them, make them as “small” as possible (e.g. don’t include every single function in them).

2 Likes

That does look a lot cleaner! I’ve implemented these changes, and generally it looks good and is something I can work with. Since I plan to use this pattern often, the one thing I wish I could relegate to a function is the creation of the comparable, but I suspect that’s not possible since you can only grab the identity when it’s unboxed in module form. For reference, the code calling into the above module:

module Map = Belt.MutableMap

module Track = {
  type k = [#searchText | #autocompleteText]
  type v = string

  // Tracked state
  module Comparable = Belt.Id.MakeComparable({
    type t = k
    let cmp = Pervasives.compare
  })
  // *** The sticking point. I need the module-level
  // reference to Comparable.identity
  type t = Map.t<k, v, Comparable.identity>
  let track: t = ObservableMap.make(~id=module(Comparable))

  // State I/O
  let setText = searchText => MapStore.set(track, #searchText, searchText)
  let getText = () => Map.get(track, #searchText)
}

Aside from the type, the map variable is created using the first-class module version of Comparable, so this could be created in a function. So yeah, really just the one type sticking point. I don’t think there’s a way around it, but figured I’d throw it out there!

1 Like

FWIW, an answer to my last question regarding creating Comparables on the fly for different situations based on reading through the forum, reading code on GH and experimenting.This is based on my current understanding so take it with a grain of salt!

Since Comparable at a minimum requires binding a type, you need a functor to create one, which is what we’ve already got with Belt.Id.MakeComparable. So rather than trying to further abstract this abstraction away, it’s better to not be afraid to repeat the code to create these and occasionally embrace modifying them for some really powerful comparison logic.