简体   繁体   中英

Implement Applicative builder style with Generics

Context

If we have

data Foo = Foo { x :: Maybe Int, y :: Maybe Text }

we can already build it up applicative-style in an Applicative context (here IO) as

myfoo :: IO Foo
myfoo = Foo <$> getEnvInt "someX" <*> getEnvText "someY"

Problem

What if one prefers to build with explicitly writing out the record field names? Such as:

myfoo = Foo { x = getEnvInt "someX", y = getEnvText "someY" }

This won't typecheck. One solution is

{-# LANGUAGE RecordWildCards #-}
myfoo = do
    x <- getEnvInt "someX"
    y <- getEnvText "someY"
    return $ Foo {..}

Which is not bad. But I wonder (at this point only for the sake of itself) if the following could work:

data FooC f = FooC { x :: f Int, y :: f Text }
type Foo = FooC Maybe

myfoo :: IO Foo
myfoo = genericsMagic $ FooC
    { x = someEnvInt "someX"
    , y = someEnvText "someY"
    }

I believe it can be done with bare GHC.Generics pattern matching, but that wouldn't have type safety, so I was looking for a stronger approach. I encountered generics-sop , which converts the record into a heterogeneous list, and comes with a seemingly handy hsequence operation.

Point where I'm stuck

generics-sop stores the Applicative's type in a separate type parameter of its heterogeneous list, and that is always I (Identity) when using the generated conversion. So I would need to map the hlist and remove the I from the elements which would effectively move the Applicative under I to the mentioned type parameter (it would be Comp IO Maybe ), so I could use hsequence , and finally add back the I s so I can covert back to record.

But I don't know how to write a type signature for the I removal / addition function, which communicates that the types of the respective hlist elements change consistently by losing/gaining the outer type. Is this even possible?

But I don't know how to write a type signature for the I removal / addition function, which communicates that the types of the respective hlist elements change consistently by losing/gaining the outer type. Is this even possible?

I don't know how to do that either. A possible workaround (at the cost of some boilerplate) would be to use record pattern synonyms to construct the sum-of-products representation directly, while still being able to use named fields:

{-# language DeriveGeneric #-}
{-# language TypeFamilies #-}
{-# language TypeOperators #-}
{-# language PatternSynonyms #-}

import Data.Text
import qualified GHC.Generics as GHC
import Generics.SOP
import Text.Read

data Foo = Foo { x :: Int, y :: Text } deriving (Show, GHC.Generic)

instance Generic Foo

pattern Foo' :: t Int -> t Text -> SOP t (Code Foo)
pattern Foo' {x', y'} = SOP (Z (x' :* y' :* Nil))

readFooMaybe :: SOP (IO :.: Maybe) (Code Foo)
readFooMaybe = Foo'
             {
                x' = Comp (fmap readMaybe getLine)
             ,  y' = Comp (fmap readMaybe getLine)
             }

Testing it on ghci:

ghci> hsequence' readFooMaybe >>= print
12
"foo"
SOP (Z (Just 12 :* (Just "foo" :* Nil)))

The problem with Generics is that your FooC type has the kind (* -> *) -> * and, as far as I know, it's not possible to automatically derive a GHC.Generics instance for such a type. If you are open to a solution using Template Haskell it's relatively easy to write the TH code needed to automatically handle any record type.

{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TemplateHaskell #-}

module AppCon where

import Control.Applicative
import Control.Compose ((:.), unO)
import Language.Haskell.TH

class AppCon t where
  appCon :: Applicative f => t (f :. g) -> f (t g)

deriveAppCon :: Name -> Q [Dec]
deriveAppCon name = do
  (TyConI (DataD _ _ _ _ [RecC con fields] _)) <- reify name

  let names = [mkName (nameBase n) | (n,_,_) <- fields]
      apps = go [|pure $(conE con)|] [[|unO $(varE n)|] | n <- names] where
        go l [] = l
        go l (r:rs) = go [|$l <*> $r|] rs

  [d|instance AppCon $(conT name) where
      appCon ($(conP con (map varP names))) = $apps
    |]

I use the type composition operator from the TypeCompose package to define a type-class that can "unwrap" a single applicative layer from a record type. Ie if you have a FooC (IO :. Maybe) you can turn it into a IO (FooC Maybe) .

The deriveAppCon lets you automatically derive an instance for any basic record type.

{-# LANGUAGE TemplateHaskell #-}

import Control.Compose ((:.)(..))

import AppCon

data FooC f = FooC { x :: f Int, y :: f Text }
type Foo = FooC Maybe

deriveAppCon ''FooC

myfoo :: IO Foo
myfoo = appCon $ FooC
    { x = O $ someEnvInt "someX"
    , y = O $ someEnvText "someY"
    }

The O constructor from TypeCompose is used to wrap the function result IO (Maybe a) into a composite ((IO .: Maybe) a) .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM