haskell-notes

converge :: (Ord a, Num a) => a -> [a] -> a

converge eps (a:b:xs)

| abs (a b) <= eps

= a

| otherwise

= converge eps (b:xs)

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

roots :: (Ord a, Num a) => a -> a -> (a -> a) -> a

roots eps x0 f = converge eps $ iterate f x0

За счёт ленивых вычислений функции converge и iterate работают синхронно. Функция converge запра-

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

Запустим интерпретатор. Мы ленимся и не создаём новый модуль для такой “большой” функции. Опреде-

ляем её сразу в интерпретаторе.

Prelude> let converge eps (a:b:xs) = if abs (ab)<=eps then a else converge eps (b:xs) Prelude> let roots eps x0 f = converge eps $ iterate f x0

Найдём корень уравнения:

x( x ? 2) = 0

x 2 ? 2 x = 0

1 x 2 = x

2

Prelude> roots 0.001 5 (x -> x*x/2)

Метод завис, остаётся только нажать ctrl+c для остановки. На самом деле есть одно условие для сходи-

мости метода. Метод сойдётся, если модуль производной функции f меньше единицы. Иначе есть возмож-

ность, что мы будем бесконечно генерировать новые подстановки. Вычислим производную нашей функции:

d 1 x 2 = x

dx 2

Нам следует ожидать решения в интервале от минус единицы до единицы:

Prelude> roots 0.001 0.5 (x -> x*x/2)

3.0517578125e-5

Мы нашли решение, корень равен нулю. В этой записи Ne-5 означает N · 10 ? 5

9.5 Краткое содержание

В этой главе мы узнали о том как происходят вычисления в Haskell. Мы узнали, что они ленивые. Всё

вычисляется как можно позже и как можно меньше. Такие вычисления называются вычислениями по необ-

ходимости.

Также мы узнали о вычислениях по значению и вычислениях по имени.

• В вычислениях по значению редукция проводится от листьев дерева выражения к корню

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

152 | Глава 9: Редукция выражений

Вычисление по необходимости является улучшением вычисления по имени. Мы не дублируем выражения

во время применения. Мы сохраняем значения в памяти и подставляем в функцию ссылки на значения. После

вычисления значения происходит его обновление в памяти. Так если в одном месте выражение уже было

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

считаем готовое значение.

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

форме. Это значит что оно вычислено. Может находится в слабой заголовочной нормальной форме. Это значит,

что мы знаем хотя бы один конструктор в корне выражения. Также возможно выражение ещё не вычислялось,

тогда оно является отложенным (thunk).

Суть ленивых вычислений заключается в том, что они происходят синхронно. Если у нас есть композиция

двух функций:

g ( f x)

Внутренняя функция f не начнёт вычисления до тех пор пока значения не понадобятся внешней функции

g. О последствиях этого мы остановимся подробнее в отдельной главе. Значения могут потребоваться только

при сопоставлении с образцом. Когда мы хотим узнать какое из уравнений нам выбрать.

Иногда ленивые вычисления не эффективны по расходу памяти. Это происходит когда выражение состоит

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

борьбы с ленью. Это функция seq, энергичные образцы и энергичные типы данных.

Функция seq:

seq :: a -> b -> b

Сначала приводит к слабой заголовочной форме свой первый аргумент, а затем возвращает второй.

Взрывные образцы выполняют те же функции, но они используются в декомпозиции аргументов или в объ-

явлении типа.

9.6 Упражнения

• Потренируйтесь в понимании того как происходят ленивые вычисления. Вычислите на бумаге следу-

ющие выражения (если это возможно):

sum $ take 3 $ filter (odd . fst) $ zip [1 .. ] [1, undefined, 2, undefined, 3, undefined,

undefined]

take 2 $ foldr (+) 0 $ map Succ $ repeat Zero

take 2 $ foldl (+) 0 $ map Succ $ repeat Zero

• Функция seq приводит первый аргумент к СЗНФ, убедитесь в этом на таком эксперименте. Определите

тип:

data TheDouble = TheDouble { runTheDouble :: Double }

Он запаковывает действительные числа в конструктор. Определите для этого типа экземпляр класса

Num и посмотрите как быстро будет работать функция sum’ на таких числах. Как изменится скорость

если мы заменим в определении типа data на newtype? как изменится скорость, если мы вернём data,

но сделаем тип TheDouble энергичным? Поэкспериментируйте.

• Посмотрите на приведение к СЗНФ в энергичных типах данных. Определите два типа:

data Strict a = Strict ! a

data Lazy

a = Lazy

a

И повычисляйте в интерпретаторе различные значения с undefined, const, ($! ) и seq:

> seq (Lazy undefined) ”Hi”

> seq (Strict undefined) ”Hi”

> seq (Lazy (Strict undefined)) ”Hi”

> seq (Strict (Strict (Strict undefined))) ”Hi”

• Посмотрите на такую функцию вычисления суммы всех чётных и нечётных чисел в списке.

Упражнения | 153

sum2 :: [Int] -> (Int, Int)

sum2 = iter (0, 0)

where iter c

[]

= c

iter c

(x:xs) = iter (tick x c) xs

tick :: Int -> (Int, Int) -> (Int, Int)

Страницы: 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