haskell-notes

Possible cause: the monomorphism restriction applied to the following:

eq :: a0 -> a0 -> Bool (bound at MR.hs:4:1)

Probable fix: give these definition(s) an explicit type signature

or use -XNoMonomorphismRestriction

In the expression: (==)

In an equation for ‘eq’: eq = (==)

Failed, modules loaded: none.

Компилятор жалуется о том, что в определении для eq ему встретилась неопределённость и он не смог

вывести тип. Если же мы допишем недостающие типы:

module MR where

add :: Num a => a -> a -> a

add = (+)

eq :: Eq a => a -> a -> Bool

eq

= (==)

то всё пройдёт гладко:

Prelude> :l MR

[1 of 1] Compiling MR

( MR. hs, interpreted )

Ok, modules loaded: MR.

*MR> eq 2 3

False

Но оказывается, что если мы допишем аргументы у функций и сотрём объявления, компилятор сможет

вывести тип, и тип окажется общим. Это можно проверить в интерпретаторе. Для этого начнём новую сессию:

Prelude> let eq a b = (==) a b

Prelude> :t eq

eq :: Eq a => a -> a -> Bool

Prelude> let add a = (+) a

Prelude> :t add

add :: Num a => a -> a -> a

Запишите эти выражения в модуле без типов и попробуйте загрузить. Почему так происходит? По смыслу

определения

add a b = (+) a b

add

= (+)

ничем не отличаются друг от друга, но второе сбивает компилятор столку. Компилятор путается из-

за того, что второй вариант похож на определение константы. Мы с вами знаем, что выражение справа от

знака равно является функцией, но компилятор, посчитав аргументы слева от знака равно, думает, что это

возможно константа, потому что она выглядит как константа. У таких возможно-констант есть специальное

имя, они называются константными аппликативными формами (constant applicative form или сокращённо

CAF). Константы можно вычислять один раз, на то они и константы. Но если тип константы перегружен,

и мы не знаем что это за тип (если пользователь не подсказал нам об этом в объявлении типа), то нам

приходится вычислять его каждый раз заново. Посмотрим на пример:

Проверка типов | 53

res = s + s

s = someLongLongComputation 10

someLongLongComputation :: Num a => a -> a

Здесь значение s содержит результат вычисления какой-то большой-пребольшой функции. Перед компи-

лятором стоит задача вывода типов. По тексту можно определить, что у s и res некоторый числовой тип.

Проблема в том, что поскольку компилятор не знает какой тип у s конкретно в выражении s + s, он вы-

нужден вычислить s дважды. Это привело разработчиков Haskell к мысли о том, что все выражения, которые

выглядят как константы должны вычисляться как константы, то есть лишь один раз. Это ограничение называ-

ют ограничением мономорфизма. По умолчанию все константы должны иметь конкретный тип, если только

пользователь не укажет обратное в типе или не подскажет компилятору косвенно, подставив неопределённое

значение в другое значение, тип которого определён. Например, такой модуль загрузится без ошибок:

eqToOne = eq one

eq = (==)

one :: Int

one = 1

Только в этом случае мы не получим общего типа для eq: компилятор постарается вывести значение,

которое не содержит контекста. Поэтому получится, что функция eq определена на Int. Эта очень спорная

особенность языка, поскольку на практике получается так, что ситуации, в которых она мешает, возникают

гораздо чаще. Немного забегая вперёд, отметим, что это поведение компилятора по умолчанию, и его можно

изменить. Компилятор даже подсказал нам как это сделать в сообщении об ошибке:

Probable fix: give these definition(s) an explicit type signature

or use -XNoMonomorphismRestriction

Мы можем активировать расширение языка, которое отменяет это ограничение. Сделать это можно

несколькими способами. Мы можем запустить интерпретатор с флагом -XNoMonomorphismRestriction:

Prelude> :q

Leaving GHCi.

$ ghci -XNoMonomorphismRestriction

Prelude> let eq = (==)

Prelude> :t eq

eq :: Eq a => a -> a -> Bool

или в самом начале модуля написать:

{-# Language NoMonomorphismRestriction #-}

Расширение будет действовать только в рамках данного модуля.

3.5 Рекурсивные типы

Обсудим ещё одну особенность системы типов Haskell. Типы могут быть рекурсивными, то есть одним из

подтипов в определении типа может быть сам определяемый тип. Мы уже пользовались этим в определении

для Nat

data Nat = Zero | Succ Nat

Видите, во второй альтернативе участвует сам тип Nat. Это приводит к бесконечному числу значений. Та-

ким простым и коротким определением мы описываем все положительные числа. Рекурсивные определения

типов приводят к рекурсивным функциям. Помните, мы определяли сложение и умножение:

(+) a Zero

= a

(+) a (Succ b) = Succ (a + b)

(*) a Zero

= Zero

(*) a (Succ b) = a + (a * b)

54 | Глава 3: Типы

И та и другая функция получились рекурсивными. Они следуют по одному сценарию: сначала определяем

базу рекурсии~– тот случай, в котором мы заканчиваем вычисление функции, и затем определяем путь к

базе~– цепочку рекурсивных вызовов.

Рассмотрим тип по-сложнее. Списки:

data [a] = [] | a : [a]

Деревья значений для Nat напоминают цепочку конструкторов Succ, которая венчается конструктором

Zero. Дерево значений для списка отличается лишь тем, что теперь у каждого конструктора Succ есть отро-

Страницы: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162