Що таке обмеження мономорфізму?


78

Мене бентежить те, як компілятор 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поліморфним?


Це канонічне питання про обмеження мономорфізму в Хаскелі, як обговорювалось у мета-питанні .


Чому раптове оголошення державної служби? Крім того, я думаю, що "вимкнути" слід, напевно, набагато помітніше рекомендувати у вашій відповіді.
dfeuer

2
@dfeuer Раптово? Мета-запитання було задано 4 місяці тому . Я опублікував чернетку відповіді нижче 2 тижні тому . Я також згадав ці два факти в чаті досить давно. Для мене це не "раптово". Завтра я побачу, що я можу зробити, щоб виділити найважливішу інформацію.
Бакуріу

1
Ах, я пропустив мета-посилання.
dfeuer

Відповіді:


105

Що таке обмеження мономорфізму?

Обмеження мономорфізму, як зазначено у вікі 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вимагають мономорфіческого типу, і іншу поведінку bewteen ghcі ghciпов'язано з різними недобросовісними правилами .

Коли це відбувається?

У Haskell, як визначено у звіті , є два різних типи прив’язок . Прив'язки функцій та прив'язки шаблонів. Прив'язка функції - це не що інше, як визначення функції:

f x = x + 1

Зверніть увагу, що їх синтаксис:

<identifier> arg1 arg2 ... argn = expr

Модульні охоронці та whereдекларації. Але вони насправді не мають значення.

де повинен бути принаймні один аргумент .

Прив'язка шаблону - це декларація форми:

<pattern> = expr

Знову ж таки, за модулем охоронці.

Зверніть увагу, що змінні є шаблонами , тому прив'язка:

plus = (+)

є прив'язка шаблону . Це прив'язка шаблону plus(змінної) до виразу (+).

Коли прив'язка шаблону складається лише з назви змінної, це називається простим прив'язуванням шаблону.

Обмеження мономорфізму стосується простих прив'язок шаблонів!

Ну, формально ми повинні сказати, що:

Група оголошень - це мінімальний набір взаємозалежних прив'язок.

Розділ 4.5.1 звіту .

А потім (розділ 4.5.5 звіту ):

дана група декларацій є необмеженою тоді і лише тоді, коли:

  1. кожна змінна в групі пов'язана прив'язкою функції (наприклад f x = x) або простим прив'язкою шаблону (наприклад plus = (+), розділ 4.4.3.2), і

  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потрібно якось сказати компілятору, який тип слід спробувати проаналізувати ...

Друге правило

  1. Будь-які мономорфні змінні типу, які залишаються, коли виведення типу для цілого модуля завершено, вважаються неоднозначними та вирішуються до певних типів за допомогою правил за замовчуванням (Розділ 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обмеження призводять до того, що за замовчуванням ()це майже марно.

Корисні посилання

Є багато ресурсів та дискусій щодо обмеження мономорфізму.

Ось декілька посилань, які я вважаю корисними, і які можуть допомогти вам зрозуміти тему або глибше в неї:

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.