Лучший способ реализовать специальный полиморфизм в Haskell?

У меня есть полиморфная функция, например:

convert :: (Show a) => a -> String
convert = " [label=" ++ (show a) ++ "]"

Но иногда я хочу передать ему Data.Map и сделать более причудливое преобразование значения ключа. Я знаю, что здесь нельзя сопоставить шаблон, потому что Data.Map — это абстрактный тип данных (согласно этот похожий вопрос SO), но мне не удалось использовать охранники для этой цели, и я не уверен, что ViewPatterns поможет здесь (и предпочел бы избегать их для переносимости).

Это больше то, что я хочу:

import qualified Data.Map as M

convert :: (Show a) => a -> String
convert a 
    | M.size \=0 = processMap2FancyKVString a -- Heres a Data.Map
    | otherwise = " [label=" ++ (show a) ++ "]" -- Probably a string

Но это не работает, потому что M.size не может принимать ничего, кроме Data.Map.

В частности, я пытаюсь изменить sl в библиотеке функциональных графов для обработки цвета и других атрибутов ребер в выводе GraphViz.

Обновить

Хотел бы я принять все три ответа TomMD, Antal S-Z и luqui на этот вопрос, поскольку все они поняли, о чем я действительно спрашивал. Я бы сказал:

  • Antal S-Z дал наиболее «элегантное» решение применительно к FGL, но также потребовал бы наибольшего переписывания и переосмысления для реализации в личной проблеме.
  • TomMD дал отличный ответ, который находится где-то между Antal SZ и luqui с точки зрения применимости и правильности. Это также прямо и по делу, что я очень ценю и почему я выбрал его ответ.
  • luqui дал лучший ответ «заставьте его работать быстро», который я, вероятно, буду использовать на практике (поскольку я аспирант, и это всего лишь одноразовый код для проверки некоторых идей). Причина, по которой я не согласился, заключалась в том, что ответ TomMD, вероятно, лучше поможет другим людям в более общих ситуациях.

