Как из одного выражения получилось другое? Ну, во-первых, мы посмотрели на самую правую функцию и её параметры как раз перед группой закрывающихся скобок. Это функция 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
в виде композиции функций.
fn = ceiling . negate . tan . cos . max 50
Отлично! Во многих случаях бесточечная запись легче читается и более лаконична; она заставляет думать о функциях, о том, как их соединение порождает результат, а не о данных и способе их передачи. Можно взять простые функции и использовать композицию как «клей» для создания более сложных. Однако во многих случаях написание функций в бесточечном стиле может делать код менее «читабельным», особенно если функция слишком сложна. Вот почему я не рекомендую создавать длинные цепочки функций, хотя меня частенько обвиняли в пристрастии к композиции. Предпочитаемый стиль – использование выражения let
для присвоения меток промежуточным результатам или разбиение проблемы на подпроблемы и их совмещение таким образом, чтобы функции имели смысл для того, кто будет их читать, а не представляли собой огромную цепочку композиций.
Ранее в этой главе мы решали задачу, в которой требовалось найти сумму всех нечётных квадратов меньших 10 000. Вот как будет выглядеть решение, если мы поместим его в функцию:
oddSquareSum :: Integer
oddSquareSum = sum (takeWhile (<10000) (filter odd (map ( 2) [1..])))
Со знанием композиции функций этот код можно переписать так:
oddSquareSum :: Integer
oddSquareSum = sum . takeWhile (<10000) . filter odd $ map ( 2) [1..]
Всё это на первый взгляд может показаться странным, но вы быстро привыкнете. В подобных записях меньше визуального «шума», поскольку мы убрали все скобки. При чтении такого кода можно сразу сказать, что filter odd
применяется к результату map ( 2) [1..]
, что затем применяется takeWhile (<10000)
, а функция sum
суммирует всё, что получилось в результате.
6
Модули
В языке Haskell модуль – это набор взаимосвязанных функций, типов и классов типов. Программа на Haskell – это набор модулей; главный модуль подгружает все остальные и использует функции, определённые в них, чтобы что-либо сделать. Разбиение кода на несколько модулей удобно по многим причинам. Если модуль достаточно общий, экспортируемые им функции могут быть использованы во множестве программ. Если ваш код разделён на несколько самостоятельных модулей, не очень зависящих один от другого (мы говорим, что они слабо связаны), модули могут многократно использоваться в разных проектах. Это отчасти облегчает непростую задачу написания кода, разбивая его на несколько частей, каждая из которых имеет некоторое назначение.