Skip to content

Instantly share code, notes, and snippets.

@maxigit
Created August 7, 2017 22:24
Show Gist options
  • Save maxigit/506d2ce4a9d07c1aef59c59ab8902dcd to your computer and use it in GitHub Desktop.
Save maxigit/506d2ce4a9d07c1aef59c59ab8902dcd to your computer and use it in GitHub Desktop.
Metamorphosis example
This is literate haskell file, so let start with the boring bits
>{-# LANGUAGE TemplateHaskell #-}
>{-# LANGUAGE DuplicateRecordFields #-}
>{-# LANGUAGE StandaloneDeriving,FlexibleInstances, FlexibleContexts #-}
Metamorphosis is meant to be imported unqualified, and uses lenses to configurate default records.
I'll use micro lens.
>import Metamorphosis
>import Lens.Micro
>import Metamorphosis.Applicative
>import Data.Functor.Identity
Rewritten using Metamorphosis, your example will look like this.
First, I found it easir to separate the real type (the valid one which will be used after validation,
from the temporary types needs for the validation).
The main type will be plain User, without type family or anything.
>data User = User
> { name :: String
> , age :: Int
> , active :: Bool
> } deriving Show
We need now a User parametrized by a functor
This is done in metamorphosis b, replacing `User` per `UserF` : `(fdTConsName .~ "UserF")`
and the type `t` of a field, by `f t` : `(fdTypes %~ ("f":)`
We also want to generate an applicative converter. `const (Just applicativeBCR)`
>$(metamorphosis
> ( (:[])
> . (fdTConsName .~ "UserF")
> . (fdTypes %~ ("f":))
> )
> [''User]
> (const (Just applicativeBCR))
> (const [])
> )
This will generate
data UserF f = User
{ name :: f String
, age :: f Int
, active :: f Bool
}
but also a convert User to UserF and User to UserF.
Generated converters are not direct converter have their result in a applicative functor.
UserToUserF :: User -> g (UserF f)
UserToUserF (User a b c) = UserF <$> convertA a <*> convertA b <*> convertA c
And
UserFToUser :: User -> g (UserF f)
UserFToUser (User a b c) = UserF <$> convertA a <*> convertA b <*> convertA c
In order to be able to see the a UserF we need to deriving a few show instance
>deriving instance Show (UserF Maybe)
>deriving instance Show (UserF [])
>deriving instance Show a => Show (UserF (Either a))
For example, we know we can "produce" a `UserF Maybe` from a `User` so the convertion
can be done in the Identity functor
-- runIdentity $ aUserToUserF (User "bilbo" 127 True) :: UserF Maybe
-- >>> UserF {name = Just "bilbo", age = Just 127, active = Just True}
However, we (only) may get a User from a User Maybe, so we can `aUserFToUser` To get a `Maybe User`
-- aUserFToUser (UserF (Just "a") (Just 127) (Just False)) :: Maybe User
-- >>> Just (User {name = "a", age = 127, active = False})
-- aUserFToUser (UserF (Just "a") Nothing (Just False)) :: Maybe User
-- >>> Nothing
Now, we are just missing on thing, being able to zip to `UserF` so we can apply the equivalent of a User V to a user V
At the moment the validation function has to be done manually
>validate :: User -> UserF (Either String)
>validate (User a b c) = UserF (Right a) (positive b) (Right c) where
> positive x | x <=0 = Left "negative value"
> | otherwise = Right x
-- validate $ User "a" 3 True
-- >>> UserF {name = Right "a", age = Right 3, active = Right True}
-- validate $ User "a" 0 True
-- >>> UserF {name = Right "a", age = Left "negative value", active = Right True}
Now, we can convert back (or not) to a User
fullValidate :: User -> Either (UserF (Either String)) User
>fullValidate u = let
> v = validate u
> v' = aUserFToUser v :: Either String User
> in case v' of
> Left _ -> Left v
> Right _ -> Right u
-- fullValidate (User "b" 3 False)
-- >>> Right (User {name = "b", age = 3, active = False})
-- fullValidate (User "b" (-3) False)
-- >>> Left (UserF {name = Right "b", age = Left "negative value", active = Right False})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment