Параметрический полиморфизм против специального полиморфизма - PullRequest
47 голосов
/ 18 июля 2011

Я хотел бы понять ключевое различие между параметрическим полиморфизмом, таким как полиморфизм универсальных классов / функций в языках Java / Scala / C ++ и "специальным" полиморфизмом в системе типов Хаскелла. Я знаком с языками первого типа, но я никогда не работал с Haskell.

Точнее:

  1. Как работает алгоритм вывода типа, например в Java отличается от вывода типа в Haskell?
  2. Пожалуйста, дайте мне пример ситуации, когда что-то может быть написано на Java / Scala, но не может быть написано на Haskell (в зависимости от модульных особенностей этих платформ тоже), и наоборот.

Заранее спасибо.

Ответы [ 3 ]

65 голосов
/ 30 июля 2011

Согласно TAPL , §23.2:

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

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

Итак, если выРассмотрим последовательные этапы истории, неуниверсальная официальная Java (известная как pre- J2SE 5.0 , послед. сентябрь 2004 г.) имела специальный полиморфизм - так что вы могли перегрузить метод - ноне параметрический полиморфизм, поэтому вы не можете написать общий метод .Впоследствии, конечно, вы могли сделать и то и другое.

Для сравнения, с самого начала в 1990 Хаскелл был параметрически полиморфным, то есть вы могли написать:

swap :: (A; B) -> (B; A)
swap (x; y) = (y; x)

где A и B - переменные типа, могут быть созданы для всех типов без предположений.

Но ранее не было конструкции, дающей полиморфизм ad-hoc , который предназначен дляПозволяет вам писать функции, которые применяются к нескольким , но не ко всем типам.Типовые классы были реализованы как способ достижения этой цели.

Они позволяют вам описать класс (что-то похожее на интерфейс Java), давая сигнатуру функций, которые вы хотите реализовать для вашего универсального типа.Затем вы можете зарегистрировать несколько (и, надеюсь, несколько ) экземпляров , соответствующих этому классу.Тем временем вы можете написать универсальный метод, такой как:

between :: (Ord a)  a -> a -> a -> Bool
between x y z = x ≤ y ^ y ≤ z

, где Ord - это класс, который определяет функцию (_ ≤ _).При использовании (between "abc" "d" "ghi") - это , разрешенное статически для выбора правильного экземпляра для строк (а не, например, целых чисел) - именно в тот момент, когда происходит перегрузка метода (Java).

Вы можете сделать что-то подобное в Java с ограниченными символами .Но ключевое отличие между Haskell и Java на этом фронте состоит в том, что только Haskell может автоматически выполнять передачу словаря : на обоих языках, учитывая два экземпляра Ord T, скажем b0 и b1, вы можетепостроить функцию f, которая принимает их в качестве аргументов и создает экземпляр для типа пары (b0, b1), используя, скажем, лексикографический порядок.Скажи теперь, что тебе дали (("hello", 2), ((3, "hi"), 5)).В Java вы должны запомнить экземпляры для string и int и передать правильный экземпляр (состоящий из четырех приложений f!), Чтобы применить between к этому объекту.Haskell может применить композиционность и выяснить, как построить правильный экземпляр, учитывая только наземные экземпляры и конструктор f (это, конечно, распространяется на другие конструкторы).


