В Haskell, как fmap между отдельными Traversables?

Как мы знаем, подпись fmap — это (a -> b) -> f a -> f b, где f — это Functor.

Кажется естественным, что, чтобы быть как можно более общим и улучшить код факторинга, можно сопоставить «список вещей» с другим, возможно, отличным «списком вещей». Интуитивно я не понимаю, почему это должно быть невозможно или нежелательно.

Я ищу функцию gmap с тем же поведением, что и fmap, но с той сигнатурой gmap :: (a -> b) -> (f a) -> (g b), где я разрешаю различать контейнеры прибытия и отправления.

Я не уверен, будет ли это иметь смысл в очень общем случае, когда f и g равны Functors, однако идея «списка вещей» звучит более существенно в классе Traversable, предполагая, что меня больше всего интересует перебор данных. .

Так что, возможно, подпись должна быть gmap :: (Traversable f, Traversable g) => (a -> b) -> (f a) -> (g b).

Даже если g имеет другую природу, чем f, это все же то, что можно пройти слева направо, поэтому все еще кажется, что можно сопоставить k-й посещенный элемент f с k-м посещенным элементом g.

Предполагая, что мои мысли не ошиблись, есть ли такая функция в Haskell?

По сути, мой вопрос заключается в том, как бы вы преобразовали одну списочную вещь в другую в Haskell наиболее продуманным и элегантным способом?


person jam    schedule 16.11.2019    source источник
comment
Как бы вы превратили список в дерево? Оба проходные. Даже если дерево можно пройти слева направо, знание его обхода не позволяет реконструировать дерево уникальным способом. Мы могли бы сгенерировать дерево, но это потребовало бы довольно произвольного выбора. Такие классы, как foldable/traversable, по своей сути описывают способы потребления контейнеров, а не их производства. Возможно, вы хотите другой класс.   -  person chi    schedule 16.11.2019
comment
@chi Да, возможно, это не тот класс, тогда я бы искал самый общий для этой цели.   -  person jam    schedule 16.11.2019
comment
Если предположить, что такой класс существует, будет ли он поддерживать только контейнеры, подобные спискам? Можете ли вы представить какой-либо другой контейнер, который можно создать из списка осмысленным образом?   -  person chi    schedule 16.11.2019
comment
Возможно, я хотел бы преобразовать список в вектор, но как-то не хочется писать специальный код для преобразования списка в вектор. Я хотел бы найти наиболее продуманный, наиболее общий и красивый способ преобразования между двумя списками.   -  person jam    schedule 16.11.2019
comment
Не возможно в общем. Рассмотрим x :: NonEmpty Void x = gmap id [].   -  person Joseph Sible-Reinstate Monica    schedule 16.11.2019
comment
Я не уверен, но не похоже ли это на композицию fmap и естественное преобразование между функторами..?   -  person Mark Seemann    schedule 16.11.2019
comment
@MarkSeemann Да, я думал о естественных преобразованиях, почему-то я хочу сопоставить функтор с другим функтором ... но я на самом деле не эксперт в Haskell ...   -  person jam    schedule 16.11.2019
comment
Yeah I thought of natural transformations, somehow what I want is mapping a functor to another functor.. - хотя я не очень хорошо знаком с естественными преобразованиями в Haskell, в математике это (отображение функтора в другой функтор) точно является естественным преобразованием.   -  person Robin Zigmond    schedule 16.11.2019
comment
@RobinZigmond Да, как и вы, я имел в виду математическое значение. Я также не знаю, как выразить естественные преобразования в Haskell, но я думаю, что это то, что я пытаюсь сделать здесь. Я вижу, что есть некоторые библиотеки, я могу проверить их позже. Я думаю, что встречный пример Джозефа Сибла показывает, что мне нужны дополнительные требования, которые можно было бы формализовать с помощью этих естественных преобразований.   -  person jam    schedule 16.11.2019
comment
Я думаю, что вы ищете (горизонтальную?) Композицию двух естественных изоморфизмов. То есть что-то похоже на список, если существует естественный изоморфизм между этой вещью и []. Если a ~> [] и b ~> [] оба являются изоморфизмами, то определить a ~> b и b ~> a тривиально, пройдя через [] в любом направлении. (Где type f ~> g = forall x. f x -> g x — естественное преобразование.)   -  person chepner    schedule 16.11.2019
comment
(Изоморфизм важен, потому что, хотя вы можете определить как [] ~> Maybe, так и Maybe ~> [], это не изоморфизм, потому что переход от списка к Maybe приведет к потере всех элементов исходного списка, кроме одного, поэтому вы не можете вернуть исходный список, заданный только его Maybe аналог.)   -  person chepner    schedule 16.11.2019


