Что такое ограничение мономорфизма?
Я озадачен тем, как компилятор haskell иногда выводит типы, которые менее полиморфны, чем я ожидал, например, при использовании бессмысленных определений.
Похоже, проблема заключается в "ограничении мономорфизма", которое включено по умолчанию в более старых версиях компилятора.
Рассмотрим следующую программу на haskell:
{-# LANGUAGE MonomorphismRestriction #-}
import Data.List(sortBy)
plus = (+)
plus' x = (+ x)
sort = sortBy compare
main = do
print $ plus' 1.0 2.0
print $ plus 1.0 2.0
print $ sort [3, 1, 2]
Если я скомпилирую это с ghc
Я не получаю ошибок и вывод исполняемого файла:
3.0
3.0
[1,2,3]
Если я изменю main
тело к:
main = do
print $ plus' 1.0 2.0
print $ plus (1 :: Int) 2
print $ sort [3, 1, 2]
Я не получаю ошибок времени компиляции и вывод становится:
3.0
3
[1,2,3]
как и ожидалось. Однако, если я попытаюсь изменить его на:
main = do
print $ plus' 1.0 2.0
print $ plus (1 :: Int) 2
print $ plus 1.0 2.0
print $ sort [3, 1, 2]
Я получаю ошибку типа:
test.hs:13:16:
No instance for (Fractional Int) arising from the literal ‘1.0’
In the first argument of ‘plus’, namely ‘1.0’
In the second argument of ‘($)’, namely ‘plus 1.0 2.0’
In a stmt of a 'do' block: print $ plus 1.0 2.0
То же самое происходит при попытке позвонить sort
дважды с разными типами:
main = do
print $ plus' 1.0 2.0
print $ plus 1.0 2.0
print $ sort [3, 1, 2]
print $ sort "cba"
выдает следующую ошибку:
test.hs:14:17:
No instance for (Num Char) arising from the literal ‘3’
In the expression: 3
In the first argument of ‘sort’, namely ‘[3, 1, 2]’
In the second argument of ‘($)’, namely ‘sort [3, 1, 2]’
- Почему
ghc
вдруг подумать, чтоplus
не полиморфен и требуетInt
аргумент? Единственная ссылка наInt
находится в приложенииplus
как это может иметь значение, когда определение явно полиморфное? - Почему
ghc
вдруг подумать, чтоsort
требуетNum Char
пример?
Более того, если я попытаюсь поместить определения функций в их собственный модуль, как в:
{-# LANGUAGE MonomorphismRestriction #-}
module TestMono where
import Data.List(sortBy)
plus = (+)
plus' x = (+ x)
sort = sortBy compare
Я получаю следующую ошибку при компиляции:
TestMono.hs:10:15:
No instance for (Ord a0) arising from a use of ‘compare’
The type variable ‘a0’ is ambiguous
Relevant bindings include
sort :: [a0] -> [a0] (bound at TestMono.hs:10:1)
Note: there are several potential instances:
instance Integral a => Ord (GHC.Real.Ratio a)
-- Defined in ‘GHC.Real’
instance Ord () -- Defined in ‘GHC.Classes’
instance (Ord a, Ord b) => Ord (a, b) -- Defined in ‘GHC.Classes’
...plus 23 others
In the first argument of ‘sortBy’, namely ‘compare’
In the expression: sortBy compare
In an equation for ‘sort’: sort = sortBy compare
- Почему нет
ghc
в состоянии использовать полиморфный типOrd a => [a] -> [a]
заsort
? - И почему
ghc
лечитьplus
а такжеplus'
по-другому?plus
должен иметь полиморфный типNum a => a -> a -> a
и я действительно не вижу, как это отличается от типаsort
и пока толькоsort
выдает ошибку.
И последнее: если я прокомментирую определение sort
файл компилируется. Однако, если я попытаюсь загрузить его в ghci
и проверьте типы, которые я получаю:
*TestMono> :t plus
plus :: Integer -> Integer -> Integer
*TestMono> :t plus'
plus' :: Num a => a -> a -> a
Почему не тип для plus
полиморфный?
Это канонический вопрос об ограничении мономорфизма в Хаскеле, как обсуждалось в мета-вопросе.
1 ответ
Что такое ограничение мономорфизма?
Ограничение мономорфизма, как заявлено в вики Haskell:
нелогичное правило в выводе типа Haskell. Если вы забыли предоставить сигнатуру типа, иногда это правило будет заполнять переменные свободного типа определенными типами, используя правила "тип по умолчанию".
Это означает, что в некоторых случаях, если ваш тип неоднозначный (то есть полиморфный), компилятор выберет создание экземпляра этого типа для чего-то не двусмысленного.
Как мне это исправить?
Прежде всего вы всегда можете явно указать сигнатуру типа, и это позволит избежать срабатывания ограничения:
plus :: Num a => a -> a -> a
plus = (+) -- Okay!
-- Runs as:
Prelude> plus 1.0 1
2.0
В качестве альтернативы, если вы определяете функцию, вы можете избежать бессмысленного стиля и, например, написать:
plus x y = x + y
Выключить его
Можно просто отключить ограничение, чтобы вам не пришлось ничего делать со своим кодом, чтобы это исправить. Поведение контролируется двумя расширениями: MonomorphismRestriction
включит его (по умолчанию), пока NoMonomorphismRestriction
отключит это.
Вы можете поместить следующую строку в самый верх вашего файла:
{-# LANGUAGE NoMonomorphismRestriction #-}
Если вы используете GHCi, вы можете включить расширение, используя :set
команда:
Prelude> :set -XNoMonomorphismRestriction
Вы также можете сказать ghc
чтобы включить расширение из командной строки:
ghc ... -XNoMonomorphismRestriction
Примечание: вы действительно должны предпочесть первый вариант, а не выбирать расширение через параметры командной строки.
Обратитесь к странице GHC для объяснения этого и других расширений.
Полное объяснение
Ниже я постараюсь обобщить все, что вам нужно знать, чтобы понять, что такое ограничение мономорфизма, почему оно было введено и как оно ведет себя.
Пример
Примите следующее тривиальное определение:
plus = (+)
можно подумать, что сможет заменить каждое вхождение +
с plus
, В частности с (+) :: Num a => a -> a -> a
вы ожидаете также иметь plus :: Num a => a -> a -> a
,
К сожалению, это не случай. Например, в GHCi мы попробуем следующее:
Prelude> let plus = (+)
Prelude> plus 1.0 1
Мы получаем следующий вывод:
<interactive>:4:6:
No instance for (Fractional Integer) arising from the literal ‘1.0’
In the first argument of ‘plus’, namely ‘1.0’
In the expression: plus 1.0 1
In an equation for ‘it’: it = plus 1.0 1
Вам может понадобиться :set -XMonomorphismRestriction
в более новых версиях GHCi.
И на самом деле мы можем видеть, что тип plus
это не то, что мы ожидали:
Prelude> :t plus
plus :: Integer -> Integer -> Integer
Случилось так, что компилятор увидел, что plus
имел тип Num a => a -> a -> a
полиморфный тип. Более того, бывает так, что приведенное выше определение подпадает под правила, которые я объясню позже, и поэтому он решил сделать тип мономорфным, установив по умолчанию переменную типа a
, По умолчанию Integer
как мы можем видеть.
Обратите внимание, что если вы попытаетесь скомпилировать приведенный выше код, используя ghc
вы не получите никаких ошибок. Это связано с тем, как ghci
обрабатывает (и должен обрабатывать) интерактивные определения. В основном каждое заявление, введенное в ghci
должен быть полностью проверен на тип, прежде чем будет рассмотрено следующее; другими словами, это как если бы каждое утверждение было в отдельном модуле. Позже я объясню, почему это так.
Какой-то другой пример
Рассмотрим следующие определения:
f1 x = show x
f2 = \x -> show x
f3 :: (Show a) => a -> String
f3 = \x -> show x
f4 = show
f5 :: (Show a) => a -> String
f5 = show
Мы ожидаем, что все эти функции будут вести себя одинаково и иметь одинаковый тип, то есть тип show
: Show a => a -> String
,
Тем не менее, при составлении приведенных выше определений мы получаем следующие ошибки:
test.hs:3:12:
No instance for (Show a1) arising from a use of ‘show’
The type variable ‘a1’ is ambiguous
Relevant bindings include
x :: a1 (bound at blah.hs:3:7)
f2 :: a1 -> String (bound at blah.hs:3:1)
Note: there are several potential instances:
instance Show Double -- Defined in ‘GHC.Float’
instance Show Float -- Defined in ‘GHC.Float’
instance (Integral a, Show a) => Show (GHC.Real.Ratio a)
-- Defined in ‘GHC.Real’
...plus 24 others
In the expression: show x
In the expression: \ x -> show x
In an equation for ‘f2’: f2 = \ x -> show x
test.hs:8:6:
No instance for (Show a0) arising from a use of ‘show’
The type variable ‘a0’ is ambiguous
Relevant bindings include f4 :: a0 -> String (bound at blah.hs:8:1)
Note: there are several potential instances:
instance Show Double -- Defined in ‘GHC.Float’
instance Show Float -- Defined in ‘GHC.Float’
instance (Integral a, Show a) => Show (GHC.Real.Ratio a)
-- Defined in ‘GHC.Real’
...plus 24 others
In the expression: show
In an equation for ‘f4’: f4 = show
Так f2
а также f4
не компилировать Более того, при попытке определить эти функции в GHCi мы не получаем никаких ошибок, но тип для f2
а также f4
является () -> String
!
Ограничение мономорфизма - это то, что делает f2
а также f4
требуют мономорфного типа, и различное поведение между ghc
а также ghci
из-за различных правил по умолчанию.
Когда это происходит?
В Haskell, как определено в отчете, есть два разных типа привязок. Функциональные привязки и привязки к шаблону. Привязка функции - это не что иное, как определение функции:
f x = x + 1
Обратите внимание, что их синтаксис:
<identifier> arg1 arg2 ... argn = expr
Модуло охранников и where
деклараций. Но они на самом деле не имеют значения.
где должен быть хотя бы один аргумент.
Привязка к шаблону - это объявление формы:
<pattern> = expr
Опять по модулю охранников.
Обратите внимание, что переменные являются шаблонами, поэтому связывание:
plus = (+)
это шаблон привязки. Это обязательный шаблон plus
(переменная) в выражении (+)
,
Когда привязка шаблона состоит только из имени переменной, это называется простой привязкой шаблона.
Ограничение мономорфизма применяется к простым привязкам к шаблону!
Ну, формально мы должны сказать, что:
Группа объявлений - это минимальный набор взаимозависимых привязок.
Раздел 4.5.1 отчета.
А затем (раздел 4.5.5 отчета):
данная группа объявлений является неограниченной, если и только если:
каждая переменная в группе связана функцией привязки (например,
f x = x
) или простая привязка шаблона (например,plus = (+)
; Раздел 4.4.3.2) иявная сигнатура типа дается для каждой переменной в группе, которая связана простой привязкой к шаблону. (например
plus :: Num a => a -> a -> a; plus = (+)
).
Примеры, добавленные мной.
Таким образом, группа с ограниченным объявлением - это группа, в которой либо существуют непростые привязки к шаблону (например, (x:xs) = f something
или же (f, g) = ((+), (-))
) или существует какая-то простая привязка шаблона без сигнатуры типа (как в plus = (+)
).
Ограничение мономорфизма влияет на ограниченные группы объявлений.
Большую часть времени вы не определяете взаимно рекурсивные функции, и, следовательно, группа объявлений становится просто привязкой.
Что оно делает?
Ограничение мономорфизма описывается двумя правилами в разделе 4.5.5 отчета.
Первое правило
Обычное ограничение Хиндли-Милнера на полиморфизм состоит в том, что только переменные типа, которые не встречаются свободно в среде, могут быть обобщены. Кроме того, переменные ограниченного типа в группе ограниченного объявления не могут быть обобщены на этапе обобщения для этой группы. (Напомним, что переменная типа ограничена, если она должна принадлежать некоторому классу типа; см. Раздел 4.5.2.)
Выделенная часть - это то, что вводит ограничение мономорфизма. Это говорит о том, что если тип полиморфный (то есть содержит некоторую переменную типа) и эта переменная типа ограничена (то есть имеет ограничение класса: например, тип Num a => a -> a -> a
полиморфен, потому что содержит a
а также противоречил, потому что a
имеет ограничение Num
над этим.) тогда это не может быть обобщено.
Проще говоря, не обобщение означает, что использование функции plus
может изменить свой тип.
Если у вас были определения:
plus = (+)
x :: Integer
x = plus 1 2
y :: Double
y = plus 1.0 2
тогда вы получите ошибку типа. Потому что когда компилятор видит это plus
называется над Integer
в декларации x
это унифицирует переменную типа a
с Integer
и, следовательно, тип plus
будет выглядеть так:
Integer -> Integer -> Integer
но потом, когда он наберет, проверь определение y
он увидит что plus
применяется к Double
аргумент, и типы не совпадают.
Обратите внимание, что вы все еще можете использовать plus
без получения ошибки:
plus = (+)
x = plus 1.0 2
В этом случае тип plus
первый вывод, чтобы быть Num a => a -> a -> a
но затем его использование в определении x
, где 1.0
требует Fractional
ограничение, изменит его на Fractional a => a -> a -> a
,
обоснование
В отчете говорится:
Правило 1 требуется по двум причинам, обе из которых довольно тонки.
Правило 1 предотвращает неожиданное повторение вычислений. Например,
genericLength
это стандартная функция (в библиотекеData.List
) тип которого определяетсяgenericLength :: Num a => [b] -> a
Теперь рассмотрим следующее выражение:
let len = genericLength xs in (len, len)
Это выглядит, как если бы
len
должен вычисляться только один раз, но без правила 1 он может вычисляться дважды, по одному при каждой из двух разных перегрузок. Если программист действительно хочет, чтобы вычисление повторялось, можно добавить явную сигнатуру типа:let len :: Num a => a len = genericLength xs in (len, len)
На данный момент, пример из вики, на мой взгляд, более понятен. Рассмотрим функцию:
f xs = (len, len)
where
len = genericLength xs
Если len
был полиморфный тип f
было бы:
f :: Num a, Num b => [c] -> (a, b)
Таким образом, два элемента кортежа (len, len)
на самом деле могут быть разные значения! Но это означает, что вычисления сделаны genericLength
необходимо повторить, чтобы получить два разных значения.
Обоснование здесь таково: код содержит один вызов функции, но не введение этого правила может привести к двум вызовам скрытых функций, что противоречит интуиции.
С ограничением мономорфизма тип f
будет выглядеть так:
f :: Num a => [b] -> (a, a)
Таким образом, нет необходимости выполнять вычисления несколько раз.
Правило 1 предотвращает двусмысленность. Например, рассмотрим группу объявлений
[(n,s)] = читает т
Напомним, что
reads
стандартная функция, тип которой определяется сигнатуройчитает:: (Читать a) => Строка -> [(a, Строка)]
Без правила 1
n
будет назначен тип∀ a. Read a ⇒ a
а такжеs
тип∀ a. Read a ⇒ String
, Последний является недопустимым типом, потому что он по своей сути неоднозначен. Невозможно определить, какую перегрузку использоватьs
, и это не может быть решено путем добавления сигнатуры типа дляs
, Следовательно, когда используются непростые привязки к шаблону (раздел 4.4.3.2), выведенные типы всегда мономорфны в своих переменных ограниченного типа, независимо от того, предоставляется ли сигнатура типа. В этом случае обаn
а такжеs
мономорфны вa
,
Ну, я считаю, что этот пример говорит сам за себя. Существуют ситуации, когда неприменение правила приводит к неоднозначности типа.
Если вы отключите расширение, как предложено выше, вы получите ошибку типа при попытке скомпилировать вышеуказанное объявление. Однако это на самом деле не проблема: вы уже знаете, что при использовании read
Вы должны как-то сказать компилятору, какой тип он должен пытаться проанализировать...
Второе правило
- Любые переменные мономорфного типа, которые остаются, когда вывод типа для всего модуля завершен, считаются неоднозначными и разрешаются к определенным типам с использованием правил по умолчанию (раздел 4.3.4).
Это означает, что. Если у вас есть обычное определение:
plus = (+)
Это будет иметь тип Num a => a -> a -> a
где a
является переменной мономорфного типа в соответствии с правилом 1, описанным выше. После вывода всего модуля компилятор просто выберет тип, который заменит a
в соответствии с правилами по умолчанию.
Окончательный результат: plus :: Integer -> Integer -> Integer
,
Обратите внимание, что это делается после вывода всего модуля.
Это означает, что если у вас есть следующие объявления:
plus = (+)
x = plus 1.0 2.0
внутри модуля, перед типом по умолчанию тип plus
будет: Fractional a => a -> a -> a
(см. правило 1, почему это происходит). На данный момент, следуя правилам по умолчанию, a
будет заменен на Double
и так у нас будет plus :: Double -> Double -> Double
а также x :: Double
,
Дефолт
Как указывалось ранее, существуют некоторые правила по умолчанию, описанные в Разделе 4.3.4 Отчета, которые может принять эксперт, который заменит полиморфный тип на мономорфный. Это происходит всякий раз, когда тип неоднозначен.
Например, в выражении:
let x = read "<something>" in show x
здесь выражение неоднозначно, потому что типы для show
а также read
являются:
show :: Show a => a -> String
read :: Read a => String -> a
Итак x
имеет тип Read a => a
, Но это ограничение удовлетворяется многими типами: Int
, Double
или же ()
например. Какой выбрать? Там нет ничего, что может сказать нам.
В этом случае мы можем устранить неоднозначность, сообщив компилятору, какой тип нам нужен, добавив сигнатуру типа:
let x = read "<something>" :: Int in show x
Теперь проблема в том, что, поскольку Haskell использует Num
Тип класса для обработки чисел, во многих случаях числовые выражения содержат неоднозначности.
Рассматривать:
show 1
Каким должен быть результат?
Как прежде 1
имеет тип Num a => a
и есть много типов номеров, которые могут быть использованы. Какой выбрать?
Наличие ошибки компилятора почти каждый раз, когда мы используем число, не очень хорошая вещь, и поэтому были введены правила по умолчанию. Правила можно контролировать с помощью default
декларация. Указав default (T1, T2, T3)
мы можем изменить способ вывода по умолчанию различных типов.
Переменная неоднозначного типа v
по умолчанию, если:
v
появляется только в таких ограниченияхC v
мыC
это класс (то есть если он выглядит так:Monad (m v)
тогда это не дефолт).- по крайней мере, один из этих классов
Num
или подклассNum
, - все эти классы определены в Prelude или стандартной библиотеке.
Переменная типа по умолчанию заменяется первым типом в default
список, который является экземпляром всех классов неоднозначной переменной.
По умолчанию default
декларация default (Integer, Double)
,
Например:
plus = (+)
minus = (-)
x = plus 1.0 1
y = minus 2 1
Предполагаемые типы будут:
plus :: Fractional a => a -> a -> a
minus :: Num a => a -> a -> a
которые по умолчанию нарушают правила:
plus :: Double -> Double -> Double
minus :: Integer -> Integer -> Integer
Обратите внимание, что это объясняет, почему в примере в вопросе только sort
определение вызывает ошибку. Тип Ord a => [a] -> [a]
не может быть по умолчанию, потому что Ord
не числовой класс.
Расширенные настройки по умолчанию
Обратите внимание, что GHCi поставляется с расширенными правилами по умолчанию (или для GHC8), которые также можно включить в файлах, используя ExtendedDefaultRules
расширения.
Переменные типа по умолчанию должны появляться не только в тех ограничениях, где все классы являются стандартными, и должен быть хотя бы один класс из числа Eq
, Ord
, Show
или же Num
и его подклассы.
Кроме того, по умолчанию default
декларация default ((), Integer, Double)
,
Это может привести к странным результатам. Возьмем пример из вопроса:
Prelude> :set -XMonomorphismRestriction
Prelude> import Data.List(sortBy)
Prelude Data.List> let sort = sortBy compare
Prelude Data.List> :t sort
sort :: [()] -> [()]
в GHCI мы не получаем ошибку типа, но Ord a
ограничения приводят к дефолту ()
что в значительной степени бесполезно.
Полезные ссылки
Есть много ресурсов и дискуссий об ограничении мономорфизма.
Вот некоторые ссылки, которые я считаю полезными и которые могут помочь вам понять или углубиться в тему:
- Вики-страница на Haskell: ограничение мономорфизма
- Отчет
- Доступный и красивый пост в блоге
- Разделы 6.2 и 6.3 " Истории Хаскелла: быть ленивым с классом" имеют дело с ограничением мономорфизма и типом по умолчанию