Лучший способ реализовать специальный полиморфизм в Haskell? - PullRequest
12 голосов
/ 15 октября 2010

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

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

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

Ответы [ 3 ]

13 голосов
/ 15 октября 2010

Вы только что объяснили, что вам нужна функция, которая ведет себя по-разному в зависимости от типа ввода. Хотя вы можете использовать оболочку 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 ...

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

9 голосов
/ 16 октября 2010

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

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

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

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

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

graphviz = graphvizWithLabeler sl

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

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

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

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

Параметрируется, конечно, на уровень универсальности, который вам нужен. Тогда ваша функция-метщик должна решить, что делать в каждом из этих случаев.

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

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

8 голосов
/ 15 октября 2010

Ваша проблема на самом деле не такая, как в этом вопросе.В вопросе, на который вы ссылались, у Дерека Турна была функция, которую он знал, что взял Set a, но не мог сопоставить образец.В вашем случае вы пишете функцию, которая будет принимать любой a с экземпляром Show;вы не можете сказать, какой тип вы просматриваете во время выполнения, и можете полагаться только на функции, доступные для любого Show способного типа.Если вы хотите, чтобы функция делала разные вещи для разных типов данных, это называется специальный полиморфизм и поддерживается в 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).Причина этого заключается в том, что вы должны утверждать, что все , которое Show способно, является GraphvizLabel, но компилятору это трудно сказать.Например, если вы используете этот код и напишите toGVLabel [1,2,3] в приглашении GHCi, вы получите ошибку, так как 1 имеет тип Num a => a, а Char может быть экземпляром Num!Вы должны явно указать toGVLabel ([1,2,3] :: [Int]), чтобы заставить его работать.Опять же, это, вероятно, неоправданно тяжелая техника для решения вашей проблемы.Вместо этого, если вы можете ограничить то, что, по вашему мнению, будет преобразовано в метки, что весьма вероятно, вы можете просто указать эти вещи вместо этого!Но если вы действительно хотите Show способность подразумевать GraphvizLabel способность, это то, что вам нужно:

{-# 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 следует всегда подходить с осторожностью.

...