Thanks for the clarification about the useAtom
and getters function function…I think I can see more of the issue now.
It seems that the useAtom
will “unwrap” as much as it needs to to get the value…whether by function application, waiting on the promise, whatever. But the getter is a bit different (I will call it get
here). Something like this:
external getInt: atom<int> => int = "get"
external getThunk: atom<unit => int> => int = "get"
external getAsync: atom<promise<int>> => promise<int> = "get"
external getAsyncThunk: atom<unit => promise<int>> => promise<int> = "get"
Yeah the problem is still that you can pass an async thing (aka promise returning thing), to the function that is supposed to be the non-async version. And it compiles fine because unit => promise<'a>
satisfies unit => 'a
(ie promise<'a>
is 'a
)…
Take 2 (a bit better)
I could see something like this (playground). The phantom type keeps you from mixing up values created by the Sync
and Async
module functions.
module Atom = {
type sync = Sync
type async = Async
type t<'value, 'color>
module Sync = {
external int: int => t<int, sync> = "atom"
external useInt: t<int, sync> => int = "useAtom"
external getInt: t<int, sync> => int = "get"
type thunk<'a> = unit => 'a
external thunk: thunk<'a> => t<thunk<'a>, sync> = "atom"
external useThunk: t<thunk<'a>, sync> => 'a = "useAtom"
external getThunk: t<thunk<'a>, sync> => 'a = "get"
}
module Async = {
external int: int => t<promise<int>, async> = "atom"
external useInt: t<promise<int>, async> => int = "useAtom"
external getInt: t<promise<int>, async> => promise<int> = "get"
type thunk<'a> = unit => promise<'a>
external thunk: thunk<'a> => t<thunk<'a>, async> = "atom"
external useThunk: t<thunk<'a>, async> => 'a = "useAtom"
external getThunk: t<thunk<'a>, async> => promise<'a> = "get"
}
}
It works pretty good except for when you pass an async thing to one of the sync modules. E.g., I can still pass a promise
returning thunk to the Atom.Sync.thunk
.
let x: Atom.t<Atom.Sync.thunk<promise<int>>, Atom.sync> = Atom.Sync.thunk(async () => 1)
let y: promise<int> = Atom.Sync.useThunk(x)
Which is definitely wrong at runtime as the library will “unwrap” that and y
should be int
as you say. But at least there you can clearly see something weird has happened if you look at the types…promise
mixed in with Sync
…oh something bad must have happened. Of course, it is still on the user to notice that.
Edit: Actually, I just thought of a different approach.
Take 3 (pretty good, if you don’t mind the functors)
The main problem is that you sync/async matters to the rescript type system, but the useAtom
and get
functions on the jotai side will do their thing regardless. The main problem was that you want to make sure you don’t send a promise to somewhere that can’t handle it properly. But the problem was the old thunk
function (eg (unit => 'a) => atom<unit => 'a>
), would happily take a unit => promise<'a>
thunk, then the useAtom
functions would return types that didn’t reflect what jotai was doing.
So, just don’t write any polymorphic bindings that take 'a
. Make them all work with concrete types. Obviously, this would be really tedious to do manually, and, what if the user wants to use custom types, or records, or something you the library author can’t know about? Well, how about using functors. You can generate some common cases, and then let the user do whatever else they need to.
playground
module type T = {
type t
}
module Atom = {
type t<'a>
module MakeAtomExternals = (T: T) => {
module Sync = {
external atom: T.t => t<T.t> = "atom"
external use: t<T.t> => T.t = "useAtom"
external get: t<T.t> => T.t = "get"
external atomThunk: (unit => T.t) => t<unit => T.t> = "atom"
external useThunk: t<unit => T.t> => T.t = "useAtom"
external getThunk: t<unit => T.t> => T.t = "get"
}
module Async = {
external atom: promise<T.t> => t<promise<T.t>> = "atom"
external use: t<promise<T.t>> => T.t = "useAtom"
external get: t<promise<T.t>> => promise<T.t> = "get"
external atomThunk: (unit => promise<T.t>) => t<unit => promise<T.t>> =
"atom"
external useThunk: t<unit => promise<T.t>> => T.t = "useAtom"
external getThunk: t<unit => promise<T.t>> => promise<T.t> = "get"
}
}
// Include as many basic types as you want.
module Int = MakeAtomExternals({
type t = int
})
module String = MakeAtomExternals({
type t = string
})
module Float = MakeAtomExternals({
type t = string
})
module IntArray = MakeAtomExternals({
type t = array<int>
})
}
// These work fine.
let x = [1, 2, 3]->Atom.IntArray.Sync.atom->Atom.IntArray.Sync.use
let y =
[1, 2, 3]->Js.Promise.resolve->Atom.IntArray.Async.atom->Atom.IntArray.Async.use
let x' =
Atom.String.Async.atomThunk(async () => "yo")->Atom.String.Async.useThunk
let y' = Atom.String.Sync.atomThunk(() => "yo")->Atom.String.Sync.useThunk
// This won't work
// let oops = Atom.String.Sync.atom("yo")->Atom.String.Async.use
// And finally the dreaded sending async stuff to sync....fails to compile!
// let oops = Atom.String.Sync.atomThunk(async () => "yo")->Atom.String.Sync.useThunk
// Library consumer can use the functors on their custom types, too.
module Person = {
module T = {
type t = {name: string, age: int}
}
include T
module Atom = {
include Atom.MakeAtomExternals(T)
}
}
let person =
{Person.name: "Ryan", age: 35}
->Js.Promise.resolve
->Person.Atom.Async.atom
->Person.Atom.Async.use
// won't work either
//let oops = Person.Atom.Sync.atomThunk(async () => {Person.name: "Ryan", age: 35})
See how mixing up the sync and async are now compile time errors?
Of course, I’m still not exactly sure how the getters and setters work, but maybe you can take the functor approach and get closer to what you need.