Используя GHC Generics, мы можем определить операции, которые зависят только от структуры типа (количество конструкторов и их арность).
Нам нужна функция zipWithP
, которая принимает функцию f
и архивирует два кортежа, применяя f
между соответствующими полями.Возможно, что-то с сигнатурой, совпадающей с этой:
zipWithP
:: forall c s. _
=> (forall s. c s => s -> s -> s) -> a -> a -> a
Здесь f :: forall s. c s => s -> s -> s
является полиморфным, что позволяет кортежу быть гетерогенным, если все поля являются экземплярами c
.Это требование будет учитываться ограничением _
, которое зависит от реализации, до тех пор, пока оно работает.
Существуют библиотеки, которые захватывают общие конструкции, в частности one-liner и generics-sop .
В порядке возрастания автоматизации ...
Классическим решением являетсяиспользуйте модуль GHC.Generics
.Экземпляр Generic
представляет изоморфизм между пользовательским типом a
и ассоциированным с ним «универсальным представлением» Rep a
.
Это универсальное представление построено из фиксированного набора типов, определенных в GHC.Generics
.(Документация модуля содержит более подробную информацию об этом представлении.)
Стандартные шаги:
определяют функции для этого фиксированного набора типов (возможно, подмножествоit);
адаптировать их к пользовательским типам, используя изоморфизм, заданный экземпляром Generic
.
Шаг 1 обычнокласс типа.Здесь GZipWith
- это класс общих представлений, которые могут быть заархивированы.Здесь обрабатываются конструкторы типов в порядке убывания важности:
K1
представляет поля (просто применить f
); (:*:)
представляет продукты типа (zip theоперанды отдельно); - новый тип
M1
несет информацию на уровне типов, которую мы здесь не используем, поэтому мы просто оборачиваем / разворачиваем ее; U1
представляет нулевойконструкторы, в основном для полноты.
Шаг 2 определяет zipWithP
, составляя gZipWith
с from
/ to
, где это необходимо.
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
import GHC.Generics
class GZipWith c f where
gZipWith :: (forall s. c s => s -> s -> s) -> f p -> f p -> f p
instance c a => GZipWith c (K1 _i a) where
gZipWith f (K1 a) (K1 b) = K1 (f a b)
instance (GZipWith c f, GZipWith c g) => GZipWith c (f :*: g) where
gZipWith f (a1 :*: a2) (b1 :*: b2) = gZipWith @c f a1 b1 :*: gZipWith @c f a2 b2
instance GZipWith c f => GZipWith c (M1 _i _c f) where
gZipWith f (M1 a) (M1 b) = M1 (gZipWith @c f a b)
instance GZipWith c U1 where
gZipWith _ _ _ = U1
zipWithP
:: forall c a. (Generic a, GZipWith c (Rep a))
=> (forall s. c s => s -> s -> s) -> a -> a -> a
zipWithP f a b = to (gZipWith @c f (from a) (from b))
main = do
print (zipWithP @Num (+) (1,2) (3,4) :: (Int, Integer))
generics-sop предоставляет высокоуровневые комбинаторы для общего программирования с операциями, которые выглядят как fmap
/ traverse
/ zip
...
В этом случаесоответствующий комбинатор - hcliftA2
, который объединяет универсальные неоднородные кортежи полей с двоичной функцией.Дополнительные объяснения после кода.
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
import Control.Applicative (liftA2)
import Data.Proxy (Proxy(..))
import Generics.SOP
zipWithP
:: forall c a k
. (Generic a, Code a ~ '[k], All c k)
=> (forall s. c s => s -> s -> s) -> a -> a -> a
zipWithP f x y =
case (from x, from y) of
(SOP (Z u), SOP (Z v)) ->
to (SOP (Z (hcliftA2 (Proxy :: Proxy c) (liftA2 f) u v)))
main = do
print (zipWithP @Num (+) (1,2) (3,4) :: (Int, Integer))
Начиная с вершины zipWithP
.
Ограничения:
Code a ~ '[k]
: a
должно бытьтип с одним конструктором (Code a :: [[*]]
- это список конструкторов a
, каждый из которых указан как список его полей). All c k
: все поля конструктора k
удовлетворяют ограничениюc
.
Тело:
from
отображается из обычного типа a
в общую сумму продуктов (SOP I (Code a)
). - Мы предположили, что тип
a
имеет один конструктор.Мы применяем эти знания путем сопоставления с образцом, чтобы избавиться от слоя «сумма».Мы получаем u
и v
, типы которых products (NP I k
). - Мы применяем
hcliftA2
, чтобы сжать два кортежа u
и v
. - Поля обернуты в конструкторе типов
I
/ Identity
( functor-functor или HKD style), следовательно, существует также слой liftA2
поверх f
. - Мы получаем новый кортеж и возвращаемся от первых двух шагов, применяя конструкторы и
to
(обратное к from
).
Подробнее см. В документации generics-sop.
zipWithP
относится к классу операций, которые обычно описываются как «сделать это для каждого поля». однострочный экспортные операции, некоторые имена которых могут выглядеть знакомыми (map...
, traverse...
), которые по сути являются специализациями одного "обобщенного обхода", связанного с любым универсальнымтип.
В частности, zipWithP
называется binaryOp
.
{-# LANGUAGE TypeApplications #-}
import Generics.OneLiner
main = print (binaryOp @Num (+) (1,2) (3,4) :: (Int, Integer))