I have an interesting real-world use case at my $dayjob:
I work with a suite of web-applications which interact with each other. For Items created in one upstream app that needs to be made available in a downstream app, the workflow is as follows:
Create the item on the app and store it in upstream app’s database where id for item is generated.
Fetch the catalog of items in upstream app to edit/modify items
Configure it to make it available for downstream app
Fetch the catalog of items in downstream app exposed by upstream app
// Fictional code for my use case
module Item = {
// apps
type a = A1 | A2 | A3
// data
type d
// base
type base = {
name: string,
description: string,
data: d,
createdBy: a,
isAvailableDownstream: bool
}
// for upstream catalog use
type upstream = {
id: int,
name: string,
description: string,
data: d,
isAvailableDownstream: bool
}
// for downstream catalog use
type downstream = {
id: int,
name: string,
description: string,
data: d,
createdBy: a,
isAvailableDownstream: bool
}
}
This could be easily represented (in future possibly) by:
module Item = {
// apps
type a = A1 | A2 | A3
// data
type d
// base
type base = {
name: string,
description: string,
data: d,
createdBy: a,
isAvailableDownstream: bool
}
// for upstream catalog use
type upstream = base & { id: int } ! { createdBy: a }
// for downstream catalog use
type downstream = base & { id: int }
}
Of course, there are some more fields that are added and removed from the base type for upstream and downstream types but, for the sake of communicating the idea I have simplified the use case.
The app I maintain is in ReScript and the others are Typescript or JavaScript.
Currently we maintain all the required type variations in different codebases. For ReScript, it would be syntactic sugar to introduce these field add/remove operations for Records.
Got it. And you don’t need any functions that operate on these (similar but different) types?
Using phantom types would be rather elegant to transform from e.g. base to upstream (and so forth), but I believe you’d then still have the issue of getting the matching record contents for each respective item type. You could potentially combine it with GADTs, but even after that I don’t think you could conveniently spread the contents of one record to another.
Anyway, in your above (albeit simplified) example, you’d at least be able to come a bit closer using the record type spreads (using the alpha or beta release):
module App = {
type t = Foo | Bar | Baz
}
module Item = {
type commonProps = {
name: string,
description: string,
data: string,
isAvailableDownstream: bool,
}
type base = {
...commonProps,
createdBy: App.t,
}
type upstream = {
...commonProps,
id: int,
}
type downstream = {
...commonProps,
createdBy: App.t,
}
}
I couldn’t help but try this, I’m not sure it’s feasible in a real-life application, but fun nevertheless. Also, there’s likely room for improvement.
/* Example use */
let newBaseItem: Item.t<Item.base> = Item.make(
~name="My item",
~description="My description",
~data="foo",
~createdBy=App.Foo,
)
Item.view(newBaseItem).createdBy->ignore /* has createdBy */
let newUpstreamItem: Item.t<Item.upstream> = Item.makeUpstream(
newBaseItem,
~id=1,
)
Item.view(newUpstreamItem).id->ignore /* has id */
let newDownstreamItem: Item.t<Item.downstream> = Item.makeDownstream(
newBaseItem,
)
Item.view(newDownstreamItem).createdBy->ignore /* has createdBy */
let upstreamItem: Item.t<Item.upstream> = newBaseItem->Api.upstreamItem
/* Illegal */
// Item.view(newBaseItem).id /* not available for base */
// Item.view(newUpstreamItem).createdBy /* not available for upstream */
// Item.view(newDownstreamItem).id /* not available for downstream */
// newUpstreamItem->Api.upstreamItem /* not allowed */
// newDownstreamItem->Api.upstreamItem /* not allowed */
Your GADT solution is pretty cool, but I have a question…do you really need the GADT here? Of course, the code you show in the playground is just an example and you may have more functions in mind that really need the gadt…but if you just have the make and the view functions, why not separate the different items into their own types, eg BaseItem, UpstreamItem, DownstreamItem, etc? (Unless you are needing to pack different item types into a collection using an existential…)
Anyway, cool solution, just curious about your intended use.
Mostly because I still think of these as being of the same type, just in different states (and their transition paths are known), so there was an itch to represent them as such.
I think it’s easier to represent these as separate types in a structural language like TypeScript because you could easily have functions that allow you to operate on any of them.
EDIT: And just to see if I understood you correctly, you meant module BaseItem, module UpstreamItem and so on?
Blockquote
Mostly because I still think of these as being of the same type, just in different states (and their transition paths are known), so there was an itch to represent them as such.
Your thought is spot on - they are the same type, in different states for different uses and follow different transition paths. However, I am not familiar with GADTs and there’s no documentation for GADTs in the ReScript Language manual. I might have to start reading up some OCaml documentation.
Once I understand what you intended to achieve with your solution, I might be able to tell you if it was feasible or even proper to use it for the use case I presented.
You could get rid of the phantom types (although, on second thought, I’m not sure they qualify as phantom types in my last example) and ”just” use GADTs, but @Ryan’s suggestion is still likely the most pragmatic approach.
Oh I wasn’t trying to say your GADT approach wasn’t the right one, rather just interested in your use case. But yeah, managing data in different states and their transitions with gadt sounds reasonable to me.
(Yeah I was talking about unique types for each item …baseitem, upstreamitem, etc., but just as a thought experiment rather than a suggestion. In fact, I’m not sure which is the nicest solution given that I don’t know the other constraints.)
@sprkv5: It might be worth opening a new thread about this if you keep going with the gadt approach. Would be an interesting discussion I think.