Лифтинг - обобщение

Мне нужно использовать тяжелую функцию подъема, например.

k = myFunc
  <$> someFunctionName 1
  <*> someFunctionName 2
  <*> someFunctionName 3
  <*> someFunctionName 4
  <*> someFunctionName 5
  <*> someFunctionName 6
  <*> someFunctionName 8
  <*> someFunctionName 9
  -- ...

Который не предусмотрен для больших функций (около 20 аргументов) в Prelude. Есть ли умный способ сделать такой подъем без явной цепочки тех aps? Я ищу что-то вроде

k = magic (map someFunctionName [1,2,3,4,5,6,8,9]) myFunc 

Мне может быть трудно угадать тип magic так как это зависит от количества аргументов поднятой функции. Конечно, невозможно использовать map в списке здесь (или это?), я ставлю это только в качестве точки зрения.

Я думаю, что ищу что-то, что могло бы быть хорошо решено зависимыми типами, которые не включены в Haskell, но, возможно, есть какой-то хитрый способ обойти это (TemplateHaskell?)

У вас есть идеи, как сделать его более элегантным и гибким?

редактировать: в моем случае типы связанных функций все одинаковы.

1 ответ

Решение

Лифт конструкторы

Используя классы типов, мы можем определить обобщенную версию liftA/ap, Сложная задача - определить, когда прекратить подъем и вернуть результат. Здесь мы используем тот факт, что конструкторы являются карри-функциями с таким количеством аргументов, сколько у них есть полей, и тип результата не является функцией.

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}

import Text.Read

-- apF
--   :: Applicative f
--   => (i -> f a)
--   -> (a -> a -> ... ->   x)   -- constructor type
--   -> (i -> i -> ... -> f x)   -- lifted function
class Applicative f => ApF f i a s t where
  apF :: (i -> f a) -> f s -> t

-- Recursive case
-- s ~ (a -> ...)
-- t ~ (i -> ...)
instance (a ~ a', t ~ (i -> t'), ApF f i a s' t') => ApF f i a (a' -> s') t where
  apF parseArg fconstr i = apF parseArg (fconstr <*> parseArg i)

-- Base case
-- s ~   x  -- x assumed not to be a function type (not (y -> z) for any y and z)
-- t ~ f x
instance {-# OVERLAPPABLE #-} (t ~ f x, Applicative f) => ApF f i a x t where
  apF _ fconstr = fconstr

liftF :: ApF f i a s t => (i -> f a) -> s -> t
liftF parseArg constr = apF parseArg (pure constr)

main = do
  let lookup :: Int -> Maybe Integer
      lookup i =
        case drop i [2,3,5,7,11,13] of
          [] -> Nothing
          a : _ -> Just a
  print $ liftF lookup (,,) 0 2 5

Более родственные записи и дженерики

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

data UserF f = User
  { name :: f @@ String
  , age :: f @@ Int
  } deriving G.Generic

type User = UserF Id

Функции типа определяются с использованием семейства типов (@@) (HKD в сообщении в блоге, указанном выше). Для этого ответа важны тождество и постоянные функции.

type family s @@ x
type instance Id   @@ x = x
type instance Cn a @@ x = a

data Id
data Cn (a :: *)

Например, мы можем собрать индексы, используемые для анализа CSV, в UserF (Cn Int):

userIxes = User { name = 0, age = 2 } :: UserF (Cn Int)

Учитывая такой параметризованный тип записи (p = UserF) и запись показателей (ixes :: p (Cn Int)) мы можем разобрать запись CSV (r :: [String]) с parseRec ниже. Здесь используется generics-sop.

parseRec :: _
         => p (Cn Int) -> [String] -> Maybe (p Id)
parseRec ixes r =
  fmap to .
  hsequence .
  htrans (Proxy :: Proxy ParseFrom) (\(I i) -> read (r !! i)) .
  from $
  ixes

Давайте разберем код снизу вверх. generics-sop предоставляет комбинаторы для преобразования записей единообразным способом, аналогичным использованию списков. Лучше всего следовать надлежащему руководству, чтобы понять основные детали, но для демонстрации мы представим, что середина конвейера между from а также to на самом деле трансформирует списки, используя динамический тип Field печатать разнородные списки.

  • from превращает запись в ее неоднородный список полей, но так как они все Int список пока действительно однороден from :: p (Cn Int) -> [Int],

  • Здесь с помощью (!!) а также readмы получаем и анализируем каждое поле по заданному индексу i, htrans Proxy в основном map: (Int -> Maybe Field) -> [Int] -> [Maybe Field],

  • hsequence в основном sequence :: [Maybe Field] -> Maybe [Field],

  • to превращает список полей в запись с совместимыми типами полей, [Field] -> p Id,

Последний шаг без усилий:

parseUser :: Record -> Maybe User
parseUser = parseRec $ User { name = 0, age = 2 }
Другие вопросы по тегам