Реализовать стиль Applicative Builder с помощью Generics

Контекст

Если у нас есть

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

мы уже можем построить его в аппликативном стиле в аппликативном контексте (здесь IO), как

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

Проблема

Что, если кто-то предпочитает строить с явным написанием имен полей записи? Такие как:

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

Это не будет проверять тип. Одно решение

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

Что неплохо. Но мне интересно (на данный момент только ради себя), может ли работать следующее:

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"
    }

Я считаю, что это можно сделать с помощью простого сопоставления шаблонов GHC.Generics, но это не будет обеспечивать безопасность типов, поэтому я искал более сильный подход. Я столкнулся с generics-sop, который преобразует запись в разнородный список и поставляется с, казалось бы, удобной операцией hsequence.

Точка, где я застрял

generics-sop сохраняет тип аппликации в отдельном параметре типа своего разнородного списка, и это всегда I (идентификация) при использовании сгенерированного преобразования. Поэтому мне нужно было бы отобразить список hlist и удалить I из элементов, которые эффективно переместят аппликатив под I в упомянутый параметр типа (это будет Comp IO Maybe), чтобы я мог использовать hsequence и, наконец, добавить обратно Is, чтобы я мог скрытно вернуться к записи.

Но я не знаю, как написать сигнатуру типа для функции удаления/добавления I, которая сообщает, что типы соответствующих элементов hlist последовательно изменяются, теряя/приобретая внешний тип. Это вообще возможно?


person ron    schedule 24.10.2016    source источник
comment
Я не уверен, что все это будет работать или, по крайней мере, не так хорошо, как вы себе представляете. Обратите внимание, что FooC { x = someEnvInt "someX" , y = someEnvText "someY" } не будет компилироваться сам по себе. Если вы измените someEnv___ на подпись Data.Functor.Compose IO Maybe ___, у вас может быть шанс. Но на тот момент я не уверен, что это вообще стоило бы того...   -  person Alec    schedule 24.10.2016
comment
@Alec: перенос в Compose (или эквивалент generics-sop) приемлем.   -  person ron    schedule 24.10.2016
comment
Вам не нужны дженерики.. просто напишите функцию (Applicative g, Applicative f) => FooC (Compose f g) -> f (FooC g) (эта функция по сути просто sequence) - затем измените тип someEnvInt на Compose IO Maybe Int. Если вы хотите, вы можете выполнить «разложение», используя семейства типов, что избавит вас от изменения типа someEnvInt, но я лично не думаю, что это стоит усилий.   -  person user2407038    schedule 24.10.2016
comment
@user2407038: user2407038: Я хочу избежать ручной прокрутки функции, так как «Foo» может иметь много полей, и тогда это просто шаблон. Вот почему я хотел дженериков.   -  person ron    schedule 29.10.2016


Ответы (2)


Но я не знаю, как написать сигнатуру типа для функции удаления/добавления I, которая сообщает, что типы соответствующих элементов hlist последовательно изменяются, теряя/приобретая внешний тип. Это вообще возможно?

Я тоже не знаю, как это сделать. Возможным обходным путем (за счет некоторого шаблонного кода) будет использование записать синонимы шаблонов, чтобы напрямую создать представление суммы произведений, сохраняя при этом возможность использовать именованные поля:

{-# 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)
             }

Тестирование на ghci:

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

Проблема с Generics заключается в том, что ваш тип FooC имеет вид (* -> *) -> * и, насколько я знаю, невозможно автоматически создать экземпляр GHC.Generics для такого типа. Если вы открыты для решения с использованием 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
    |]

Я использую оператор композиции типов из пакета TypeCompose для определения класса типов, который может "развернуть" один аппликативный слой из типа записи. То есть, если у вас есть FooC (IO :. Maybe), вы можете превратить его в IO (FooC Maybe).

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"
    }

Конструктор O из TypeCompose используется для переноса результата функции IO (Maybe a) в составной ((IO .: Maybe) a).

person shang    schedule 25.10.2016