I’ve been wondering about the implementation of a library I’m working on. I kind of have a solution but would love comments or if this can be achieved differently.
So this is a media library. What I want users to be able to do is to create Records where there is one mandatory field and one optional field. The rest of the fields are ignored but they are passed down through components so these fields have to be stored so they can be raised as the same type again.
This was my initial idea
newtype Media r = Media
{ src :: String -- mandatory field
, thumbnail :: Maybe String -- optional field
| r -- rest of the fields
}
Now this is cool and all but it has some limitation. Because we are ignoring the fields but still keeping track of them this means I cannot actually mix together two “Media r” types, meaning if I have two different types such as
newtype Video = Video { id :: Int, name :: String, src :: String, thumbnail :: Maybe String }
newtype Image = Image { id :: Int, src :: String, thumbnail :: Maybe String }
the compiler will complain because the Image type does not contain the field name, although it does contain all the other fields.
My solution was the following:
Create the media type
newtype Media =
{ src :: String
, thumbnail :: Maybe String
, json :: Json
}
- Require users to make their type an instance of EncodeJson / DecodeJson
- Pass the type to/from a function encodeMedia/decodeMedia with the required constraints
- Encoding
- Encode the type to Json with encodeJson
- Use toObject to convert the Json to an Object so we get lookup
- Lookup the available keys (src, thumbnail)
- Assign the available keys to the Media type fields (src, thumbnail)
- Assign the original encoded Json to the Media json field
- Decoding
- Simply run decodeJson on the Media.json field. Since it has an instance of DecodeJson, that’s it
Pros
- I can now pass in any type that is encoded/decoded to/from { src :: String, thumbnail :: Maybe String } and the fields between types can vary
- I still have access to the actual Record so I could alter other fields (although now through Json)
Cons
- Might add a performance overhead (I have to do this to every img/video that is passed in)
- Errors are detected at runtime
It is my understanding that I cannot really do the same with records because as soon as I start picking out fields I need to restrict the function to those particular fields and then some polymorphic fields but of course it cannot be two different types of fields.
If anyone has comments on this approach I would love to hear them
1 Like
Would you mind posting an example of what you’re trying to do? I’m not entirely sure what this means. For example, if you had a function that took Media r
as input and/or returned Media r
as output, I would expect you could call that function with either your Video
type or your Image
type (ignoring the coercions that need to happen with all the newtypes). Are you trying to throw a Video
and an Image
together into the same Array or something like that? Seeing the code that generates the compile errors might help us out.
1 Like
Yes exactly. What I’m tryiing to achieve is that I can have e.g. thumbnails for both Video and Image in the same array. This way I could select either a Image or a Video and the handler receiving the type being raised could then decide what to do with the type e.g. how to display it. Both types include a src and a thumbnail but an Image can have different fields than a Video.
I’m also trying to make the code a bit less verbose to work with.
You can see an example with the first approach here
Eventually I want to end up with something similar to Media browsers in e.g. Wordpress etc. Where you can upload an image/video/file. They all have the same display fields in common, src and thumbnail so I can present them in the same way in the browser, but their additional field types may vary.
You can do something similar using a trick around existential types. I only barely understand it myself, I think @hdgarrood has a much better grasp on it. If you define a class for something with a src :: String
and a thumbnail :: Maybe String
, you can use existential types to throw them together into an array. Here is an example where I defined a Shape
class and a Circle
type and a Rectangle
type and threw them together into the same array of Shapes. The trouble is once you throw something into an existential type like that it’s very difficult to get back to the original type. Existential types basically throw away all the extra type information you’re not using. So that might not actually be useful to you, but I thought I’d show it as one possible option.
IMO this is where some form of “typeable” for PureScript would be nice, but that doesn’t exist. You can see some discussion on that here and here.
1 Like
The “standard” FP way to solve this is to create a sum type for all the variants you want.
data Media
= Video { id :: Int, ... }
| Image { id :: Int, ... }
| ...
You can then put them all into a container because they all have the same type Media
, and you can discriminate them. This data type is closed, so if you want to add new things to it, then you have to change the data type. For most cases this is probably OK. The only time it’s not OK is when you want downstream users of your library to be able extend it with their own types. When you need something like that, one solution is purescript-variant
which uses row-types for compositional sum types:
type Video r = (video :: { id :: Int, ... } | r)
type Image r = (image :: { id :: Int ... } | r)
type Media r =
( Video
+ Image
+ r
)
You would write code polymorphic over the tail r
(much like with polymorphic records), and users can instantiate that with their own additions.
Existentials are not often a great solution for this (when you need to get the original back), because they forget type information, and it’s not recoverable.
5 Likes
Nice, thank you @natefaubion it seems that purescript-variant is exactly what I was looking for