Почему определение функций для всех типов одновременно не разрешено в Haskell?
Это, наверное, очень простой вопрос, но...
Функция, которая определяется как, скажем,
foo :: a -> 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
Почему существует такое ограничение?
5 ответов
Это особенность, и на самом деле это очень важно. Это сводится к свойству, известному как параметричность в теории языка программирования. Грубо говоря, это означает, что оценка никогда не должна зависеть от типов, которые являются переменными во время компиляции. Вы не можете смотреть на значение, если вы не знаете его конкретный тип статически.
Почему это хорошо? Это дает гораздо более сильные инварианты о программах. Например, по одному только типу вы знаете, что a -> a
должна быть функция идентичности (или расходится). Подобные "свободные теоремы" применимы ко многим другим полиморфным функциям. Параметричность также является основой для более продвинутых методов абстракции на основе типов. Например, тип ST s a
в Haskell (государственная монада) и тип соответствующего runST
функция, положиться на s
будучи параметрическим. Это гарантирует, что у запущенной функции нет возможности связываться с внутренним представлением состояния.
Это также важно для эффективной реализации. Программа не должна передавать дорогостоящую информацию о типах во время выполнения (стирание типов), и компилятор может выбирать перекрывающиеся представления для разных типов. В качестве примера последнего 0 и False и () и [] все представлены 0 во время выполнения. Это было бы невозможно, если бы была разрешена такая функция, как ваша.
У Haskell есть стратегия реализации, известная как "стирание типов": у типов нет вычислительной значимости, поэтому код, который вы генерируете, не должен их отслеживать. Это значительное благо для производительности.
Цена, которую вы платите за это преимущество в производительности, заключается в том, что типы не имеют вычислительной значимости: функция не может изменить свое поведение в зависимости от типа передаваемого аргумента. Если бы вы позволили что-то вроде
f () = "foo"
f [] = "bar"
тогда это свойство не будет правдой: поведение f
будет действительно зависеть от типа его первого аргумента.
Конечно, есть языки, которые допускают подобные вещи, особенно в зависимо типизированных языках, где типы вообще не могут быть стерты.
Для функции 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 может оперировать только той информацией, которую имеет компилятор о типе чего-либо. Это может показаться ограничительным, но на практике классы типов и параметрический полиморфизм настолько мощны, что я никогда не пропускаю динамическую типизацию. На самом деле меня обычно раздражает динамическая типизация, потому что я никогда не до конца понимаю, что же на самом деле есть.
Тип 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 полностью скрывает идею передачи параметров типа в качестве аргументов функциям.
Этот вопрос основан на ошибочной предпосылке, Хаскелл может сделать это! (хотя это обычно используется только в очень специфических обстоятельствах)
{-# 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
ограничение. Некоторые пакеты вводят свои собственные альтернативные функции, которые служат по существу той же цели. Без чего-то подобного этим классам это невозможно сделать.