Два первых аргумента функции process выступают в роли параметров для генерации функций с типом
Arg1 -> Arg2 -> Result.
Давайте потренируемся с частичным применением в интерпретаторе. Для этого загрузим модуль Nat из
предыдущей главы:
Prelude> :l Nat
[1 of 1] Compiling Nat
( Nat. hs, interpreted )
Ok, modules loaded: Nat.
*Nat> let add = (+) :: Nat -> Nat -> Nat
*Nat> let addTwo = add (Succ (Succ Zero))
*Nat> :t addTwo
addTwo :: Nat -> Nat
*Nat> addTwo (Succ Zero)
Succ (Succ (Succ Zero))
*Nat> addTwo (addTwo Zero)
Succ (Succ (Succ (Succ Zero)))
Сначала мы ввели локальную переменную add, и присвоили ей метод (+) из класса Num для Nat. Нам
пришлось выписать тип функции, поскольку ghci не знает для какого экземпляра мы хотим определить этот
синоним. В данном случае мы подсказали ему, что это Nat. Затем с помощью частичного применения мы
объявили новый синоним addTwo, как мы видим из следующей строки это функция оного аргумента. Она
принимает любое значение типа Nat и прибавляет к нему двойку. Мы видим, что этой функцией можно
пользоваться также как и обычной функцией.
Попробуем выполнить тоже самое для функции с символьной записью имени:
*Nat> let add2 = (+) (Succ (Succ Zero))
*Nat> add2 Zero
Succ (Succ Zero)
Мы рассмотрели частичное применение для функций в префиксной форме записи. В префиксной фор-
ме записи функция пишется первой, затем следуют аргументы. Для функций в инфиксной форме записи
существует два правила применения.
Это применение слева:
(*) :: a -> (b -> c),
x :: a
——————————
(x *) :: b -> c
И применение справа:
(*) :: a -> (b -> c),
x :: b
——————————
(* x) :: a -> c
Обратите внимание на типы аргумента и возвращаемого значения. Скобки в выражениях (x*) и (*x)
обязательны. Применением слева мы фиксируем в бинарной операции первый аргумент, а применением
справа – второй.
Поясним на примере, для этого давайте возьмём функцию минус (—). Если мы напишем (2—) 1 то мы
получим 1, а если мы напишем (—2) 1, то мы получим —1. Проверим в интерпретаторе:
*Nat> (2—) 1
1
*Nat> (—2) 1
< interactive>:4:2:
Структура функций | 47
No instance for (Num (a0 -> t0))
arising from a use of syntactic negation
Possible fix: add an instance declaration for (Num (a0 -> t0))
In the expression: — 2
In the expression: (— 2) 1
In an equation for ‘it’: it = (— 2) 1
Ох уж этот минус. Незадача. Ошибка произошла из-за того, что минус является хамелеоном. Если мы
пишем —2, компилятор воспринимает минус как унарную операцию, и думает, что мы написали константу
минус два. Это сделано для удобства, но иногда это мешает. Это единственное такое исключение в Haskell.
Давайте введём новый синоним для операции минус:
*Nat> let (#) = (—)
*Nat> (2#) 1
1
*Nat> (#2) 1
—1
Эти правила левого и правого применения работают и для буквенных имён в инфиксной форме записи:
*Nat> let minus = (—)
*Nat> (2 ‘minus‘ ) 1
1
*Nat> ( ‘minus‘ 2) 1
—1
Так если мы хотим на лету получить новую функцию, связав в функции второй аргумент мы можем
написать:
… = … ( ‘fun‘ x) …
Частичное применение для функций в инфиксной форме записи называют сечением (section), они бывают
соответственно левыми и правыми.
Связь с логикой
Отметим связь основного правила применения с Modus Ponens, известным правилом вывода в логике:
a -> b,
a
————-
b
Оно говорит о том, что если у нас есть выражение из a следует b и мы знаем, что a истинно, мы смело
можем утверждать, что b тоже истинно. Если перевести это правило на Haskell, то мы получим: Если у нас
определена функция типа a -> b и у нас есть значение типа a, то мы можем получить значение типа b.
Декомпозиция и сопоставление с образцом
Декомпозиция применяется слева от знака равно, при этом наша задача состоит в том, чтобы опознать
дерево определённого вида и выделить из него некоторые поддеревья. Мы уже пользовались декомпозицией
много раз в предыдущих главах, давайте выпишем примеры декомпозиции:
not :: Bool -> Bool
not True
= …
not False
= …
xor :: Bool -> Bool -> Bool
xor a b = …
show :: Show a => a -> String
show (Time h m s) = …
addZero :: String -> String
addZero (a:[])
= …
addZero as
= …
(*)
a
Zero
= …
(*)
a
(Succ b)
= …
48 | Глава 3: Типы
Декомпозицию можно проводить в аргументах функции. Там мы видим строчную запись дерева, в узлах
стоят конструкторы (начинаются с большой буквы), переменные (с маленькой буквы) или символ безразлич-
ной переменой (подчёркивание).
С помощью конструкторов, мы указываем те части, которые обязательно должны быть в дереве для дан-
ного уравнения. Так уравнение
not True
= …
сработает, только если на вход функции поступит значение True. Мы можем углубляться в дерево значе-
ния настолько, насколько нам позволят типы, так мы можем определить функцию:
is7 :: Nat -> Bool
is7
(Succ (Succ (Succ (Succ (Succ (Succ (Succ Zero)))))))
= True
is7
_
= False
С помощью переменных мы даём синонимы поддеревьям. Этими синонимами мы можем пользоваться в
правой части функции. Так в уравнении
addZero (a:[])
мы извлекаем первый элемент из списка, и одновременно говорим о том, что список может содержать