Haskell использует классы типов внутри сигнатур типов

Предполагая простую подпись с ограничениями по классу типов:

f :: (Eq a, Num b) => a -> b
f str = 4

Мне было интересно, почему это не сработало

f :: (Eq a) -> (Num b)
f str = 4

f :: Eq -> Num
f str = 4

Я знаю, что классы типов имеют вид * -> Constraint, тогда как сигнатуры типов принимают только виды *.

Но у меня вопрос: почему существует это ограничение? Почему нельзя использовать классы типов как типы? Каковы были бы преимущества и недостатки предоставления возможности использовать классы типов, такие как типы?


person CMCDragonkai    schedule 23.12.2014    source источник
comment
Что делать со значением типа Eq?   -  person luqui    schedule 23.12.2014
comment
Это просто надуманный пример. Меня не интересует реализация функции.   -  person CMCDragonkai    schedule 23.12.2014
comment
А что насчет Functor? Это (* -> *) -> Constraint. Как это записать? (Functor f) something или (Functor f) something? What if something` нужно удерживать другое ограничение? Более того, что, если я захочу использовать один и тот же тип в разных местах, например ([Ord a] -> [Ord a]). Последнее кажется довольно многословным. И напоследок: а что насчет Num a, Ord a, Show a? Как использовать ваш синтаксис в этой ситуации?   -  person Zeta    schedule 23.12.2014
comment
@CMCDragonkai, я знаю, я задаю вопрос, чтобы помочь вам понять, почему может быть такое ограничение ...   -  person luqui    schedule 23.12.2014
comment
@Zeta: ну, я не предлагаю заменять текущий стиль использования классов типов, просто есть ли какие-либо причины, по которым нельзя использовать классы типов в качестве типов, кроме многословности?   -  person CMCDragonkai    schedule 23.12.2014


Ответы (2)


Существует (если пренебречь неупакованными типами) только один вид, типы которого на самом деле имеют какие-либо значения: *. Все остальные типы не содержат типов как таковых, а только «сущности уровня типа». Компилятор может использовать их, чтобы определить что делать с фактическими типами и их значениями, но никогда невозможно иметь во время выполнения значение типа с чем-то вроде * -> Constraint.

То, что * тип для типов-значений, - это просто правило игры. Это хорошо по той же причине, по которой неплохо иметь сильную систему статических типов, которая предотвращает бессмысленные преобразования во время выполнения. Wat ?? < / sup> Или, давайте понимать это буквально, по той же причине, по которой вы не можете просто перепрыгивать своим королем через пешки, независимо от того, насколько привлекательно эта функция может выглядеть в конкретной ситуации, с которой вы столкнулись.

Если бы какое-то расширение действительно позволяло создавать значения из не *-родственных типов, в частности * -> Constraint, нам понадобилась бы целая куча неочевидных определений, чтобы прояснить, как на самом деле предполагается использовать эти «значения класса». Скорее всего, это будет тип записи, содержащий методы класса в качестве словаря. Но как именно ... спецификация могла бы стать настоящим кошмаром. И усложнение самого языка таким образом никоим образом не стоит затрат, поскольку 1. стандартный способ использования классов типов подходит как минимум для 95% всех приложений, а 2. когда вам действительно нужны классы овеществленного типа, вы можете легко сделать это с помощью GADT, ConstraintKinds или даже старых простых записей словаря, определенных вручную. Ничто из этого не требует искажения фундаментальных представлений о том, как язык обрабатывает значения, в отличие от не-*-типов.


В любом случае ... давайте на минутку рассмотрим, как это может работать. Одно можно сказать наверняка: это не позволит вам писать что-нибудь столь же простое, как f str = 4!

Учитывать

f1 :: forall a, b . Eq a -> Num b

Оба Eq a, Num b :: Constraint, поэтому у нас будут значения типа Constraint. По сути, это будет особый словарь методов для данного экземпляра. Таким образом, реализация f1 должна выглядеть примерно так:

f1 (EqDict (d_eq :: a -> a -> Bool))
       = NumDict { dict_fromInteger = ??? :: Integer -> b
                 , dict_plus        = ??? :: b -> b -> b
                 , ...
                 , dict_signum      = ??? :: b -> b
                 }

Очевидно, что нет значимого способа определить все эти методы в результате. Все, что вы могли делать с таким «классом-функцией», - это «проецироваться» от более сильного класса к более слабому. Например. вы могли бы определить

monadApp :: forall m . Monad m -> Applicative m
monadApp (MonadDict {dict_return = d_ret, dict_bind = d_bd})
        = ApplicativeDict { dict_pure = d_ret
                          , dict_app = \fs vs -> d_bd fs (\f -> d_bd vs $ d_ret . f) }

Этот конкретный вариант на самом деле был бы несколько полезен, но только потому, что Monad (все еще, но не надолго!) отсутствует Applicative в качестве суперкласса, который он просто должен иметь. Обычно не должно быть особых причин для явного «понижения» каких-либо классов, потому что отношения суперкласса (или кортеж-ConstraintKinds) делают это автоматически.

person leftaroundabout    schedule 23.12.2014
comment
Итак, вы говорите, что если бы классы типов были доступны как переменные типа, нам понадобился бы какой-то способ деконструирования значения ограничения, которое теперь, вероятно, было бы реализовано как словарь. Принимая во внимание то, что мы сейчас используем оператор => в юниверсе типов, это не говорит о том, что a в Eq a является ограничением, но что что бы ни было a, это просто экземпляр Eq. Я всегда считал классы типов просто еще одним типом. Но вы называете их типами. - person CMCDragonkai; 23.12.2014
comment
Да, это почти все. Классы типов - это вовсе не просто другой тип, в отличие от объектно-ориентированных классов. Скорее воспринимайте их как математические теоремы, особые свойства, которым обладают различные типы. Тогда instances являются доказательствами этих теорем для определенных типов. - person leftaroundabout; 23.12.2014
comment
Я сейчас читаю этот документ lambda-the-ultimate.org/node/3837 & i.cs.hku.hk/~bruno/papers/Objects.pdf Похоже, есть способ объединить эти конструкции во что-то более общее. - person CMCDragonkai; 23.12.2014
comment
Вы можете обобщить все в нечто более общее ... но обычно в Haskell вам лучше думать о типах как о простых data (что на самом деле может увести вас довольно далеко в территорию объектно-ориентированного программирования!), И классы типов как нечто полностью ортогональное. - person leftaroundabout; 23.12.2014

Переменные типа могут появляться в сигнатуре типа более одного раза:

f :: Eq a => a -> a -> a -> Bool

Написав вышеизложенное как

f :: Eq a -> Eq a -> Eq a -> Bool

выглядит неудобно. Хуже того, у нас может быть несколько ограничений для a:

g :: (Show a, Eq a) => a -> Bool

Как бы мы записали это в альтернативных обозначениях?

g :: (Show & Eq) a -> Bool    -- ??

Полное игнорирование a, как вы предлагаете в последнем примере, делает сигнатуры неоднозначными: рассмотрите

h1 :: (Eq a)       => a -> a -> a -> a -> Bool
h2 :: (Eq a, Eq b) => a -> a -> b -> b -> Bool

Это совершенно разные сигнатуры: вы можете вызывать h2 1 2 [1] [2], но не h1 1 2 [1] [2], поскольку последний требует, чтобы четыре аргумента были одного типа. После использования предложенного соглашения они сводятся к одной и той же сигнатуре:

h12 :: Eq -> Eq -> Eq -> Eq -> Bool

Звонок h12 1 2 [1] [2] действителен? Вышеупомянутая подпись слишком двусмысленна, чтобы ее сказать.

person chi    schedule 23.12.2014
comment
Использование только Eq кажется неоднозначным, но я, вероятно, предполагаю, как новый пользователь, что это просто означает, что все параметры должны быть экземпляром Eq, и нет никаких дополнительных ограничений. Таким образом, они могут быть все одного типа или все могут быть разных типов, без ограничений. (Show & Eq) a похоже, что это та же проблема, что и с использованием типов объединения / суммы. Вам понадобится какой-то союз Show & Eq. - person CMCDragonkai; 23.12.2014
comment
@CMCDragonkai Значит, f :: Eq -> Eq -> Bool ; f x y = (x==y) будет недействительным? В вашей интерпретации это будет соответствовать f :: (Eq a, Eq b) => a -> b -> Bool ; f x y = (x==y), что вызывает ошибку типа, поскольку вы не можете сравнивать вещи разных типов a и b. - person chi; 23.12.2014
comment
Я бы предположил, что Eq сам по себе будет неявно иметь префикс forall для всех Eq. Это будет означать, что каждый из параметров вашего типа будет любым типом, являющимся экземпляром Eq. - person CMCDragonkai; 24.12.2014
comment
@CMCDragonkai Если она должна быть обоих типов Int и Stringвсех других Eq типов), единственное значение, которое вы можете передать такой функции, - это bottom, то есть undefined или error "message". Вы не можете передать 4 (поскольку это не строка) или "abc" (не целое число). Фактически вы можете попробовать это в текущем GHC, объявив type MyEq = forall a. Eq a => a, а затем f :: MyEq -> MyEq -> .... Если вместо этого он должен быть некоторого Eq типа, тогда все типы будут рассматриваться как разные, например первый Eq мог быть Int, второй String - не позволял сравнивать x==y между ними. - person chi; 24.12.2014
comment
Да, я вижу, как такая сигнатура типа действительно дает возможность сбоя во время выполнения. - person CMCDragonkai; 25.12.2014