Set or Dictionary with embedded keys?

I want to make a collection, like a set or dict, where the keys are embedded in the items. For example, suppose there is a Person type with a social-security field. Notice that when I add a person to the collection I need to specify the SSN even though it is part of the Person record. This isn’t that big a deal but it is possible to accidentally specify the wrong SSN (one that doesn’t match what is in the record). It would be cleaner if I could just do something like Set.add(mike) and it would know to use the SSN field in the record as the “key” and I could look up people only using that key, like Set.get("561"->SocialSecurity.makeExn). Is this possible with the built-in Belt collections? I see a collection called SetDict and thought maybe it would work for this scenario but I can’t figure it out. I also thought maybe I could use the normal Set but it seems like if I put a whole Person in then when I query I need to query for a whole Person, not just the SSN. Part of the problem is I’m a little confused by the whole MakeComparable thing; I thought I could do a MakeComparable on Person and only compare the ssn, but I can’t figure out how to use one of these created modules to make a Set or Dict that takes a whole Person when inserting data, and allows querying just with the SSN.

open Belt

module SocialSecurity = {
  @unboxed
  type t = Ssn(string)

  let make = s => {
    let s = s->Js.String2.trim
    if s->Js.String2.length < 10 {
      None
    } else {
      Some(Ssn(s))
    }
  }

  let makeExn = s => s->make->Option.getExn

  let cmp = (a: t, b: t) => Pervasives.compare(a, b)
}

module SocialSecurityCmp = Id.MakeComparable(SocialSecurity)

module Person = {
  type t = {
    ssn: SocialSecurity.t,
    name: string,
    age: int,
  }

  let ssn = p => p.ssn
}

let bob: Person.t = {ssn: "561"->SocialSecurity.makeExn, name: "Bob", age: 32}
let mike: Person.t = {ssn: "976"->SocialSecurity.makeExn, name: "Mike", age: 12}

let addressBook =
  Map.make(~id=module(SocialSecurityCmp))
  ->Map.set(bob->Person.ssn, bob) // specify SSN even though part of record
  ->Map.set(mike->Person.ssn, mike) // specify SSN even though part of record

let findSomeone = addressBook->Map.get(SocialSecurity.makeExn("561"))

Perhaps you can define a helper function to add to the set? E.g.

let addContact = (addrBook, person) =>
  addrBook->Map.set(person->Person.ssn, person)

This way you don’t need to worry about the possibility of a mismatched entry.

If you want of course you can also put this function in a module that makes the ‘address book’ set an abstract type and prevents using plain Map.set so there’s no possibility of error.