A possible Userland method of reducing Newtype boilerplate for functions that are not part of a typeclass

Hi!
I’ve been recently learning purescript, and I have learnt about the Orphan Rule in Purescript, and specifically how orphan instances are fully disallowed in PS.

I, for example, wanted to deserialize/serialize a record with Argonaut, containing a UUID, and sadly purescript-uuid contains no implementations for Argonaut, meaning my only options were an orphan instance or a newtype.

No problem, I thought, I can simply derive newtype instance and be done with it!

Sadly, newtype deriving only works on typeclasses, and most useful functions in the uuid library are not within typeclasses, meaning I was out of luck.

I learned I could manually lift the function signatures over the newtype using the Applicative and other typeclasses, but that meant manually deriving and applying the transformation, which being a programmer, needlessly tried to automate.

So I built this:

It’s a typeclass that will automatically lift any function/value over a newtype, through any functors, bifunctors, profunctors, records, tuples, functions, you name it!

Therefore the usage is as such, with my UUID example:

module Data.UUID.Newtyped (UUID(..),emptyUUID, genUUID   ,parseUUID ,genv3UUID ,genv5UUID ,toString) where

import Data.Eq
import Data.Newtype
import Data.Newtype.Lift
import Data.Ord
import Data.Show

import Data.UUID as X


newtype UUID = UUID X.UUID
derive instance Newtype UUID _
derive newtype instance Show    UUID
derive newtype instance Eq      UUID
derive newtype instance Ord     UUID

lift' = lift @UUID @(X.UUID)

emptyUUID = lift' X.emptyUUID
genUUID   = lift' X.genUUID
parseUUID = lift' X.parseUUID
genv3UUID = lift' X.genv3UUID
genv5UUID = lift' X.genv5UUID
toString  = lift' X.toString 

Which is still a bit boilerplaty, but certainly better than before.

The use of instance chains means I find it kinda difficult to extend this system for any other types for which I provide no implementation, but I thought it was a start.

I post here to get some feedback on this code, whether someone has already done this, and if not, if it’s any good to publish as a small library!

I appreciate all constructive feedback. Cheers!

2 Likes

I don’t think that this is a good reason to use a new type UUID wrapper all over your application.

What would be the more idiomatic solution?

The only other alternative I can think of is doing a custom serializer and deserializer for every model in my application, since all of them use a UUID for their primary key.

Yes. Another method would be having a separate serializable “transport” type in which you convert your app’s type to. Or a better solution for bi-directional json encoding/decoding GitHub - garyb/purescript-codec-argonaut: Bi-directional JSON codecs for argonaut. Implicit encoding is bad.

Interesting. I’ll take that into account in future. Thanks for the pointer!

I like it. It’s a nice solution for the orphan instance problem without all of the wrap and unwrap noise.

Quick question about the end user API. is it recommended to use Explicit type application, like I’m using here, or proxy values? Is it dependent on some other factors?

Furthermore, is there a cleaner way to make a reflexive typeclass? This lift and unlift business makes me uneasy, but I can’t seem to fit a reflexive instance in the instance chain without overlap.

Published the code on github with a couple improvements. Removed the Proxy/Type Applications altogether, and allowed for the wrap and unwrap functions to be fed manually, to allow for lifting and unlifting of values without exposing the Newtype class instance, and followed the Newtype naming convention of using over and under for the function names