enable_if в Haskell

Как мне написать что-то вроде следующего в Haskell:

showSquare :: (Show a, Num a) => a -> String
showSquare x = "The square of " ++ (show x) ++ " is " ++ (show (x * x))

showSquare :: (Show a, not Num a) => a -> String
showSquare x = "I don't know how to square " ++ (show x)

По сути, что-то вроде boost::enable_if на C++.

Расширения GHC в порядке.


person Clinton    schedule 10.08.2012    source источник
comment
Кстати, статья Полиморфизм, контролируемый концепцией на сайте Boost ссылка имеет прямое сравнение с классами типов Haskell (которые на самом деле являются мотивацией). Отрицание, которое вы использовали, на самом деле исходит из шага метакомпиляции, связанного с шаблонами C++ (чего простой Haskell не делает).   -  person Volker Stolz    schedule 10.08.2012


Ответы (4)


Зачем тебе это? Средство проверки типов гарантирует, что вы никогда не вызовете showSquare для чего-то, что не является Num в первом случае. В Haskell нет instanceof, так как все печатается статически.

Это не работает для произвольных типов: вы можете определить только свои собственные класс типа, например.

class Mine a where
  foo :: a -> String

instance (Num a) => Mine a where
  foo x = show x*x

И вы можете добавить больше экземпляров для других классов, но вы не сможете написать просто instance Mine a для произвольного a. Дополнительный instance (Show a) => ... также не поможет, так как перекрывающиеся экземпляры также не допускаются (ссылка описывает способ обойти это, но для этого требуется довольно много дополнительного оборудования).

person Volker Stolz    schedule 10.08.2012
comment
ShiDoiSi: Я хочу, чтобы это разрешилось статически. Например, как show может делать разные вещи для Int, String, Tree и т. д. все статически. - person Clinton; 10.08.2012
comment
Я не уверен, справедливо ли называть метакомпиляцию статической :-) - person Volker Stolz; 10.08.2012
comment
Несмотря на все это, будет справедливо подчеркнуть @Clinton, что он действительно не должен этого делать, ссылка «перекрывающиеся случаи» является буквальным ответом на его вопрос. - person dave4420; 10.08.2012

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

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

f :: Num a => a -> a -> a
f x y = x + y

Num a в типе f означает, что мы можем применять любые методы класса класса типа Num к значениям типа a. Мы сознательно не называем конкретный тип, чтобы получить общее поведение. По сути, мы говорим: «Нас не волнует, что такое a, но мы знаем, что к нему применимы Num операции». Следовательно, мы можем использовать Num методы на x и y, но не более того, то есть мы не можем использовать ничего кроме Num методов на x и y. Вот что такое ограничения класса типов и зачем они нужны. Они определяют общий интерфейс для функции.

Теперь рассмотрим ваше воображаемое ограничение not Num a. Какую информацию несет это заявление? Ну, мы знаем, что a не должно быть Num. Однако эта информация совершенно бесполезна для нас. Рассмотреть возможность:

f :: not Num a => a -> a
f = ???

Что можно поставить вместо ???? Очевидно, мы знаем, что мы не можем разместить. Но кроме того, эта подпись имеет не больше информации, чем

f :: a -> a

и единственной операцией f может быть id (ну undefined тоже возможно, но это уже другая история).

Наконец, рассмотрим ваш пример:

showSquare :: (Show a, not Num a) => a -> String
showSquare x = "I don't know how to square " ++ (show x)

Я не привожу первую часть вашего примера намеренно, см. первое предложение в моем ответе. У вас не может быть разных уравнений с разными типами. Но сама по себе эта функция совершенно бесполезна. Здесь можно смело убрать ограничение not Num a, и это ничего не изменит.

Единственное использование таких отрицательных ограничений в статически типизированном Haskell — это создание ошибок времени компиляции, когда вы указываете, скажем, Int для переменной с not Num a-ограничениями. Но я не вижу в этом никакой пользы.