С учетом сказанного, все они являются отличными ответами, а приведенная выше классификация является грубым упрощением. Я также обновил заголовок вопроса, чтобы лучше представить мой вопрос (Спасибо еще раз всем за расширение моего кругозора!


person JKnight    schedule 15.10.2010    source источник


Ответы (3)


Вы только что объяснили, что вам нужна функция, которая ведет себя по-разному в зависимости от типа ввода. В то время как вы можете использовать оболочку data, таким образом закрывая функцию на все время:

data Convertable k a = ConvMap (Map k a) | ConvOther a
convert (ConvMap m) = ...
convert (ConvOther o) = ...

Лучше использовать классы типов, оставив функцию convert открытой и расширяемой, не позволяя пользователям вводить бессмысленные комбинации (например, ConvOther M.empty).

class (Show a) => Convertable a where
    convert :: a -> String

instance Convertable (M.Map k a) where
    convert m = processMap2FancyKVString m

newtype ConvWrapper a = CW a
instance Convertable (ConvWrapper a) where
    convert (CW a) = " [label=" ++ (show a) ++ "]"

Таким образом, вы можете использовать экземпляры, которые хотите использовать для каждого типа данных, и каждый раз, когда требуется новая специализация, вы можете расширить определение convert, просто добавив еще один instance Convertable NewDataType where ....

Некоторые люди могут нахмуриться над оберткой newtype и предложить такой пример:

instance Convertable a where
    convert ...

Но для этого потребуются настоятельно не рекомендуемые перекрывающиеся и неразрешимые расширения экземпляров для очень небольшого удобства программиста.

person Thomas M. DuBuisson    schedule 15.10.2010

Возможно, вы не то спрашиваете. Я собираюсь предположить, что у вас либо есть граф, все узлы которого — Map, либо у вас есть граф, все узлы которого — что-то другое. Если вам нужен граф, в котором сосуществуют Map и не-карты, то ваша проблема заключается не только в этом (но это решение все равно поможет). Смотрите конец моего ответа в этом случае.

Самый чистый ответ здесь — просто использовать разные convert функции для разных типов, и любой тип, который зависит от convert, принимает его в качестве аргумента (функция более высокого порядка).

Итак, в GraphViz (избегая переделки этого дрянного кода) я бы изменил функцию graphviz, чтобы она выглядела так:

graphvizWithLabeler :: (a -> String) -> ... -> String
graphvizWithLabeler labeler ... =
   ...
   where sa = labeler a

А затем пусть graphviz тривиально делегирует ему:

graphviz = graphvizWithLabeler sl

Тогда graphviz продолжает работать как и раньше, и у вас есть graphvizWithLabeler когда вам нужна более мощная версия.

Таким образом, для графов с узлами Maps используйте graphvizWithLabeler processMap2FancyKVString, иначе используйте graphviz. Это решение можно отложить как можно дольше, приняв соответствующие меры, такие как функции более высокого порядка или методы классов типов.

Если вам нужно, чтобы Maps и другие элементы сосуществовали в одном графе, вам нужно найти один тип, населенный всем, чем может быть узел. Это похоже на предложение TomMD. Например:

data NodeType
    = MapNode (Map.Map Foo Bar)
    | IntNode Int

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

Важно помнить, что в Haskell нет преобразования вниз. Функция типа foo :: a -> a не имеет возможности узнать что-либо о том, что ей было передано (в разумных пределах, охладите своих педантов). Таким образом, функцию, которую вы пытались написать, невозможно выразить на Haskell. Но, как видите, есть и другие способы выполнения работы, и они оказываются более модульными.

Это сказало вам, что вам нужно знать, чтобы достичь того, что вы хотели?

person luqui    schedule 16.10.2010

Ваша проблема на самом деле не такая, как в этом вопросе. В вопросе, на который вы ссылались, у Дерека Терна была функция, которая, как он знал, принимает Set a, но не может сопоставить шаблон. В вашем случае вы пишете функцию, которая будет принимать любой a, у которого есть экземпляр Show; вы не можете сказать, какой тип вы просматриваете во время выполнения, и можете полагаться только на функции, доступные для любого типа Showable. Если вы хотите, чтобы функция выполняла разные действия для разных типов данных, это называется специальным полиморфизмом и поддерживается в Haskell классами типов, такими как Show. (Это в отличие от параметрического полиморфизма, когда вы пишете функцию, подобную head (x:_) = x, которая имеет тип head :: [a] -> a; неограниченный универсальный a делает ее параметрической.) Итак, чтобы делать то, что вы хотите, вы вам придется создать свой собственный класс типов и создавать его экземпляры, когда вам это нужно. Однако это немного сложнее, чем обычно, потому что вы хотите сделать все, что является частью Show, неявно частью вашего нового класса типов. Для этого требуются некоторые потенциально опасные и, возможно, излишне мощные расширения GHC. Вместо этого, почему бы не упростить вещи? Вы, вероятно, сможете определить подмножество типов, которые вам действительно нужно напечатать таким образом. Как только вы это сделаете, вы можете написать код следующим образом:

{-# LANGUAGE TypeSynonymInstances #-}

module GraphvizTypeclass where

import qualified Data.Map as M
import Data.Map (Map)

import Data.List (intercalate) -- For output formatting

surround :: String -> String -> String -> String
surround before after = (before ++) . (++ after)

squareBrackets :: String -> String
squareBrackets = surround "[" "]"

quoted :: String -> String
quoted = let replace '"' = "\\\""
             replace c   = [c]
         in surround "\"" "\"" . concatMap replace

class GraphvizLabel a where
  toGVItem  :: a -> String
  toGVLabel :: a -> String
  toGVLabel = squareBrackets . ("label=" ++) . toGVItem

-- We only need to print Strings, Ints, Chars, and Maps.

instance GraphvizLabel String where
  toGVItem = quoted

instance GraphvizLabel Int where
  toGVItem = quoted . show

instance GraphvizLabel Char where
  toGVItem = toGVItem . (: []) -- Custom behavior: no single quotes.

instance (GraphvizLabel k, GraphvizLabel v) => GraphvizLabel (Map k v) where
  toGVItem  = let kvfn k v = ((toGVItem k ++ "=" ++ toGVItem v) :)
              in intercalate "," . M.foldWithKey kvfn []
  toGVLabel = squareBrackets . toGVItem

В этой настройке все, что мы можем вывести в Graphviz, является экземпляром GraphvizLabel; функция toGVItem заключает элементы в кавычки, а toGVLabel заключает все это в квадратные скобки для немедленного использования. (Возможно, я напортачил с форматированием, которое вы хотите, но это всего лишь пример.) Затем вы объявляете, что является экземпляром GraphvizLabel и как превратить его в элемент. Флаг TypeSynonymInstances просто позволяет нам писать instance GraphvizLabel String вместо instance GraphvizLabel [Char]; это безвредно.

Теперь, если вам действительно нужно, чтобы все с экземпляром Show также были экземпляром GraphvizLabel, есть способ. Если вам это действительно не нужно, то не используйте этот код! Если вам действительно нужно это сделать, вы должны использовать языковые расширения с пугающими названиями UndecidableInstances и OverlappingInstances (и менее пугающее название FlexibleInstances). Причина этого в том, что вы должны утверждать, что все, что Showable, является GraphvizLabel, но компилятору трудно это сказать. Например, если вы воспользуетесь этим кодом и напишете toGVLabel [1,2,3] в приглашении GHCi, вы получите сообщение об ошибке, поскольку 1 имеет тип Num a => a, а Char может быть экземпляром Num! Вы должны явно указать toGVLabel ([1,2,3] :: [Int]), чтобы заставить его работать. Опять же, это, вероятно, излишне тяжелая техника для решения вашей проблемы. Вместо этого, если вы можете ограничить то, что, по вашему мнению, будет преобразовано в метки, что весьма вероятно, вы можете просто указать эти вещи! Но если вы действительно хотите, чтобы Showability подразумевал GraphvizLabelability, вот что вам нужно:

{-# LANGUAGE TypeSynonymInstances, FlexibleInstances
,          UndecidableInstances, OverlappingInstances #-}

-- Leave the module declaration, imports, formatting code, and class declaration
-- the same.

instance GraphvizLabel String where
  toGVItem = quoted

instance Show a => GraphvizLabel a where
  toGVItem = quoted . show

instance (GraphvizLabel k, GraphvizLabel v) => GraphvizLabel (Map k v) where
  toGVItem  = let kvfn k v = ((toGVItem k ++ "=" ++ toGVItem v) :)
              in intercalate "," . M.foldWithKey kvfn []
  toGVLabel = squareBrackets . toGVItem

Обратите внимание, что ваши конкретные случаи (GraphvizLabel String и GraphvizLabel (Map k v)) остаются прежними; вы только что свернули дела Int и Char в дело GraphvizLabel a. Помните, что UndecidableInstances означает именно то, о чем говорит: компилятор не может сказать, можно ли проверять экземпляры, или вместо этого создаст цикл проверки типов! В этом случае я достаточно уверен, что все здесь на самом деле разрешимо (но если кто-нибудь заметит, где я не прав, пожалуйста, дайте мне знать). Тем не менее, к использованию UndecidableInstances всегда следует подходить с осторожностью.

person Antal Spector-Zabusky    schedule 15.10.2010