Теперь, что касается вывода типа (и это, вероятно, должно быть отдельным вопросом), для обоих языков это неполно , в том смысле, что вы всегда можете написать неаннотированная программа, для которой компилятор не сможет определить тип.

  1. для Haskell, это потому, что он имеет непредсказуемый (он же первоклассный) полиморфизм, для которого вывод типа неразрешим.Обратите внимание, что в этой точке Java ограничивается полиморфизмом первого порядка (то, на чем расширяется Scala).

  2. для Java, это потому, что она поддерживает контравариантный подтип .

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

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

    • По своей сути вывод Хаскелла основан на Хиндли-Милнера , который дает вам полные результаты сразу после вывода типа приложения, переменных типа (например, A иB в приведенном выше примере) можно создавать только с неполиморфными типами (я упрощаю, но это по сути полиморфизм в стиле ML, который вы можете найти, например, в Ocaml.).
    • недавний GHC позаботится о том, чтобы аннотация типа могла потребоваться только для привязки let или λ-абстракции, которая имеет тип не-Damas-Milner .
    • Haskell пытался держаться относительно близко к этому непогрешимому ядру даже на самых волосатых его участках (например, GADTs ).В любом случае, предлагаемые расширения почти всегда приводятся в документе с доказательством правильности вывода расширенного типа.
  2. Для Java вывод типав любом случае применяется гораздо более ограниченным образом :

    До выхода Java 5 в Java не было никакого вывода типа.Согласно языковой культуре Java, тип каждой переменной, метода и динамически размещаемого объекта должен быть явно объявлен программистом .Когда дженерики (классы и методы, параметризованные по типу) были введены в Java 5, язык сохранил это требование для переменных, методов и распределений .Но введение полиморфных методов (параметризованных по типу) диктовало, что либо (i) программист предоставляет аргументы типа метода на каждом сайте вызова полиморфного метода, либо (ii) язык поддерживает вывод аргументов типа метода.Чтобы избежать создания дополнительной технической нагрузки для программистов, разработчики Java 5 решили выполнить вывод типа для определения аргументов типа для полиморфных вызовов методов .( source , выделено мной)

    * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * GJ 1153 * kludgy добавление подстановочных знаков в качестве запоздалой мысли (Обратите внимание, что я не в курсе возможных исправлений, внесенных в J2SE 6.0, хотя).Большая концептуальная разница в подходе заключается в том, что логический вывод Java local , в том смысле, что предполагаемый тип выражения зависит только от ограничений, сгенерированных из системы типов, и от типов его подвыражений, но нев контексте.

    Обратите внимание, что партийная линия относительно неполного, а иногда и неправильного вывода типа относительно спокойна.В соответствии с спецификацией :

    Обратите внимание также, что вывод типа никак не влияет на надежность.Если выведенные типы являются бессмысленными, вызов приведет к ошибке типа.Алгоритм вывода типов следует рассматривать как эвристику, предназначенную для эффективной работы на практике.Если не удается получить желаемый результат, вместо него можно использовать явные параметры типа.

24 голосов
/ 24 октября 2012

Параметрический полиморфизм означает, что мы не заботимся о типе, мы будем реализовывать функцию одинаково для любого типа.Например, в Haskell:

length :: [a] -> Int
length [] = 0          
length (x:xs) = 1 + length xs

Нам не важно, какой тип элементов списка, нам просто важно, сколько их.

Ad-однако полиморфизм hoc (он же перегрузка метода) означает, что мы будем использовать другую реализацию в зависимости от типа параметра.

Вот пример на Хаскеле.Допустим, мы хотим определить функцию с именем makeBreakfast.

Если входным параметром является Eggs, я хочу, чтобы makeBreakfast вернул сообщение о том, как сделать яйца.

Если входным параметром является Pancakes, я хочу, чтобы makeBreakfast вернул сообщение о том, как приготовить блины.

Мы создадим класс типов с именем BreakfastFood, который реализует *Функция 1024 *.Реализация makeBreakfast будет отличаться в зависимости от типа ввода для makeBreakfast.

class BreakfastFood food where
  makeBreakfast :: food -> String

instance BreakfastFood Eggs where
  makeBreakfast = "First crack 'em, then fry 'em"

instance BreakfastFood Toast where
  makeBreakfast = "Put bread in the toaster until brown"

Согласно Концепциям в языках программирования Джона Митчелла ,

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

0 голосов
/ 19 июля 2011

Полное обсуждение того, что означают параметрический полиморфизм и специальный полиморфизм и в какой степени они доступны в Haskell и в Java, является длинным; однако, ваши конкретные вопросы могут быть решены намного проще:

Как алгоритм вывода типа, например в Java отличается от вывода типа в Haskell?

Насколько я знаю, Java не делает вывод типов. Так что разница в том, что Haskell делает это.

Пожалуйста, приведите пример ситуации, когда что-то может быть написано на Java / Scala, но не может быть написано на Haskell (в зависимости от модульных особенностей этих платформ), и наоборот.

Один очень простой пример того, что Haskell может сделать, чего не может Java, - определить maxBound :: Bounded a => a. Я не знаю достаточно Java, чтобы указать на то, что он может сделать, чего не может Хаскелл.

...