Операторы в языке программирования Haskell

Операторы в отличие от функций вызываются в инфиксном стиле.
Prelude> 6 + 7
13

Однако это отличие можно убрать. Функции можно вызывать в операторном стиле, а операторы можно вызывать в функциональном стиле.
Prelude> 6 `max` 7
7 Prelude> (+) 6 7 13

Все операторы в Haskell бинарные, т.е. принимают ровно два аргумента. За одним исключением - унарный префиксный минус. Есть и бинарный минус. Из-за того, что в языке присутствует два этих оператора могут возникать коллизии и неудобства. Когда отрицательное число используется в качестве аргумента функции, то его нужно заключать в круглые скобки.
Prelude> - 7
-7
Prelude> (-) 5 3
2
Prelude> max (-5) 5
5


Синтаксис сечений

Имеется компактный синтаксис для частичного применения оператора к одному из своих аргументов. Рассмотрим например оператор деления. Оператор деления в функциональном стиле (/) 2 может быть представлен в эквивалентном синтаксисе (2 /) - такое выражение называется сечением оператора. Тут связывается первый аргумент оператора деления с некоторым числом. Такое выражение представляет собой функцию одного аргумента, которая примет другой аргумент оператора деления и возвратит результат. 
Prelude> (2 /) 4
0.5
Имеется возможность связать не только левый аргумент (т.е. первый аргумент в функциональном стиле), но и правый аргумент оператора деления.
Prelude> (/ 2) 4
2.0
Синтаксис сечений требует обязательного использования круглых скобок. Оператор заключенный в скобки со связанным аргументом (левым или правым) представляет собой сечение. У оператора - не существует правого сечения. Т.е. выражение (- 2) не является сечением оператора -, а это применение унарного оператора - к 2, т.е. это число -2.

Приоритет и ассоциативность операторов

Приоритет в Haskell представляет собой число от 0 и до 9. Чем больше число, тем выше приоритет. Применение функций имеет приоритет равный 10 - самый высокий. 
Prelude> 3 + 5 * 8
43
Prelude> sin 5 + 4
3.0410757253368614

Ассоциативность играет роль тогда, когда оператор не является ассоциативным. Результаты для не ассоциативной операции могут быть разными. Пример принятой в математике левой ассоциативности для оператора:
Prelude> (3 - 9) - 5
-11
Prelude> 3 - 9 - 5
-11
Про оператор вычитания говорят, что он лево-ассоциативен. При правой ассоциативности скобки расставляются начиная с правой стороны:
Prelude> 3 - (9 - 5)
-1

Для того чтобы задать ассоциативность и приоритет операторов в Haskell используются ключевые слова, которые начинаются  с infix. После этого для лево-ассоциативных операторов пишет l, а для право-ассоциативных операторов пишется r. Также можно просто не задавать ассоциативность. Далее указывается уровень приоритета операторов. И наконец имя оператора. Пример из стандартной библиотеки:
infixr 8 ^, `logBase`
infixl 7 *, /, `div`, `mod`
infixl 6 +, -
infix 4 ==, /=, >, >=, <, <=
Отсутствие ассоциативности означает, что операторы не могут быть использованы в цепочке. Например, если мы можем с помощью оператора сложения много складывать в длинной цепочке, то строить цепочку с помощью оператора равенства невозможно. В ситуации когда приоритет и ассоциативность не заданы, Haskell  полагает, что ассоциативность оператора левая и приоритет у него равен 9.

Применение оператора как функции изменяет приоритет:
Prelude> (*) 2 ((+) 1 4) ^ 2
100

Определение собственных операторов

В Haskell нет встроенных операторов. Все операторы определены в стандартной библиотеке. Более того программист может задавать свои собственные операторы. Для этого можно использовать символы из следующего набора:
! # $ % & * + . / < = > ? @ \ ^ | - ~
Однако можно строить и более сложные операторы, которые составлены их этих символов (например стандартный оператор <= составлен из двух символов).
Символ : тоже можно использовать для операторов, но этот символ играет специальную роль - он используется в инфиксных конструкторах данных. Поэтому начинать свой собственный оператор с символа : не надо. В середине его можно использовать безболезненно.

module Demo where
infixl 6 *+*
-- a *+* b = a ^ 2 + b ^ 2 -- инфиксный стиль
(*+*) a b = a ^ 2 + b ^ 2 -- префиксный стиль

*Demo> 3 *+* 4
25
*Demo> (*+*) 3 4
25

$ - оператор применения с низким приоритетом 

Применение функции к своему аргументы записывается следующим образом. Слева стоит функция, справа стоит оператор и между ними знак пробел. Знак пробела может рассматриваться как некоторый специальный оператор. Более того можно определить этот оператор каким-то другим образом. И таким образом записывать применение функции как: функция, оператор и аргумент этой функции.
f $ x = f x

Такой оператор уже определен в стандартной библиотеке и им можно сразу пользоваться.
Prelude> sin 0
0.0
Prelude> sin $ 0
0.0

Суть этого оператора в том что у него самый низкий из возможных нулевой приоритет. Это позволяет использоваться оператор с целью избавления от избыточных скобок.
Prelude> sin (pi / 2)
1.0
Prelude> sin $ pi / 2
1.0

Более того этому оператору присвоена правая ассоциативность:
  • f (g x (h y)) == f $ g x (h y) == f $ g x $ h y
  • logBase 4 (min 20 (9 + 7)) == logBase 4 $ min 20 $ 9 + 7
Правая ассоциативность говорит, что нужно расставить скобки соответствующим образом вправо.

Оператор композиции функций

