haskell-notes

Haskell:

Succ (Succ (Succ Zero))

Time (Hour 13) (Minute 10) (Second 0)

Mul (Add One Ten) (Neg (Mul Six Zero))

За одним исключением, если конструктор бинарный, символьный (начинается с двоеточия), мы помеща-

ем его между аргументов:

(One :+ Ten) :* (Neg (Six :* Zero))

3.3 Структура функций

Функции описывают одни значения в терминах других. При этом важно понимать, что функция это лишь

новое имя, пусть и составное. Мы можем написать 5, или 2+3, это лишь два разных имени для одной кон-

станты. Теперь мы разобрались с тем, что константы это деревья. Значит функции строят одни деревья из

других. Как они это делают? Для этого этого в Haskell есть две операции: это композиция и декомпозиция де-

ревьев. С помощью композиции мы строим из простых деревьев сложные, а с помощью декомпозиции разбиваем

составные деревья на простейшие.

Композиция и декомпозиция объединены в одной операции, с которой мы уже встречались, это операция

определения синонима. Давайте вспомним какое-нибудь объявление функции:

(+) a

Zero

= a

(+) a

(Succ b)

= Succ (a + b)

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

части мы составляем новое дерево из тех значений, что были нами получены слева от знака равно. Или

посмотрим на другой пример:

show (Time h m s) = show h ++ ”:” ++ show m ++ ”:” ++ show s

Слева от знака равно мы также выделили из составного дерева (Time h m s) три его дочерних для корня

узла и связали их с переменными h, m и s. А справа от знака равно мы составили из этих переменных новое

выражение.

Итак операцию объявления синонима можно представить в таком виде:

name

декомпозиция

=

композиция

В каждом уравнении у нас три части: новое имя, декомпозиция, поступающих на вход аргументов, и

композиция нового значения. Теперь давайте остановимся поподробнее на каждой из этих операций.

Структура функций | 45

Композиция и частичное применение

Композиция строится по очень простому правилу, если у нас есть значение f типа a -> b и значение x

типа a, мы можем получить новое значение (f x) типа b. Это основное правило построения новых значений,

поэтому давайте запишем его отдельно:

f :: a -> b,

x :: a

—————————

(f x) :: b

Сверху от черты, то что у нас есть, а снизу от черты то, что мы можем получить. Это операция называется

применением или аппликацией.

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

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

аргументов, сколько у него было дочерних узлов (или подтипов). Так мы строили константы. Но в Haskell мы

можем с помощью применения строить функции на лету, передавая меньшее число аргументов, этот процесс

называется частичным применением или каррированием (currying). Поясним на примере, предположим у нас

есть функция двух аргументов:

add :: Nat -> Nat -> Nat

add a b = …

На самом деле компилятор воспринимает эту запись так:

add :: Nat -> (Nat -> Nat)

add a b = …

Функция add является функцией одного аргумента, которая в свою очередь возвращает функцию одного

аргумента (Nat -> Nat). Когда мы пишем в где-нибудь в правой части функции:

… =

(add Zero (Succ Zero))

Компилятор воспринимает эту запись так:

… =

((add Zero) (Succ Zero))

Присмотримся к этому выражению, что изменилось? У нас появились новые скобки, вокруг выражения

(add Zero). Давайте посмотрим как происходит применение:

add :: Nat -> (Nat -> Nat),

Zero :: Nat

———————————————-

(add Zero) :: Nat -> Nat

Итак применение функции add к Zero возвращает новую функцию (add Zero), которая зависит от одного

аргумента. Теперь применим к этой функции второе значение:

(add Zero) :: Nat -> Nat,

(Succ Zero) :: Nat

———————————————-

((add Zero) (Succ Zero)) :: Nat

И только теперь мы получили константу. Обратите внимание на то, что получившаяся константа не может

принять ещё один аргумент. Поскольку в правиле для применения функция f должна содержать стрелку, а

у нас есть лишь Nat, это значение может участвовать в других выражениях лишь на месте аргумента.

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

fun :: a1 -> a2 -> a3 -> a4 -> res

… = fun a b c d

На самом деле мы пишем

fun :: a1 -> (a2 -> (a3 -> (a4 -> res)))

… = (((fun a) b) c) d

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

Это очень удобно. Так, определив лишь одну функцию fun, мы получили в подарок ещё три функции

(fun a), (fun a b) и (fun a b c). С ростом числа аргументов растёт и число подарков. Если смотреть на

функцию fun, как на функцию одного аргумента, то она представляется таким генератором функций типа

a2 -> a3 -> a4 -> res, который зависит от параметра. Применение функций через пробел значительно

упрощает процесс комбинирования функций.

Поэтому в Haskell аргументы функций, которые играют роль параметров или специфических флагов, то

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

process :: Param1 -> Param2 -> Arg1 -> Arg2 -> Result

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