Generating border types

Here are some thoughts I had while writing code in Elm and Reason around ports:

https://www.notion.so/bloombuilt/A-script-to-generate-both-Reason-and-Elm-types-for-ports-5177b1c921c74d55a05be797ef4a43bf

@dillonkearns gave feedback on slack and I wanted to carry on the conversation here.

To quote Dillon:

Some feedback: using yaml to generate the types seems to be missing the opportunity to create something that offers true type-safety that crosses the borders here. That’s what I attempted to do with GitHub - dillonkearns/elm-typescript-interop: Generate TypeScript declaration files for your elm ports! by generating types based on the actual ports that you define in your Elm code. So it is guaranteed to match up, you can’t make a typo or forget to change the yaml file when you update the types somewhere, and then get a runtime error :scream:. (I’m still working on some webpack integration that would allow you to build it in more seamlessly to build as part of webpack so you can’t forget to rerun the script so it’s even more fail-safe). Is there a reason that the approach you’re considering is using a file that describes the types rather than looking at the type signatures for the ports?

I’d be interested in taking the parsed AST from my elm-typescript-interop package and generating something for type-safe ReasonML interop. (It might make sense to name the package elm-typesafe-interop or something then). Thoughts on that approach?

I’ve been thinking about ways to generate unions as well for this package, I’m not sure how feasible it would be but I’ve got some ideas on how that might be done. But for now I’m still polishing some of the basics to make the package more robust and easy to use.

I haven’t dug into ReasonML yet, but it is really tempting to consider writing some of the things I need to write in JS using an ML-family language rather than TypeScript! It seems like it could be a really nice pairing for Elm interop.

So I’d probably need a little guidance with the ReasonML-side, and I may be missing something there, but I’d be interested in experimenting with it!

this feels remarkably similar to JSON Schema. If you’re going to generate code, it’s already got a well-specified interchange format (not just for JSON, you can write 'em in YAML or whatever)

If you wanna go big, why not Dhall?

Thanks for the good feedback, Dillon! Here are my thoughts.

If I understand it correctly, the approach that elm-typescript-interop takes reads the Elm port declarations and generates typescript definitions for use on the TS side. That’s great for when you only want to send supported types through the port!

The approach that I’ve liked taking for interop uses few ports, maybe just an in-out port pair for a specific “theme” of tasks, and uses an ADT to represent the things that can be done through that port.

So, for example, let’s say I’m making an Elm module with ports that will allow interaction with the browser’s client-side storage. I’d have a port for messages going out, and a port for messages coming in. Then I’d have two ADTs, one that describes the messages I can send out, and one that describes the messages that can come back in. (I know you already know about this because of my Elm conf talk, and we’ve talked about it in-person, I’m just writing it here for clarity).

So the ADT going out might look like:

type MsgToBrowserStorage
    = StoreItem {key: String, item: Json.Encode.Value}
    | LoadItem {key: String}
    | ClearItem {key: String}

And I’d have a similar one for messages coming in:

type MsgFromBrowserStorage
    = LoadedItem {key: String, item: Json.Encode.Value}

I feel like these ADTs make for a nice API on the Elm side. The painful part is that, since ADTs aren’t supported in ports, we’ve got to write encoders and decoders for these messages. And we’ve got to write those encoders & decoders on both the Elm side and the host language side (Reason, TS, Flow, whatever).

So for this specific workflow, generating the types from the port definitions doesn’t quite fit the bill.

There are a couple of reasons why I reached for something like YAML:

  • The parser is already written and usable in whatever language the CLI will be written in.
    (VS trying to re-use the Elm parser somehow, or re-write the parser by hand)
  • It’s clear to the user where they should write the code for the generator to read
    (negative example: if the format used to describe the types we wanted generated were Elm types, we’d have to figure out the UX for the user to specify where those types live, and which types in the file should be generating code VS which types should be left alone)
  • It’s clear to the user what values they can use with the generator.
    (VS just writing an Elm type and then discovering after you’ve written a bunch of stuff that you didn’t understand the lib very well, and the types you wrote aren’t supported)
  • When the code that describes what will be generated is edited in the YAML file (or whatever format), it’s very clear to the user that they are making changes to both Elm code and host language code. It forces the user to think about the border between the two worlds at that point, considering what would work well for both languages, instead of just thinking in Elm-world the whole time.

Now that I’ve written those reasons out, I find them less compelling than they were when they were just sitting in my head, except for the first. It’s still very compelling to me that we wouldn’t have to write and maintain a copycat parser.

Also, now that you’ve brought it up, I think it’d be a great idea to make this pluggable, so that a plug for any host language could be written, not just Reason and TS.

What are your thoughts on all of that?

Also, @brian, JSON Schema is a great idea. I didn’t think of that.
Also you just blew my mind with Dhall. Stack overflow. Brain melting.

I have a few thoughts to add about using a separate system like YAML to define types:

  • It’s reasonable to think that someone writing Elm will want types generated for their backend too(typescript, reason, etc). Generating types from something like YAML will let people use this tool for both ports and backends.This is also applicable to making this pluggable to allow any host language.

  • Having the types defined in a central place, rather than automatically based on the port types, can eliminate some of the “magic” that might be confusing to people who are trying to use this. IMO, it seems more straight forward to have a config file to define your types, rather than automatically take them from your elm program

Those points, plus some of the points that @splodingsocks mentioned, are compelling to me.

I also like the idea that @dillonkearns is saying about true type safety. A possible solution: specifying to a list of Elm files that define/export types, specifying which types you want to use from those files and generating the host language types/decoders from there. That would allow for completely type-safe method, but doesn’t limit to what you declare as ports.

The drawbacks that I see to this idea are:

  • “Trying to re-use the Elm parser somehow, or re-write the parser by hand” - Murphy
  • You still have to write your elm types by hand (and decoders?), then generate the host language types from there, so it’s possible for the types to be out of sync. If the YAML approach is taken, then the types are generated at the same time and the types will always be in sync.