Миран Липовача - Изучай Haskell во имя добра!
sqrtSums :: Int
sqrtSums = length (takeWhile (< 1000) (scanl1 (+) (map sqrt [1..]))) + 1
ghci> sqrtSums
131
ghci> sum (map sqrt [1..131])
1005.0942035344083
ghci> sum (map sqrt [1..130])
993.6486803921487
Мы задействовали функцию takeWhile вместо filter, потому что последняя не работает на бесконечных списках. В отличие от нас, функция filter не знает, что список возрастает, поэтому мы используем takeWhile, чтобы отсечь список, как только сумма превысит 1000.
Применение функций с помощью оператора $
Пойдём дальше. Теперь объектом нашего внимания станет оператор $, также называемый аппликатором функций. Прежде всего посмотрим, как он определяется:
($) :: (a –> b) –> a –> b
f $ x = f x
Зачем? Что это за бессмысленный оператор? Это просто применение функции! Верно, почти, но не совсем!.. В то время как обычное применение функции (с пробелом) имеет высший приоритет, оператор $ имеет самый низкий приоритет. Применение функции с пробелом левоассоциативно (то есть f a b c i – это то же самое, что (((f a) b) c)), в то время как применение функции при помощи оператора $ правоассоциативно.
Всё это прекрасно, но нам-то с того какая польза? Прежде всего оператор $ удобен тем, что с ним не приходится записывать много вложенных скобок. Рассмотрим выражение sum (map sqrt [1..130]). Поскольку оператор $ имеет самый низкий приоритет, мы можем переписать это выражение как sum $ map sqrt [1..130], сэкономив драгоценные нажатия на клавиши. Когда в функции встречается знак $, выражение справа от него используется как параметр для функции слева от него. Как насчёт sqrt 3 + 4 + 9? Здесь складываются 9, 4 и корень из 3. Если мы хотим получить квадратный корень суммы, нам надо написать sqrt (3 + 4 + 9) – или же (в случае использования оператора $) sqrt $ 3 + 4 + 9, потому что у оператора $ низший приоритет среди всех операторов. Вот почему вы можете представить символ $ как эквивалент записи открывающей скобки с добавлением закрывающей скобки в крайней правой позиции выражения.
Посмотрим ещё на один пример:
ghci> sum (filter (> 10) (map (*2) [2..10]))
80
Очень много скобок, даже как-то уродливо. Поскольку оператор $ правоассоциативен, выражение f (g (z x)) эквивалентно записи f $ g $ z x. Поэтому пример можно переписать:
sum $ filter (> 10) $ map (*2) [2..10]
Но кроме избавления от скобок оператор $ означает, что само применение функции может использоваться как и любая другая функция. Таким образом, мы можем, например, применить функцию к списку функций:
ghci> map ($ 3) [(4+), (10*), ( 2), sqrt]
[7.0,30.0,9.0,1.7320508075688772]
Функция ($ 3) применяется к каждому элементу списка. Если задуматься о том, что она делает, то окажется, что она берёт функцию и применяет её к числу 3. Поэтому в данном примере каждая функция из списка применится к тройке, что, впрочем, и так очевидно.
Композиция функций
В математике композиция функций определяется следующим образом:
(f ° g)(x) = f (g (x))
Это значит, что композиция двух функций создаёт новую функцию, которая, когда её вызывают, скажем, с параметром x, эквивалентна вызову g с параметром x, а затем вызову f с результатом первого вызова в качестве своего параметра.
В языке Haskell композиция функций понимается точно так же. Мы создаём её при помощи оператора (.), который определён следующим образом:
(.) :: (b –> c) –> (a –> b) –> a –> c
f . g = x –> f (g x)
По декларации типа функция f должна принимать параметр того же типа, что и результат функции g. Таким образом, результирующая функция принимает параметр того же типа, что и функция g, и возвращает значение того же типа, что и функция f. Выражение negate . (* 3) возвращает функцию, которая принимает число, умножает его на три и меняет его знак на противоположный.
Одно из применений композиции функций – это создание функций «на лету» для передачи их другим функциям в качестве параметров. Конечно, мы можем использовать для этого анонимные функции, но зачастую композиция функций понятнее и лаконичнее. Допустим, что у нас есть список чисел и мы хотим сделать их отрицательными. Один из способов сделать это – получить абсолютное значение числа (модуль), а затем перевести его в отрицательное, вот так:
ghci> map (x –> negate (abs x)) [5,–3,–6,7,–3,2,–19,24]
[–5,–3,–6,–7,–3,–2,–19,–24]
Обратите внимание на анонимную функцию и на то, как она похожа на результирующую композицию функций. А вот что выйдет, если мы воспользуемся композицией:
ghci> map (negate . abs) [5,–3,–6,7,–3,2,–19,24]
[–5,–3,–6,–7,–3,–2,–19,–24]
Невероятно! Композиция функций правоассоциативна, поэтому у нас есть возможность включать в неё много функций за один раз. Выражение f (g (z x)) эквивалентно (f . g . z) x. Учитывая это, мы можем превратить
ghci> map (xs –> negate (sum (tail xs))) [[1..5],[3..6],[1..7]]
[–14,–15,–27]
в
ghci> map (negate . sum . tail) [[1..5],[3..6],[1..7]]
[–14,–15,–27]
Функция negate . sum . tail принимает список, применяет к нему функцию tail, суммирует результат и умножает полученное число на -1. Получаем точный эквивалент анонимной функции из предыдущего примера.
Композиция функций с несколькими параметрами
Ну а как насчёт функций, которые принимают несколько параметров? Если мы хотим использовать их в композиции, обычно мы частично применяем их до тех пор, пока не получим функцию, принимающую только один параметр. Запись
sum (replicate 5 (max 6.7 8.9))
может быть преобразована так:
(sum . replicate 5) (max 6.7 8.9)
или так:
sum . replicate 5 $ max 6.7 8.9
Функция replicate 5 применяется к результату вычисления max 6.7 8.9, после чего элементы полученного списка суммируются. Обратите внимание, что функция replicate частично применена так, чтобы у неё остался только один параметр, так что теперь результат max 6.7 8.9 передаётся на вход replicate 5; новым результатом оказывается список чисел, который потом передаётся функции sum.
Если вы хотите переписать выражение с кучей скобок, используя функциональную композицию, можно сначала записать самую внутреннюю функцию с её параметрами, затем поставить перед ней знак $, а после этого пристраивать вызовы всех других функций, записывая их без последнего параметра и разделяя точками. Например, выражение
replicate 2 (product (map (*3) (zipWith max [1,2] [4,5])))
можно переписать так:
replicate 2 . product . map (*3) $ zipWith max [1,2] [4,5]
Как из одного выражения получилось другое? Ну, во-первых, мы посмотрели на самую правую функцию и её параметры как раз перед группой закрывающихся скобок. Это функция zipWith max [1,2] [4,5]. Так её и запишем:
zipWith max [1,2] [4,5]
Затем смотрим на функцию, которая применяется к zipWith max [1,2] [4,5], это map (*3). Поэтому мы ставим между ней и тем, что было раньше, знак $:
map (*3) $ zipWith max [1,2] [4,5]
Теперь начинаются композиции. Проверяем, какая функция применяется ко всему этому, и присоединяем её к map (*3):
product . map (*3) $ zipWith max [1,2] [4,5]
Наконец, дописываем функцию replicate 2 и получаем окончательное выражение:
replicate 2 . product . map (*3) $ zipWith max [1,2] [4,5]
Если выражение заканчивалось на три закрывающие скобки, велики шансы, что у вас получится два оператора композиции.
Бесточечная нотация
Композиция функций часто используется и для так называемого бесточечного стиля записи функций. Возьмём, для примера, функцию, которую мы написали ранее:
sum' :: (Num a) => [a] –> a
sum' xs = foldl (+) 0 xs
Образец xs представлен дважды с правой стороны. Из–за каррирования мы можем пропустить образец xs с обеих сторон, так как foldl (+) 0 создаёт функцию, которая принимает на вход список. Если мы запишем эту функцию как sum' = foldl (+) 0, такая запись будет называться бесточечной. А как записать следующее выражение в бесточечном стиле?
fn x = ceiling (negate (tan (cos (max 50 x))))
Мы не можем просто избавиться от образца x с обеих правых сторон выражения. Образец x в теле функции заключён в скобки. Выражение cos (max 50) не будет иметь никакого смысла. Вы не можете взять косинус от функции! Всё, что мы можем сделать, – это выразить функцию fn в виде композиции функций.
Откройте для себя мир чтения на siteknig.com - месте, где каждая книга оживает прямо в браузере. Здесь вас уже ждёт произведение Миран Липовача - Изучай Haskell во имя добра!, относящееся к жанру Программирование. Никаких регистраций, никаких преград - только вы и история, доступная в полном формате. Наш литературный портал создан для тех, кто любит комфорт: хотите читать с телефона - пожалуйста; предпочитаете ноутбук - идеально! Все книги открываются моментально и представлены полностью, без сокращений и скрытых страниц. Каталог жанров поможет вам быстро найти что-то по настроению: увлекательный роман, динамичное фэнтези, глубокую классику или лёгкое чтение перед сном. Мы ежедневно расширяем библиотеку, добавляя новые произведения, чтобы вам всегда было что открыть "на потом". Сегодня на siteknig.com доступно более 200000 книг - и каждая готова стать вашей новой любимой. Просто выбирайте, открывайте и наслаждайтесь чтением там, где вам удобно.

