How do you choose a library or strategy for dealing with JS FFI that includes records with optional fields? I believe there are at least 4 libraries that all claim to solve this problem,
and I would not at all be surprised to learn that there are more that I just don’t know about yet. Also there’s the approach of just using the Union
and Nub
typeclasses with pure PureScript records.
Does anybody have any advice on how to choose an approach? Is there any chance that the community could land on a “canonical” approach to this problem and make it easier for other PureScript programmers to discover the “canonical” solution?
1 Like
I’m using purescript-options probably because that’s the first thing I found that solved my problem. Being part of purescript-contrib, I would think it’s the most “canonical” of the listed libraries for this particular problem.
1 Like
I think this really depends on what you are trying to accomplish. There are a lot of awkward APIs out there that attach semantic meaning to a field being missing (rather than existing, but undefined or null). My first suggestion would be to write your FFI binding such that weird stuff like this isn’t leaking into your PureScript code.
If you want the convenience of having required/optional fields, and all optional fields have a default value (as they really really should), then I’d define two functions:
- One that takes the full set of options.
- One that has Row constraints for
Record.merge
ing with defaults.
The reason being that the former has better type inference and suggestions since no constraints are involved.
I personally try to avoid other solutions as best I can, and it’s primarily because I think a lot of the motivation comes from wanting to attach PureScript semantics to JS directly, which is always going to be a bear. I do think option
is the cleanest API with actual PureScript semantics. options
predates all the Row constraint magic, AFAIK. I don’t really like the Union
approach, because you can’t actually write PureScript with those constraints to achieve those results! To me that sounds awfully fishy, though I’ll admit it’s certainly ergonomic. I haven’t used the other ones.
I’ll note, that you can do pretty much anything with enough constraints, but it’s not going to be a great experience. As an example, https://github.com/purescript-web/purescript-web-fetch/blob/master/src/Web/Fetch/Request.purs is a well-typed version of the Fetch API. It’s pretty neat that you can do all the auto-lifting and conversions with typeclass magic, but I wouldn’t want to actually write and maintain a real API like this.
5 Likes
Great answers! Thank you!
I put together another option in this design space, based on the Fetch stuff I linked to.
2 Likes
The untagged-union approach is specifically for JS FFI and almost a literal translation of basic JS/TS types. There’s no runtime cost since it’s just a type coercion.
An advantage of untagged-union vs Row.Union type classes is that the latter only work on the request arguments. If you need to handle either a return type or a callback, you’d have to use a different approach.
Another small advantage is you can omit names. That is, if some option has a type of say, undefined | Buffer | string
, on the untagged union approach, you can leave it as Undefined |+| Buffer |+| String
. When working on ffi bindings for large libraries, especially when codegenerating these, it’s at least one less problem.
The pitfall of untagged-union is it really is just for JS ffi. JS libraries are designed differently from purescript ones. And while exposing records with untagged-unions is most performant since there are no runtime translations - it’s not the most ergonomic to use.
2 Likes