简体   繁体   English

使用泛型实现Applicative构建器样式

[英]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 我们已经可以在Applicative上下文(这里是IO)中将它构建为applicative-style

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. 我相信它可以通过裸GHC.Generics模式匹配来完成,但是它不具有类型安全性,所以我一直在寻找更强大的方法。 I encountered generics-sop , which converts the record into a heterogeneous list, and comes with a seemingly handy hsequence operation. 我遇到了generics-sop ,它将记录转换为异构列表,并带来了看似方便的hsequence操作。

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. generics-sop将Applicative的类型存储在其异构列表的单独类型参数中,并且在使用生成的转换时始终为I (Identity)。 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. 所以,我需要映射hlist并删除I从中会有效地移动应用型下的元素I所提到的类型参数(这将是Comp IO Maybe ),这样我就可以使用hsequence ,最后加回I小号所以我可以转回记录。

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. 但我不知道如何为I删除/添加功能编写类型签名,该功能通过丢失/获取外部类型来传达各个hlist元素的类型一致地改变。 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. 但我不知道如何为I删除/添加功能编写类型签名,该功能通过丢失/获取外部类型来传达各个hlist元素的类型一致地改变。 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上测试它:

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. Generics的问题是你的FooC类型有类型(* -> *) -> * ,据我所知,不可能为这种类型自动派生GHC.Generics实例。 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. 如果您对使用Template Haskell的解决方案持开放态度,则编写自动处理任何记录类型所需的TH代码相对容易。

{-# 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. 我使用TypeCompose包中的类型组合运算符来定义一个类型类,它可以从记录类型“解包”单个应用层。 Ie if you have a FooC (IO :. Maybe) you can turn it into a IO (FooC Maybe) . 即如果你有一个FooC (IO :. Maybe)你可以把它变成一个IO (FooC Maybe)

The deriveAppCon lets you automatically derive an instance for any basic record type. deriveAppCon允许您自动派生任何基本记录类型的实例。

{-# 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) . TypeComposeO构造函数用于将函数结果IO (Maybe a)包装到复合((IO .: Maybe) a) TypeCompose ((IO .: Maybe) a)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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