Является ли возвратный тип (только) -полиморфизм в Haskell хорошей вещью?


29

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

class Foo a where
    foo::Int -> a

Некоторые из причин, по которым мне не нравится это:

Ссылочная прозрачность:

«В Haskell при одном и том же вводе функция всегда будет возвращать один и тот же вывод», но так ли это на самом деле? read "3"возвращает 3, когда используется в Intконтексте, но выдает ошибку, когда используется, скажем, в (Int,Int)контексте. Да, вы можете утверждать, что readон также принимает параметр типа, но, по моему мнению, неявность параметра типа заставляет его терять свою красоту.

Ограничение мономорфизма:

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

Тип по умолчанию:

Снова одна из самых раздражающих вещей о Хаскеле. Происходит, например, если вы передаете результат полиморфных функций при их выводе функциям, полиморфным при их вводе. Опять же, поправьте меня, если я ошибаюсь, но это не было бы необходимо без функций, тип возвращаемых данных не может быть определен их типом ввода (и полиморфными константами).

Таким образом, мой вопрос (рискуя быть отмеченным как «вопрос для обсуждения»): возможно ли создать язык, похожий на Haskell, где средство проверки типов запрещает подобные определения? Если да, каковы будут преимущества / недостатки этого ограничения?

Я вижу некоторые насущные проблемы:

Если бы, скажем, 2имел только тип Integer, 2/3не проверял бы тип больше с текущим определением /. Но в этом случае я думаю, что классы типов с функциональными зависимостями могут прийти на помощь (да, я знаю, что это расширение). Кроме того, я думаю, что гораздо более интуитивно понятно иметь функции, которые могут принимать разные типы ввода, чем иметь функции, которые ограничены в своих типах ввода, но мы просто передаем им полиморфные значения.

Ввод значений, как []и Nothingкажется мне, как более крепкий орешек. Я не думал о хорошем способе справиться с ними.

Ответы:


37

Я действительно считаю, что полиморфизм возвращаемых типов является одной из лучших особенностей классов типов. После того, как я использовал его некоторое время, мне иногда трудно вернуться к моделированию в стиле ООП, где у меня его нет.

Рассмотрим кодирование алгебры. В Haskell у нас есть класс типов Monoid(игнорируя mconcat)

class Monoid a where
   mempty :: a
   mappend :: a -> a -> a

Как мы могли бы закодировать это как интерфейс на языке OO? Короткий ответ: мы не можем. Это потому , что тип memptyявляется так (Monoid a) => aназываемым, типом возвращаемого значения полиморфизма. Умение моделировать алгебру невероятно полезно для ИМО. *

Вы начинаете свое сообщение с жалобы на «ссылочную прозрачность». Это поднимает важный вопрос: Haskell является ценностно-ориентированным языком. Таким образом, такие выражения, как read 3не нужно понимать как вещи, которые вычисляют значения, их также можно понимать как значения. Это означает, что реальная проблема не в полиморфизме возвращаемого типа: это значения с полиморфным типом ( []и Nothing). Если язык должен иметь их, то он действительно должен иметь полиморфные возвращаемые типы для согласованности.

Должны ли мы быть в состоянии сказать, []имеет тип forall a. [a]? Я так думаю. Эти функции очень полезны, и они делают язык намного проще.

Если бы у Haskell был подтип, полиморфизм []мог бы быть подтипом для всех [a]. Проблема в том, что я не знаю способа кодирования, при котором тип пустого списка не будет полиморфным. Рассмотрим, как это будет сделано в Scala (это короче, чем в каноническом статически типизированном языке ООП, Java)

abstract class List[A]
case class Nil[A] extends List[A]
case class Cons[A](h: A. t: List[A]) extends List[A]

Даже здесь, Nil()это объект типа Nil[A]**

Другое преимущество полиморфизма возвращаемого типа заключается в том, что он значительно упрощает встраивание по Карри-Говарду.

Рассмотрим следующие логические теоремы:

 t1 = forall P. forall Q. P -> P or Q
 t2 = forall P. forall Q. P -> Q or P

Мы можем тривиально описать их как теоремы в Haskell:

data Either a b = Left a | Right b
t1 :: a -> Either a b
t1 = Left
t2 :: a -> Either b a
t2 = Right

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


*. В комментариях ysdx указывает, что это не совсем верно: мы могли бы повторно реализовать классы типов, моделируя алгебру как другой тип. Как и Java:

abstract class Monoid<M>{
   abstract M empty();
   abstract M append(M m1, M m2);
}