Пускай у нас есть две полиморфные функции f и g. Оператор композиции этих функций - это такая функция, которая принимает f и g, а возвращает функцию которая берет некоторый аргумент, применяет сначала функцию g к этому аргументу, а потом к результату этого применения применяет функцию f.
f :: b -> c
g :: a -> b
x :: a
f (g x) :: c
\x -> f (g x) :: a -> c
Prelude> let compose f g = \x -> f (g x)
Prelude> :t compose
compose :: (t1 -> t) -> (t2 -> t1) -> t2 -> t

Точка представляет собой оператор композиции функций. Ассоциативность не очень важна потому что в Haskell композиция функция ассоциативна по определению. Поэтому в какую сторону расставлять скобочки не важно, но поскольку мы должны выбрать какую-то ассоциативность для оператора применения функции выбрана правая ассоциативность.
sumFstFst' = (+) `on` (\pp -> fst $ fst pp)

sumFstFst'' = (+) `on` (fst . fst)

Цепочка последовательных применений может быть заменена композицией:
doIt x = f ( g (h x) ) = f ( (g . h) x ) = ( f . (g . h) ) x
Переход с бесточечному стилю:
doIt = f . (g . h) = f . g . h


Функция одной переменной doItYourself выбирает наибольшее из переданного ей аргумента и числа 42, затем возводит результат выбора в куб и, наконец, вычисляет логарифм по основанию 2 от полученного числа. Эта функция реализована в виде:
doItYourself = f . g . h
f = logBase 2
g = (^3)
h = max 42


Оператор seq

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

Оператор seq нарушает ленивую семантику Haskell, при его использовании вычисления перестают быть ленивыми. seq это функция двух аргументов, которая возвращает свой второй аргумент, а первый пытается довести до WHNF. Если эта попытка приводит к расходимости, то вызов seq расходится. Смысл seq в том, что он добавляет в наш ленивый Haskell некоторый элемент энергичности.
{-
seq :: a -> b -> b -- определен как функция 2 аргументов
seq _|_ b = _|_ -- здесь используется конструкция основание, которая обозначает расходящиеся вычисления
seq a b = b
-}
Если первый аргумент оператора seq расходится, тогда он тоже расходится. Если же первый аргумент вычисляется до значения, то он просто возвращает свой второй аргумент игнорируя первый. Такой оператор не может быть реализован в Haskell, он является вычислительным примитивом, позволяющим форсировать вычисления. Он форсирует вычисления своего первого аргумента. seq запускает вычисление своего первого аргумента и если первый аргумент расходится, то seq расходится. Если же первый аргумент не расходится, то тогда возвращается значение его второго аргумента.
Prelude> seq 1 2
2
Prelude> seq undefined 2
*** Exception: Prelude.undefined
Prelude> seq (id undefined) 2
*** Exception: Prelude.undefined
Prelude> seq (undefined,undefined) 2
2
Prelude> seq (\x -> undefined) 2
2
seq форсирует вычисления не до нормальной формы, а до слабой заголовочной нормальной формы. Хотя оператор seq и форсирует вычисления, но это не полное форсирование, а форсирование не очень глубокое, до слабой заголовочной нормальной формы.

Строгий оператор применения $! или аппликации с вызовом по значению

Хотя функция seq хороша как примитив, однако ее использование не очень удобно. Поэтому в Haskell определен специальный оператор аппликации с вызовом по значению, который позволяет более удобно использоваться форсированные вычисления.
{-
($!) :: (a -> b) -> a -> b
f $! x = x `seq` f x
-}
Первый аргумент это некоторая произвольная функция. Второй аргумент это тип, который совпадает с типом аргумента функции. И наконец возвращаемое значение такое же как и у возвращаемого значения функции.

Первый аргумент `seq` это x - тот аргумент вычисление которого происходит до WHNF. После этого вычисленный x используется в аппликации f x. Возвращаемым значением оператора seq служит второй его аргумент, т.е. возвращается значение выражения f x. Фактически происходит следующее, функция f применяется к своему аргументу, но при этом аргумент вычисляется, до того как происходит применение этой функции.

Строгий оператор применения имеет тот же самый приоритет, что и оператор $, т.е. нулевой и ту же самую ассоциативность.

Prelude> const 42 undefined
42
Prelude> const 42 $ undefined
42
Prelude> const 42 $! undefined
*** Exception: Prelude.undefined
Форсирование вычислений с помощью этого оператора приводит к уменьшению определенности. Выражения получаются хуже определенными. Естественно оператор форсирования нужен не для того чтобы хуже определять функции. Оператор форсирования нужен для того чтобы в длинной цепочке вычислений не накапливались отложенные вычисления.


Пример с факториалом:
factorial7 :: Integer -> Integer
factorial7 n | n >= 0    = helper 1 n
             | otherwise = error "arg must be >= 0"
 where
  helper acc 0 = acc
  helper acc n = helper (acc * n) (n - 1)
Поскольку при вычислении факториала функция helper в правой части вызывается сразу же, то у нас до тех пор пока мы не дойдем до нулевого значения возникает некоторая цепочка. helper вызывает helper вызывает helper... значения подставляются и в первом аргументе накапливается цепочка произведений. Вся цепочка вычислений n, n-1, n-2, ..., 1 будет накапливаться прежде чем начнется вычисление произведения acc * n. Для того чтобы избавиться от этой цепочки вычислений можно сказать, что применение helper к своему первому аргументу должно быть строгим.
factorial8 :: Integer -> Integer
factorial8 n | n >= 0    = helper 1 n
             | otherwise = error "arg must be >= 0"
 where
  helper acc 0 = acc
  helper acc n = (helper $! (acc * n)) (n - 1)
В этой версии нет цепочки вычислений и при большом значении n у нас на каждом шаге будет вычисляться произведение (acc * n) прежде чем передаваться в следующий рекурсивный вызов функции helper.

Комментариев нет:

Отправить комментарий