Що таке обмеження мономорфізму?
Обмеження мономорфізму, як зазначено у вікі Haskell:
контр-інтуїтивне правило у виведенні типу Хаскелла. Якщо ви забули вказати підпис типу, іноді це правило заповнює вільні змінні типу конкретними типами, використовуючи правила "типу за замовчуванням".
Це означає, що, за певних обставин , якщо ваш тип неоднозначний (тобто поліморфний), компілятор вибере інстанцію цього типу до чогось, що не є двозначним.
Як це виправити?
Перш за все, ви завжди можете явно вказати підпис типу, і це дозволить уникнути спрацьовування обмеження:
plus :: Num a => a -> a -> a
plus = (+)
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
вимагають мономорфіческого типу, і іншу поведінку bewteen 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)] = читає t
Нагадаємо, 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
.
- всі ці класи визначені в Прелюдії або стандартній бібліотеці.
Змінна типу за замовчуванням замінюється першим типом у 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
розширень.
Змінні defaultable типу повинні не тільки з'являтися в контрсили , де всі класи є стандартними і має бути принаймні один клас , який є одним
з 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
обмеження призводять до того, що за замовчуванням ()
це майже марно.
Корисні посилання
Є багато ресурсів та дискусій щодо обмеження мономорфізму.
Ось декілька посилань, які я вважаю корисними, і які можуть допомогти вам зрозуміти тему або глибше в неї: