Есть ли zip с аналогом для кортежей? - PullRequest
0 голосов
/ 26 апреля 2018

Предварительное примечание: это ответ удаленный вопрос от SeanD .

Также как zipWith для списков ...

GHCi> zipWith (+) [1,2] [3,4]
[4,6]

... такое чувство, что должно быть что-то аналогичное для кортежей, в духе ...

tupleZipWith (+) (1,2) (3,4)

... но, похоже, ничего подобногочто в базе .Какие варианты у меня есть?

Ответы [ 2 ]

0 голосов
/ 26 апреля 2018

Используя 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.(Документация модуля содержит более подробную информацию об этом представлении.)

Стандартные шаги:

  1. определяют функции для этого фиксированного набора типов (возможно, подмножествоit);

  2. адаптировать их к пользовательским типам, используя изоморфизм, заданный экземпляром 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))
0 голосов
/ 26 апреля 2018

Одним из вариантов является использование пакета tuples-homogenous-h98 , который предоставляет упаковщики нового типа для однородных кортежей, которые имеют соответствующие Applicative экземпляры:

GHCi> import Data.Tuple.Homogenous
GHCi> import Control.Applicative
GHCi> liftA2 (+) (Tuple2 (1,2)) (Tuple2 (3,4))
Tuple2 {untuple2 = (4,6)}
GHCi> (>) <$> Tuple3 (7,4,7) <*> Tuple3 (6,6,6)
Tuple3 {untuple3 = (True,False,True)}

Если у вас есть любимая библиотека однородных кортежей / векторов фиксированного размера / списка фиксированных размеров, отличная от tuples-homogenous-h98 , есть вероятность, что она также будет иметь подходящие ZipList -подобные Applicative экземпляры.


Для немного другого подхода к вопросу о парах вы можете рассмотреть Data.Biapplicative из бифункторов :

GHCi> import Data.Biapplicative
GHCi> bimap (+) (+) (1,2) <<*>> (3,4)
(4,6)

Одна приятная особенность этого подхода заключается в том, что он может обрабатывать гетерогенные пары:

GHCi> bimap (+) (+) (1,2.5) <<*>> (3,4)
(4,6.5)
GHCi> bimap (+) (++) (1,"foo") <<*>> (3,"bar")
(4,"foobar")
...