Ответы (3)


Один трюк, который мы часто используем в Haskell, чтобы показать, что это невозможно, — это попытка произвести с ним «false» — иначе говоря, создать значение типа

data Void

тип без конструкторов. Если возможно, используя вашу подпись типа, создать значение типа Void, то ваша подпись типа не может быть реализована. Это также известно как «reducto ad absurdum» или «опровержение от противного». Если ваша сигнатура типа позволит нам создать значение типа Void... тогда "очевидно" ваша сигнатура типа является чушью и не может быть реализована.

В этом случае мы «возвращаем» экземпляр Traversable, поэтому давайте использовать Traversable как (,) Void:

instance Traversable ((,) w) where
    traverse f (x, y) = (x,) <$> f y

Теперь воспользуемся f как любым старым функтором. Это может быть что угодно... давайте использовать Maybe, потому что кажется, что все уже это понимают.

Тогда вы могли бы написать:

gmap :: (a -> b) -> Maybe a -> (Void, b)

О нет, этого не может быть... похоже, с помощью gmap вы можете создать Void, просто передав любую старую вещь:

gmap :: (() -> ()) -> Maybe () -> (Void, ())

Итак, теперь моя стратегия создания Void:

bad :: Void
bad = fst (gmap id Nothing)

Поскольку у Void нет конструкторов, значение типа bad :: Void не должно существовать (не учитывая что-то вроде бесконечного цикла или частичной функции). Итак, если само существование if gmap может позволить нам создать значение типа Void... это должно означать, что gmap не может существовать в той форме, которую вы дали.


Что касается вашей более общей проблемы, «почему» в том, как работает Traversable, заключается в том, что он может только когда-либо модифицировать структуры. Он не может создавать их. Здесь вы хотите создать значение g b, но Traversable не может его "создать", оно может только "преобразовать" его. Ваше непонимание может исходить из того, что вы думаете, что Traversable - это класс типов, похожий на список: это не совсем так. Использование [] в качестве архетипа может ввести вас в заблуждение.

Моим "типичным" Traversable для представления свойств класса типов является Map k из контейнеров Data.Map: Map не является а, а скорее значениями, связанными с ключами. Любые операции над ним должны учитывать это свойство ассоциации... и не рассматривать его как большой список без дополнительной структуры.

Итак, что было бы возможно, выглядит примерно так:

replace :: (Foldable f, Traversable g) => f a -> g b -> g a

Где все значения g b заменены всеми значениями f a. Это на самом деле весело писать, если вы ищете упражнение. По сути, replace сохранит ту же структуру, что и g a, но просто заменит значения. Таким образом, вы можете «создать» g a из f a, если у вас есть, так сказать, «пример g b». Если вы использовали что-то вроде:

replace :: [a] -> Map k b -> Map k a

затем replace заменит все значения во второй карте элементами в списке, заменив их на правильные значения ключей.

Затем вы можете написать:

gmap :: (Traversable a, Traversable g) => (a -> b) -> f a -> g c -> g b

Где вы берете «пример» g a структуры, которую хотите скопировать.

Ближе всего к возможности «построить» структуру в общих классах типов Haskell — это IsList из https://hackage.haskell.org/package/base-4.12.0.0/docs/GHC-Exts.html.#t:IsList

Этот класс типов дает вам две функции, fromList и toList, так что вы можете написать:

throughIsList :: (IsList l, IsList m, Item l ~ Item m) => l -> m
throughIsList = fromList . toList

И заставить его работать более Functors:

gmap :: (IsList (f a), IsList (g b), Item (f a) ~ a, Item (g b) ~ b) => (a -> b) -> f a -> g b
gmap f = fromList . map f . toList

Проблема в том, что большинство Functor не являются экземплярами IsList... и многие из реальных экземпляров не являются тотальными. Так что это не совсем удобно для большинства Functor.