Затем вы должны передать объекты этого типа с вами. В Scala есть понятие неявных параметров, которое позволяет избежать некоторых, но, по моему опыту, не всех, накладных расходов на явное управление этими вещами. Помещение ваших служебных методов (фабричных методов, двоичных методов и т. Д.) В отдельный F-ограниченный тип оказывается невероятно хорошим способом управления вещами в ОО-языке, который имеет поддержку обобщенных типов. Тем не менее, я не уверен, что я бы испортил этот шаблон, если бы у меня не было опыта моделирования вещей с помощью классов типов, и я не уверен, что другие люди будут.

Он также имеет ограничения, из коробки нет способа получить объект, который реализует класс типов для произвольного типа. Вы должны либо передать значения явно, использовать что-то вроде последствий Scala, либо использовать какую-то форму технологии внедрения зависимостей. Жизнь становится безобразной. С другой стороны, приятно, что у вас может быть несколько реализаций для одного и того же типа. Что-то может быть моноидом несколькими способами. Кроме того, ношение этих структур по отдельности делает ИМО более математически современным, конструктивным, чувствующим его. Таким образом, хотя я все еще предпочитаю делать это на Haskell, я, вероятно, переоценил свой случай.

Классы типов с полиморфизмом возвращаемых типов делают такие вещи простыми в обращении. Это не значит, что это лучший способ сделать это.

**. Йорг Миттаг отмечает, что это не совсем канонический способ сделать это в Scala. Вместо этого мы бы следовали стандартной библиотеке с чем-то вроде:

abstract class List[+A] ...  
case class Cons[A](head: A, tail: List[A]) extends List[A] ...
case object Nil extends List[Nothing] ...

Это использует поддержку Scala для нижних типов, а также для параметров ковариантного типа. Так что, Nilтипа Nilнет Nil[A]. На данный момент мы довольно далеки от Haskell, но интересно отметить, как Haskell представляет нижний тип

undefined :: forall a. a

То есть это не подтип всех типов, это полиморфно (sp) член всех типов.
Еще больше возвращаемого типа полиморфизма.


4
«Как мы можем закодировать это как интерфейс на языке OO?» Вы можете использовать алгебру первого класса: interface Monoid <X> {X empty (); Х дописать (Х, Х); } Однако вам нужно передать это явно (это делается за кулисами в Haskell / GHC).
ysdx

@ysdx Это правда. А в языках, которые поддерживают неявные параметры, вы получаете что-то очень похожее на классы типов haskell (как в Scala). Я знал об этом. Однако я хотел сказать, что это делает жизнь довольно трудной: мне приходится использовать контейнеры, которые хранят «класс типов» повсюду (чёрт!). Тем не менее, я, вероятно, должен был быть менее гиперболическим в своем ответе.
Филип JF

+1, отличный ответ. Один неуместный клеветник, хотя: Nilвероятно, должно быть, а case objectне case class.
Йорг Миттаг

@ Jörg W Mittag Это не совсем неважно. Я отредактировал, чтобы оставить свой комментарий.
Филипп JF

1
Спасибо за очень хороший ответ. Вероятно, мне потребуется немного времени, чтобы переварить / понять это.
Дайничи

12

Просто чтобы заметить заблуждение:

«В Haskell при одном и том же вводе функция всегда будет возвращать один и тот же вывод», но так ли это на самом деле? читать «3», возвращать 3 при использовании в контексте Int, но выдает ошибку при использовании, скажем, в (Int, Int) контексте.

То, что моя жена ездит на Subaru, а я на Subaru, не означает, что мы ездим на одной машине. То, что названы две функции read, не означает, что это одна и та же функция. Действительно read :: String -> Int, определяется в экземпляре Read Int, где read :: String (Int, Int)определяется в экземпляре Read (Int, Int). Следовательно, они совершенно разные функции.

Это явление распространено в языках программирования и обычно называется перегрузкой .


6
Я думаю, вы как бы упустили суть вопроса. В большинстве языков, которые имеют специальный полиморфизм, то есть перегрузку, выбор вызываемой функции зависит только от типов параметров, а не от типа возвращаемого значения. Это облегчает размышления о значении вызовов функций: просто начните с листьев дерева выражений и продолжайте свой путь "вверх". В Haskell (и небольшом числе других языков, поддерживающих перегрузку возвращаемого типа) вам, возможно, придется одновременно рассмотреть все дерево выражений, даже чтобы выяснить смысл крошечного подвыражения.
Лоуренс Гонсалвес

1
Я думаю, что вы попали в суть вопроса отлично. Даже Шекспир сказал: «Функция с любым другим именем будет работать так же хорошо». +1
Томас Эдинг