person Vladimir Matveev    schedule 10.08.2012
comment
Ты знаешь, что делает boost::enable_if, Владимир? - person Andre; 10.08.2012
comment
Независимо от того, знает он это или нет, его ответ по-прежнему действителен - boost::enable_if имеет семантику, которая просто не имеет смысла в Haskell, у которого есть система типов, из-за которой C++ выглядит небрежно. - person Matthew Walton; 10.08.2012
comment
@Andre, я просмотрел статью на enable_if по ссылке в вопросе и, если я правильно понял, это какое-то указание домена, в котором может меняться параметр шаблона, то есть вещь, похожая на классы типов в Haskell. enable_if расширяет этот домен, а disable_if сужает его. Однако классы типов работают совершенно иначе, чем шаблоны. У них более сложные правила разрешения (особенно когда включены определенные расширения). У них также есть совершенно разные механизмы, лежащие в основе их работы. Все это делает бесполезными рассуждения об этом в контексте Haskell. - person Vladimir Matveev; 10.08.2012
comment
@MatthewWalton Я не думал, что его ответ недействителен, но не отвечал на вопрос. У меня сложилось впечатление, что Владимир мог бы улучшить свой ответ, узнав больше о enable_if. - person Andre; 10.08.2012

Если бы мне действительно нужно было что-то подобное (и я не верю, что когда-либо нуждался), я думаю, что это самый простой подход в Haskell:

class Show a => ShowSquare a where
  showSquare :: a -> String
  showSquare a = "I don't know how to square " ++ (show a)

instance ShowSquare Int where
  showSquare = showSquare'

instance ShowSquare Double where
  showSquare = showSquare'

-- add other numeric type instances as necessary

-- make an instance for everything else
instance Show a => ShowSquare a

showSquare' :: (Show a, Num a) => a -> String
showSquare' x = "The square of " ++ (show x) ++ " is " ++ (show (x * x))

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

Вероятно, вы могли бы заставить что-то работать, используя идеи из вики-страницы Advanced Overlap. Обратите внимание, что метод по-прежнему требует явного перечисления экземпляров, так что лучше ли это, вероятно, дело вкуса.

Также можно подойти к проблеме с помощью шаблона haskell, написав TH-сращивание вместо функции. Сращивание должно reify ''Num на месте вызова определить, находится ли экземпляр Num в области действия, а затем выбрать соответствующую функцию. Однако выполнение этой работы, вероятно, будет более проблематичным, чем просто запись экземпляров Num вручную.

person John L    schedule 10.08.2012
comment
Хорошо, что в любом Haskell-решении приходится вычислять экземпляры вручную (в том числе понимать, где использовать реализацию по умолчанию, которая здесь работает, но может не работать вообще). Это ясно из статей, но я не указал это прямо в своем ответе. - person Volker Stolz; 10.08.2012

Зависимость от «не Num a» очень хрупка в Haskell, но не в C++.

В C++ классы определены в одном размещенном (закрытом), в то время как классы типа Haskell открыты и могут иметь экземпляры, объявленные в модуле C данных из модуля A и класса из модуля B.

Руководящий принцип (без расширения) разрешения классов типов заключается в том, что импорт модуля, такого как «C», никогда не изменит предыдущее разрешение классов типов.

Код, который ожидал «не Num Custom», изменится, если какой-либо рекурсивно импортированный модуль (например, из другого пакета) определил «экземпляр Num Custom».

Существует дополнительная проблема с полиморфизмом. Рассмотрим функцию в модуле "D"

useSS :: Show a => a -> Int -> [String]
useSS a n = replicate n (showSquare a)

data Custom = Custom deriving Show
use1 :: Int -> String
use1 = useSS Custom -- no Num Custom in scope

Теперь рассмотрим модуль «E» в другом пакете, который импортирует указанный выше модуль «D».

instance Num Custom
use2 :: Int -> String
use2 = useSS Custom -- has a Num Custom now

Что должны оценить (use1 1) и (use2 1)? Вы хотите работать с языком с такими ловушками? Haskell принципиально пытается предотвратить существование этой ловушки.

Такого рода нерегламентированная перегрузка присутствует везде в разрешении C++, но именно этого Haskell и старался избежать. Возможно с расширениями GHC делать такие вещи, но нужно быть осторожным, чтобы не создавать опасных ловушек, и это не поощряется.

person Chris Kuklewicz    schedule 10.08.2012