Так что, в конце концов, я не думаю, что есть удовлетворительный ответ. Если что-то, что вы делаете, зависит от того факта, что есть хороший ответ (кроме ответа «нет») ... может быть, я могу спросить, какова ваша «конечная цель»? Для чего вы планируете использовать этот тип?

(Например, в 90 % ситуаций, когда люди задают вопросы вроде «Могу ли я преобразовать монады» или что-то в этом роде, обычно они не хотят что-то делать в общем , а имели в виду конкретные типы.)

person Justin L.    schedule 16.11.2019
comment
Хороший ответ. Я думаю, что подпись для replace недостаточно общая: replace :: (...) => f a -> g b -> g a. Или g (), если мы действительно хотим дать понять, что мы просто используем его форму. - person luqui; 17.11.2019
comment
@Justin Нет, я не пытаюсь добиться чего-то конкретного. Я чувствую, что мой вопрос отдаляется от того, что я действительно хотел знать. Возможно, Traversable здесь не подходит, я не хочу, чтобы люди сосредотачивались на этом. Существуют различные виды списков, например, векторы, массивы, списки. Мне интересно, что является общим знаменателем для этих структур. Что у них общего, что делает их похожими на списки ?? Существует ли тип Haskell, который выражает это? Если да, то можно ли использовать этот общий спископодобный тип для преобразования между различными спископодобными типами? - person jam; 17.11.2019
comment
Я должен сделать репост вопроса. Я думаю, что, вероятно, в любом случае нет списочных типов... это слишком специфично, чтобы быть частью базы. В любом случае использование этой функции замены кажется достаточно хорошим - person jam; 17.11.2019
comment
@Дж.М. Если вы ищете, какие единицы измерения являются векторами, массивами, списками и т. д., то IsList может быть началом. Но более принципиальным подходом могла бы быть библиотека mono-traversable, в которой есть классы типов для различных типов контейнеров разной степени специфичности/функциональности. - person Justin L.; 18.11.2019

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

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleContexts #-}

class (Functor f, Functor g) => GFunctor f g where
  toG :: f a -> g a
  gmap :: (a -> b) -> (f a) -> (g b)
  gmap fn functor = toG $ fmap fn functor

data Tree a = Leaf | Node (Tree a) a (Tree a) deriving Show

instance Functor Tree where
  fmap f Leaf = Leaf
  fmap f (Node t1 x t2) = Node (fmap f t1) (f x) (fmap f t2)

instance GFunctor [] Tree  where
  toG [] = Leaf 
  toG [x] = Node Leaf x Leaf
  toG (x:xs) = Node (toG $ (takeHalf xs)) x ((toG $ dropHald xs))

takeHalf xs = take ((length xs) `div` 2) xs
dropHald xs = drop ((length xs) `div` 2) xs

res :: Tree Int
res = gmap (+1) [1,2,3,4,5]

выход:

   res
=> Node (Node Leaf 3 (Node Leaf 4 Leaf)) 2 (Node Leaf 5 (Node Leaf 6 Leaf))
person Damián Rafael Lattenero    schedule 16.11.2019

Я думаю, что вы можете сделать это с помощью fmap и естественного преобразования. Вот пример использования пакета natural-transformations, просто чтобы продемонстрировать идею.

Первое естественное преобразование, которое приходит мне в голову, это safeHead:

safeHead :: [a] -> Maybe a
safeHead [] = Nothing
safeHead (x:_) = Just x

Запуск GHCi с пакетом natural-transformations:

Prelude Control.Natural> t = wrapNT safeHead
Prelude Control.Natural> t # [1..3]
Just 1
Prelude Control.Natural> t # []
Nothing
Prelude Control.Natural> fmap show (t # [1..3])
Just "1"
Prelude Control.Natural> fmap show (t # [])
Nothing

Итак, для естественного преобразования t будет ли что-то вроде fmap f . (t #) делать то, что вы хотите?

(Здесь я представляю, что f — это обычная функция a -> b.)

person Mark Seemann    schedule 16.11.2019
comment
Я не минусовал, но похоже, что спрашивающий ищет общую версию, которая работает для всех f и g, вместо конкретных f и g (например, safeHead) - person Justin L.; 16.11.2019