Реализовать прикладной стиль построения с помощью обобщений

#haskell #ghc-generics

#haskell #ghc-обобщения

Вопрос:

Контекст

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

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

мы уже можем создать его в стиле applicative в контексте Applicative (здесь 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 из элементов, которые эффективно перемещали бы Applicative под I указанным параметром типа (это было бы Comp IO Maybe ), чтобы я мог использовать hsequence , и, наконец, добавить обратно I s, чтобы я мог скрытно вернуться к записи.

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

Комментарии:

1. Я не уверен, что все это сработает, или, по крайней мере, не так хорошо, как вы себе представляете. Обратите внимание, что FooC { x = someEnvInt "someX" , y = someEnvText "someY" } это не будет компилироваться само по себе. Если вы измените, someEnv___ чтобы иметь подпись Data.Functor.Compose IO Maybe ___ , у вас может появиться шанс. Но на данный момент я не уверен, что это вообще того стоило бы…

2. @Alec: перенос в Compose (или generics-sop эквивалент) является приемлемым.

3. Вам не нужны generics .. просто напишите функцию (Applicative g, Applicative f) => FooC (Compose f g) -> f (FooC g) (эта функция по сути просто sequence ) — затем измените тип someEnvInt на Compose IO Maybe Int . Если вы хотите, вы можете выполнить ‘uncomposition’, используя семейства типов, что избавило бы вас от изменения типа someEnvInt , но я лично не думаю, что это стоит затраченных усилий.

4. @user2407038: Я хочу избежать ручного развертывания функции, поскольку ‘Foo’ может содержать много полей, и тогда это просто шаблон. Вот почему я хотел использовать Generics.

Ответ №1:

Но я не знаю, как написать сигнатуру типа для функции удаления / добавления 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)))
  

Ответ №2:

Проблема с Generics заключается в том, что ваш FooC тип имеет вид (* -> *) -> * и, насколько я знаю, невозможно автоматически создать GHC.Generics экземпляр для такого типа. Если вы открыты для решения с использованием шаблона Haskell, относительно легко написать код, необходимый для автоматической обработки записей любого типа.

 {-# 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) файл.