Изучай Haskell во имя добра! - Миран Липовача
Шрифт:
Интервал:
Закладка:
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 – это набор модулей; главный модуль подгружает все остальные и использует функции, определённые в них, чтобы что-либо сделать. Разбиение кода на несколько модулей удобно по многим причинам. Если модуль достаточно общий, экспортируемые им функции могут быть использованы во множестве программ. Если ваш код разделён на несколько самостоятельных модулей, не очень зависящих один от другого (мы говорим, что они слабо связаны), модули могут многократно использоваться в разных проектах. Это отчасти облегчает непростую задачу написания кода, разбивая его на несколько частей, каждая из которых имеет некоторое назначение.
Стандартная библиотека языка Haskell разбита на модули, каждый из которых содержит взаимосвязанные функции и типы, служащие некоторой общей цели. Есть модуль для работы со списками, модуль для параллельного программирования, модуль для работы с комплексными числами и т. д. Все функции, типы и классы типов, с которыми мы имели дело до сих пор, были частью стандартного модуля Prelude – он импортируется по умолчанию. В этой главе мы познакомимся с несколькими полезными модулями и их функциями. Но для начала посмотрим, как импортировать модули.
Импорт модулей
Синтаксис для импорта модулей в программах на языке Haskell – import ModuleName. Импортировать модули надо прежде, чем вы приступите к определению функций, поэтому обычно импорт делается в начале файла. Конечно же, одна программа может импортировать несколько модулей. Для этого вынесите каждый оператор import в отдельную строку.
Давайте импортируем модуль Data.List, который содержит массу функций для работы со списками, и используем экспортируемую им функцию для того, чтобы написать свою – вычисляющую, сколько уникальных элементов содержит список.
import Data.List
numUniques :: (Eq a) => [a] –> Int
numUniques = length . nub
Когда выполняется инструкция import Data.List, все функции, экспортируемые модулем Data.List, становятся доступными в глобальном пространстве имён. Это означает, что вы можете вызывать их из любого места программы. Функция nub определена в модуле Data.List; она принимает список и возвращает список, из которого удалены дубликаты элементов исходного списка. Композиция функций length и nub создаёт функцию, которая эквивалентна xs –> length (nub xs).
ПРИМЕЧАНИЕ. Чтобы найти нужные функции и уточнить, где они определены, воспользуйтесь сервисом Hoogle, который доступен по адресу http://www.haskell.org/hoogle/. Это поистине удивительный поисковый механизм для языка Haskell, который позволяет вести поиск по имени функции, по имени модуля и даже по сигнатуре.
В интерпретаторе GHCi вы также можете подключить функции из модулей к глобальному пространству имён. Если вы работаете в GHCi и хотите вызывать функции, экспортируемые модулем Data.List, напишите следующее:
ghci> :m + Data.List
Если требуется подгрузить программные сущности из нескольких модулей, не надо вызывать команду :m + несколько раз, так как можно загрузить ряд модулей одновременно:
ghci> :m + Data.List Data.Map Data.Set
Кроме того, если вы загрузили скрипт, который импортирует модули, то не нужно использовать команду :m +, чтобы получить к ним доступ.
Если вам необходимо всего несколько функций из модуля, вы можете выборочно импортировать только эти функции. Если бы вам были нужны только функции nub и sort из модуля Data.List, импорт выглядел бы так:
import Data.List (nub, sort)
Также вы можете осуществить импорт всех функций из модуля за исключением некоторых. Это бывает полезно, когда несколько модулей экспортируют функции с одинаковыми именами, и вы хотите избавиться от ненужных повторов. Предположим, у вас уже есть функция с именем nub и вы хотите импортировать все функции из модуля Data.List, кроме nub, определённой в нём:
import Data.List hiding (nub)
Другой способ разрешения конфликтов имён – квалифицированный импорт. Модуль Data.Map, который содержит структуру данных для поиска значения по ключу, экспортирует несколько функций с теми же именами, что и модуль Prelude, например filter и null. Если мы импортируем модуль Data.Map и вызовем функцию filter, язык Haskell не будет знать, какую функцию использовать. Вот как можно обойти такую ситуацию:
import qualified Data.Map
Если после такого импорта нам понадобится функция filter из модуля Data.Map; мы должны вызывать её как Data.Map.filter – просто идентификатор filter ссылается на обычную функцию из модуля Prelude, которую мы все знаем и любим. Но печатать строку Data.Map перед именем каждой функции может и поднадоесть! Вот почему желательно переименовать модуль при импорте во что-нибудь более короткое:
import qualified Data.Map as M
Теперь, чтобы сослаться на функцию из Data.Map, мы вызываем её как M.filter.
Как вы видите, символ . используется для обращения к функциям, импортированным из модулей с указанием квалификатора, например: M.filter. Мы также помним, что он используется для обозначения композиции функций. Как Haskell узнаёт, что мы имеем в виду? Если мы помещаем символ . между квалифицированным именем модуля и функцией без пробелов – это обращение к функции из модуля; во всех остальных случаях – композиция функций.
ПРИМЕЧАНИЕ. Отличный способ узнать Haskell изнутри – просмотреть документацию к стандартной библиотеке и исследовать все стандартные модули и их функции. Также можно изучить исходные тексты всех модулей. Чтение исходных текстов некоторых модулей – отличный способ освоить язык и прочувствовать его особенности[9].
Решение задач средствами стандартных модулей
Модули стандартной библиотеки содержат массу функций, способных облегчить программирование на языке Haskell. Познакомимся с некоторыми из них, решая конкретные задачи.
Подсчёт слов
Предположим, что у нас имеется строка, содержащая много слов. Мы хотим выяснить, сколько раз в этой строке встречается каждое слово. Первой функцией, которую мы применим, будет функция words из модуля Data.List. Эта функция преобразует строку в список строк, в котором каждая строка представляет одно слово из исходной строки. Небольшой пример: