I’m trying to write a simple binding to the CRDT library automerge and I’m having some trouble figuring out how to type the change function nicely. I’ve looked around for some other ReScript libraries that use Proxy objects like this, and haven’t found anything, but here is what I have so far:
I don’t think ReScript is suitable for Immer or Autometgecs mutable api. In the case of immer I would drop it’s use in a ReScript project in favour of functional immutable updates. Does something similar exist for automerge?
No, Automerge uses Proxies (like immer…) to create a list of change operations which is what gets saved in the CRDT. There isn’t an immutable interface that uses diffing or something like that. Inlining the JS as change handlers isn’t terrible but it is unfortunate. Is there a way to trick the compiler into treating a local object like a JS object to allow mutability?
Hi. Did you consider using records with mutable fields? Here is what I came up with:
module Automerge = {
module Document = {
type t<'a>
}
external get: Document.t<'a> => 'a = "%identity"
@module("automerge")
external make: 'a => Document.t<'a> = "init"
@module("automerge")
external change: (Document.t<'a>, @uncurry ('a => unit)) => Document.t<'a> =
"change"
}
type person = {mutable name: string, mutable phone: string}
let raw = {name: "Scott", phone: "000000000"}
let doc1 = raw->Automerge.make // Automerge.Document.t<{ "name": string; "phone": string }>
let doc2 = doc1->Automerge.change(draft => {
draft.name = "Scott Trinh"
draft.phone = "111111111"
})
let check = (doc2->Automerge.get).name == "Scott Trinh" // true
I’m not quite happy about the external get: Document.t<'a> => 'a = "%identity" hack though. If automerge has a function for extracting regular unwrapped object from a document, maybe it’s better to use that instead of %identity. Also, would be good if get would return an immutable object — strip mutable from the type somehow — but I’m not sure how to do that.
Oh, this is actually quite good! I’m fine with the hack here, although, as you say it’d be good if it was mutable only within the change callback. I’ll play around with this and see if I can come up with something there to avoid the external record being mutable.
Following on here, is there anyway to restrict the value type to be based on the value of the original record/object type at that key? Like in pseudo-code: (t<'a>, 'key, 'a['key]) => unit?
@rpominov’s solution with the mutable fields looks great, but if you wanted to avoid the mutable fields, then maybe you can use @set_index to enable mutations, e.g.:
module Person = {
type t = {name: string, phone: string}
%%private(@set_index external set: (t, string, 'a) => unit = "")
let setName = (person: t, name: string) => set(person, "name", name)
let setPhone = (person: t, phone: string) => set(person, "phone", phone)
}
And your callback becomes:
let doc2 = doc1->Automerge.change(draft => {
draft->Person.setName("Scott Trinh")
draft->Person.setPhone("111111111")
})
Looks like there is a @set/@get decorator that does exactly this! Having to define the getter and setter in the binding is an unfortunate amount of Java-esque boilerplate, but I’m happy with the output and the level of type safety here!
You can look at the JS output in the Playground link I posted there. Seems to be doing the correct thing. Also seems to match the documentation that I linked to which looks like this:
type textarea
@set external setName: (textarea, string) => unit = "name"
@get external getName: textarea => string = "name"
I think the “string out of thin air” is actually relative to the type passed as the first argument in the getter/setter, so it’s not quite as weird as it seems. It definitely seems to be prone to error (you can pass any string at all in the = "notARealKey") but this is a binding, so it’s going to be a bit unsafe anyway.
Yeah, that’s a good point, too. I want to discourage the mutability of these fields outside of the Automerge change callback, but this doesn’t really do anything to enforce that. I wonder if there is some way to make a functor or something that defines the mutable interface on the base type and uses that type in the change callback instead. I’ll give that some thought, but feel free to suggest something! I’m still prettty new to ReScript.