Почему определение функции для всех типов одновременно не разрешено в Haskell?

Это, наверное, очень простой вопрос, но...

Функция, которая определяется, скажем, как

foo :: a -> Integer

обозначает функцию от любого типа до Integer. Если это так, то теоретически можно определить его для любого типа, например

foo 1 = 10
foo 5.3 = 100
foo (x:xs) = -1
foo  _     = 0

Но Haskell допускает только общее определение, например foo a = 0.

И даже если вы ограничите a одним из определенного класса типов, например, экземпляром класса типов Show:

foo :: (Show a) => a -> Integer

вы все еще не можете сделать что-то вроде

foo "hello" = 10
foo   _     = 0

хотя "hello" :: [Char] является экземпляром Show

Почему такое ограничение?


person user1425230    schedule 30.05.2012    source источник


Ответы (5)


Это особенность, и на самом деле очень фундаментальная. Это сводится к свойству, известному как параметричность в теории языков программирования. Грубо говоря, это означает, что вычисление никогда не должно зависеть от типов, являющихся переменными во время компиляции. Вы не можете смотреть на значение, если вы не знаете его конкретный тип статически.

Почему это хорошо? Это дает гораздо более сильные инварианты о программах. Например, вы знаете только из типа, что a -> a должна быть функцией тождества (или расходится). Подобные «свободные теоремы» применимы ко многим другим полиморфным функциям. Параметричность также является основой для более продвинутых методов абстракции на основе типов. Например, тип ST s a в Haskell (монада состояния) и тип соответствующей функции runST полагаются на то, что s является параметрическим. Это гарантирует, что работающая функция не сможет возиться с внутренним представлением состояния.

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

person Andreas Rossberg    schedule 30.05.2012
comment
Эта статья может оказаться актуальной. - person Chris Taylor; 30.05.2012

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

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

f () = "foo"
f [] = "bar"

тогда это свойство не было бы истинным: поведение f действительно зависело бы от типа его первого аргумента.

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

person Daniel Wagner    schedule 30.05.2012
comment
Теоретически возможно просто разбить определение f на все возможные типы во время компиляции, но для существования экзистенциальных типов. - person Louis Wasserman; 30.05.2012
comment
Это интересно. Когда я программировал на Java, меня часто раздражало стирание типов. Это означало, что List<T> не знал, что такое T во время выполнения, поэтому вы не могли написать код, подобный if (x instanceOf T) {...}. Но я никогда даже не замечал, что Haskell использует стирание типов. Интересно, почему это так. - person Chris Taylor; 30.05.2012
comment
Частично это связано с тем, что, поскольку в Haskell нет подтипов, вы почти всегда знаете точный тип каждой переменной во время компиляции, тогда как в Java вы можете получить разные реализации одного и того же интерфейса. В редких исключениях, таких как экзистенциально квантифицированные типы, вы преднамеренно отбрасывали информацию о типе, поэтому вы должны были знать, что вам не понадобится эта информация в будущем. - person Louis Wasserman; 30.05.2012
comment
@ChrisTaylor: Хороший вопрос, у меня был такой же опыт. Немного подумав об этом, я подозреваю, что причина в подтипах. Почти каждый раз, когда я хотел/должен был использовать instanceof, это было связано с подтипом. Кроме того, типы сумм позволяют вам писать код, который в основном работает так же, за исключением того, что вам нужно обернуть все в ADT. Это похоже на использование instanceof, за исключением того, что все возможности указаны явно. - person Tikhon Jelvis; 30.05.2012
comment
@ChrisTaylor Вероятно, потому что GHC использует стирание типов в качестве оптимизации, а Java использует стирание типов в качестве гарантии. typeOf x в Haskell вызовет правильный экземпляр Typeable для x. Хотя за универсальные функции приходится платить... они, конечно, не бесплатны. Общий налог будет уплачиваться во время компиляции, во время выполнения или в обоих случаях. - person Nathan Howell; 30.05.2012
comment
@ChrisTaylor: Вы никогда не заметите в Haskell, потому что вы никогда не используете отражение в Haskell. Checked downcasts и instanceof являются отражающими функциями - и это единственное место, где вы хотите смотреть на типы времени выполнения (Java также должна делать это для назначения массива из-за своих нарушенных правил подтипирования, но это другая история). В Haskell вам обычно не требуется отражение, потому что у вас есть более адекватные языковые конструкции для выражения типичных программных шаблонов, в частности, алгебраических типов данных. - person Andreas Rossberg; 30.05.2012
comment
На самом деле, однако, при наличии контекста Typeable Haskell действительно поддерживает функции, ожидаемые OP! - person Louis Wasserman; 30.05.2012
comment
Хотя стирание типов выполняется быстро по сравнению с динамической типизацией, обычно это не так быстро, как использование явно специализированного кода для каждого возможного типа (например, шаблонов C++), потому что тогда вам также не нужны словари/указатели виртуальных таблиц. К счастью, GHC тоже может это сделать, по крайней мере, для некоторых локальных функций. В отличие от Java, я думаю. - person leftaroundabout; 30.05.2012
comment
@LouisWasserman: Да, хорошо, если бы только Typeable не был таким ужасным, совершенно необоснованным взломом ... То, как это работает на самом деле, я не могу серьезно рассматривать это как отражение, а просто как мерзость. :) - person Andreas Rossberg; 30.05.2012
comment
Это справедливая позиция, и, честно говоря, я изо всех сил стараюсь ее избегать. Но это основной способ аппроксимации отражения в Haskell. - person Louis Wasserman; 30.05.2012