@Laurence Gonsalves - Вывод типа в Haskell не является прозрачным по ссылкам. Значение кода может зависеть от контекста, в котором он используется, из-за логического вывода типа, тянущего информацию внутрь. Это не ограничивается проблемами типа возврата. В Haskell эффективно встроен Prolog в систему типов. Это может сделать код менее понятным, но также имеет большие преимущества. Лично я думаю, что вид ссылочной прозрачности, который имеет наибольшее значение, это то, что происходит во время выполнения, потому что без него невозможно справиться с ленью.
Steve314

@ Steve314 Думаю, мне еще не доводилось видеть ситуацию, когда отсутствие референтно прозрачного вывода типов не делает код менее понятным. Чтобы рассуждать о чем -то сложном, нужно уметь мысленно «разбивать» на куски. Если вы скажете мне, что у вас есть кошка, я не думаю о облаке из миллиардов атомов или отдельных клеток. Аналогично, при чтении кода я делю выражения на их подвыражения. Haskell побеждает это двумя способами: «неверный» вывод типа и чрезмерно сложная перегрузка операторов. Сообщество Haskell также испытывает отвращение к паренсу, что делает его еще хуже.
Лоуренс Гонсалвес

1
@ LaurenceGonsalves Вы правы, что эта infixфункция может быть нарушена. Но это провал пользователей. OTOH, ограничения, как в Java, ИМХО не правильный путь. Чтобы увидеть это, посмотрите не далее, чем какой-то код, который имеет дело с BigIntegers.
Инго

7

Я действительно хотел бы, чтобы термин «полиморфизм возвращаемого типа» никогда не создавался. Это поощряет недопонимание того, что происходит. Достаточно сказать, что, хотя устранение «полиморфизма возвращаемого типа» было бы крайне случайным и выразительным убийством изменения, оно даже не решило бы удаленно проблемы, поднятые в вопросе.

Тип возврата ни в коем случае не является особенным. Рассмотреть возможность:

class Foo a where
    foo :: Maybe a -> Bool

x = foo Nothing

Этот код вызывает те же проблемы, что и «полиморфизм возвращаемых типов», и демонстрирует те же самые отличия от ООП. Никто не говорит о «первом аргументе типа полиморфизма».

Основная идея заключается в том, что реализация использует типы, чтобы определить, какой экземпляр использовать. Значения (времени выполнения) любого вида не имеют к этому никакого отношения. Действительно, это будет работать даже для типов, которые не имеют значений. В частности, программы на Haskell не имеют смысла без их типов. (По иронии судьбы, это делает Haskell языком церковного стиля, а не языком карри. У меня есть статья в блоге, в которой я работаю над этим.)


«Этот код вызывает все те же проблемы, что и« полиморфизм возвращаемого типа ». Нет, это не так. Я могу посмотреть на «Foo Nothing» и определить его тип. Это Бул. Не нужно смотреть на контекст.
Дайничи

4
На самом деле, код не проверяет тип, потому что компилятор не знает, как aэто происходит в случае «возвращаемого типа». Опять же, нет ничего особенного в типах возвращаемых данных. Нам нужно знать тип всех подвыражений. Посмотрим let x = Nothing in if foo x then fromJust x else error "No foo".
Дерек Элкинс

2
Не говоря уже о «полиморфизме второго аргумента»; такая функция, как Int -> a -> Bool, путем карри, на самом деле, Int -> (a -> Bool)и вот вы, полиморфизм в возвращаемом значении снова. Если вы позволите это где угодно, то это должно быть везде.
Райан Райх

4

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

Рассмотрим имя 1 . Он часто обозначает разные (но фиксированные) объекты:

  • 1 как в Integer
  • 1 как в реале
  • 1 как в квадратной матрице тождественности
  • 1 как в функции идентичности

Здесь важно отметить , что в каждом контексте , 1является фиксированной величиной. Другими словами, каждая пара имя-контекст обозначает уникальный объект. Без контекста мы не можем знать, какой объект мы обозначаем. Таким образом, контекст должен быть выведен (если возможно) или явно предоставлен. Хорошим преимуществом (кроме удобной записи) является возможность выражать код в общем контексте.

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

  • integer1 как в Integer
  • real1 как в реале
  • matrixIdentity1 как в квадратной матрице тождественности
  • functionIdentity1 как в функции идентичности

Здесь мы получаем имена, которые являются явными. Это позволяет нам получить контекст только из названия. Таким образом, только имя объекта необходимо для полного обозначения объекта.

Классы типа Хаскелла выбрали прежнюю схему обозначений. Теперь вот свет в конце туннеля:

readэто имя, а не значение (у него нет контекста), но read :: String -> aэто значение (у него есть как имя, так и контекст). Таким образом, функции read :: String -> Intи read :: String -> (Int, Int)буквально разные функции. Таким образом, если они не согласны с их входами, ссылочная прозрачность не нарушается.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.