Для функции a -> Integer разрешено только одно поведение - возвращать постоянное целое число. Почему? Потому что вы понятия не имеете, что такое тип a. Без указания ограничений это может быть абсолютно что угодно, а поскольку Haskell статически типизирован, вам нужно разрешать все, что связано с типами, во время компиляции. Во время выполнения информация о типе больше не существует, и поэтому к ней нельзя обратиться — все выборы функций для использования уже сделаны.

Самое близкое, что Haskell допускает к такому поведению, - это использование классов типов - если вы создали класс типов с именем Foo с одной функцией:

class Foo a where
    foo :: a -> Integer

Затем вы можете определить его экземпляры для разных типов

instance Foo [a] where
    foo [] = 0
    foo (x:xs) = 1 + foo xs

instance Foo Float where
    foo 5.2 = 10
    foo _ = 100

Затем, пока вы можете гарантировать, что какой-то параметр x является Foo, вы можете вызвать для него foo. Вам все равно это нужно - вы не можете написать функцию

bar :: a -> Integer
bar x = 1 + foo x

Поскольку компилятор не знает, что a является экземпляром Foo. Вы должны сказать это или пропустить сигнатуру типа, и пусть она разберется сама.

bar :: Foo a => a -> Integer
bar x = 1 + foo x

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

person Matthew Walton    schedule 30.05.2012

Тип a -> Integer на самом деле не означает «функция от любого типа до Integer», как вы его описываете. Когда определение или выражение имеет тип a -> Integer, это означает, что для любого типа T можно специализировать или создать экземпляр этого определения или выражения в функцию типа T -> Integer.

Немного изменив нотацию, можно представить, что foo :: forall a. a -> Integer на самом деле является функцией двух аргументов: типа a и значения этого типа a. Или, с точки зрения каррирования, foo :: forall a. a -> Integer — это функция, которая принимает тип T в качестве аргумента и создает специализированную функцию типа T -> Integer для этого T. Используя функцию тождества в качестве примера (функция, которая выдает свой аргумент в качестве результата), мы можем продемонстрировать это следующим образом:

-- | The polymorphic identity function (not valid Haskell!)
id :: forall a. a -> a
id = \t -> \(x :: t) -> x

Идея реализации полиморфизма в качестве аргумента типа для полиморфной функции исходит из математической структуры под названием System F. , который Haskell фактически использует как один из своих теоретических источников. Однако Haskell полностью скрывает идею передачи параметров типа в качестве аргументов функциям.

person Luis Casillas    schedule 30.05.2012
comment
+1, в то время как в других ответах говорится несколько замечательных вещей, я считаю, что это объяснение неявной передачи типа - правильный ответ. Рассмотрим разницу в Typed Racket между типами (Any -> Integer) и (All (A) (A -> Integer)). Поскольку в Haskell нет подтипов, первое невозможно. - person Dan Burton; 30.05.2012
comment
+1, но есть одна вещь, которую я хотел бы добавить: универсальную количественную оценку можно рассматривать как функцию, принимающую аргумент дополнительного типа, только когда набор всех типов (* в Haskell) является абстрактным (т.е. вы не можете сопоставить его с образцом) . Если бы это было не так, у вас вполне могла бы быть функция \(t :: *) -> case t of Int -> .... Например, функция id в Agda имеет тип (A : Set) → A → A, поскольку нам не разрешено сопоставлять шаблоны с Set, id должна иметь форму id A x = x. - person Vitus; 31.05.2012

Этот вопрос основан на ошибочной предпосылке, Haskell может это сделать! (хотя обычно он используется только в очень специфических обстоятельствах)

{-# LANGUAGE ScopedTypeVariables, NoMonomorphismRestriction #-}

import Data.Generics

q1 :: Typeable a => a -> Int
q1 = mkQ 0 (\s -> if s == "aString" then 100 else 0)

q2 :: Typeable a => a -> Int
q2 = extQ q1 (\(f :: Float) -> round f)

Загрузите это и поэкспериментируйте с ним:

Prelude Data.Generics> q2 "foo"
0
Prelude Data.Generics> q2 "aString"
100
Prelude Data.Generics> q2 (10.1 :: Float)
10

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

Большинство так называемых универсальных функций (например, SYB) полагаются либо на ограничение Typeable, либо на ограничение Data. Некоторые пакеты вводят свои собственные альтернативные функции, по сути, для той же цели. Без чего-то вроде этих классов это невозможно сделать.

person John L    schedule 30.05.2012