Изучай Haskell ради Добра! Модули

Miran Lipovača, “Learn You a Haskell for Great Good! Modules”, public translation into Russian from English More about this translation.

Translate into another language.

Модули

Загрузка модулей

В Хаскеле модуль - это набор взаимосвязанных функций, типов и классов типов. Программа на Хаскеле это набор модулей, главный модуль подгружает остальные модули и использует функции определенные в них для того чтобы что-либо сделать. Разбиение кода на несколько модулей имеет множество преимуществ. Если модуль достаточно общий, экспортируемые им функции могут быть использованы во множестве программ. Если ваш код разделен на несколько самостоятельных модулей, не очень зависящих один от другого (мы говорим что они слабо связаны), модули могут многократно использоваться в разных проектах. Это делает непростую задачу написания кода легче, разбивая ее на несколько частей, каждая из которых имеет некоторое назначение.

Стандартная библиотека Хаскеля разбита на модули, каждый из которых содержит взаимосвязанные функции и типы, служащие некоторой общей цели. Есть модуль для работы со списками, модуль для параллельного программирования, модуль для комплексных чисел, и так далее. Все функции, типы и классы типов, с которыми мы имели дело до сих пор, были частью модуля Prelude, он импортируется по умолчанию. В этой главе мы познакомимся с несколькими полезными модулями и их функциями. Но для начала, посмотрим как импортировать модули.

Синтаксис для импорта модулей в скриптах на Хаскеле - import <module name>. Это надо сделать до того как начинать делать определения функций, поэтому импорт обычно делается в начале файла. Конечно же, один скрипт может импортировать несколько функций. Для этого поместите каждый оператор 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).

В 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, Хаскель не будет знать какую функцию использовать. Вот как мы можем обойти такую ситуацию:

import qualified Data.Map

После такого импорта, если нам нужна функция filter из Data.Map, мы должны вызывать ее как Data.Map.filter, просто filter ссылается на обычную функцию из Prelude, которую мы все знаем и любим. Но печатать Data.Map перед именем каждой функции может и поднадоесть. Вот почему мы можем переименовать модуль при импорте во что-либо более короткое:

import qualified Data.Map as M

Чтобы сослаться на функцию из Data.Map, мы вызываем ее как M.filter.

Используйте этот удобный справочник (http://www.haskell.org/ghc/docs/latest/html/libraries/) чтобы посмотреть, какие модули входят в стандартную библиотеку. Отличный способ узнать Хаскель изнутри - просмотреть все стандартные модули и их функции. Также вы можете просмотреть исходные тексты всех модулей. Чтение исходных текстов некоторых модулей - отличный способ для того чтобы изучить язык, и хорошенько его прочувствовать.

Для поиска функций, или чтобы найти в каком конкретно месте они объявлены, используйте Hoogle (http://haskell.org/hoogle). Это волшебная поисковая машина по Хаскелю, вы можете искать по имени, модулю или даже сигнатуре функции.

Модуль Data.List

Модуль Data.List содержит все для работы со списками. Он предоставляет несколько очень полезных функций для работы с ними. Мы уже встречались с некоторыми из них (например map и filter), потому что модуль Prelude экспортирует некоторые функции из Data.List для удобства. Вы не должны импортировать Data.List с помощью уточненного (qualified) импорта, потому что он не конфликтует ни с одним именем в Prelude за исключением тех имен, которые Prelude сам импортирует из Data.List. Давайте посмотрим на некоторые еще неизвестные нам функции.

intersperse принимает элемент и список и помещает элемент между элементами списка. Вот демонстрация:

ghci> intersperse '.' "MONKEY"

"M.O.N.K.E.Y"

ghci> intersperse 0 [1,2,3,4,5,6]

[1,0,2,0,3,0,4,0,5,0,6]

intercalate принимает список и список списков. Затем он помещает первый параметр между списками второго параметра и возвращает результат в виде одного списка.

ghci> intercalate " " ["hey","there","guys"]

"hey there guys"

ghci> intercalate [0,0,0] [[1,2,3],[4,5,6],[7,8,9]]

[1,2,3,0,0,0,4,5,6,0,0,0,7,8,9]

transpose транспонирует список списков. Если посмотреть на список списков как на двумерную матрицу, столбцы становятся строками и наоборот.

ghci> transpose [[1,2,3],[4,5,6],[7,8,9]]

[[1,4,7],[2,5,8],[3,6,9]]

ghci> transpose ["hey","there","guys"]

["htg","ehu","yey","rs","e"]

Предположим, у нас есть полиномы 3x2 + 5x + 9, 10x3 + 9 и 8x3 + 5x2 + x - 1, и мы хотим сложить их. Мы можем использовать списки [0,3,5,9], [10,0,0,9] и [8,5,1,-1] для того чтобы представить их полиномиальные коэффициенты в Хаскеле. Чтобы сложить их, мы могли бы поступить так:

ghci> map sum $ transpose [[0,3,5,9],[10,0,0,9],[8,5,1,-1]]

[18,8,6,17]

После транспонирования списка, третьи степени находятся в первой строке, вторые степени во второй строке и т.д. Применяя sum к ним, получаем желаемый результат.

foldl' и foldl1' - более строгие версии своих ленивых инкарнаций. При использовании ленивых сверток на очень больших списках, вы можете получать ошибку переполнения стека. Причина кроется в том, что благодаря ленивой природе сверток, значение аккумулятора на самом деле не обновляется во время выполнения свертки. На самом деле аккумулятор типа как бы дает обещание вычислить свое значение когда его попросят (это называется "переходник", thunk). Переходники создаются для каждого промежуточного значения аккумулятора и забивают стек. Строгие свертки не любители ленивости, они на самом деле вычисляют промежуточные значения вместо создания thunk в стеке. Так что если вы когда-нибудь получите переполнение стека при выполнении ленивых сверток, попытайтесь использовать строгие версии.

concat преобразует список списков в один список.

ghci> concat ["foo","bar","car"]

"foobarcar"

ghci> concat [[3,4,5],[2,3,4],[2,1,1]]

[3,4,5,2,3,4,2,1,1]

concat убирает один уровень вложенности. Если вам нужно "выпрямить" [[[2,3],[3,4,5],[2]],[[2,3],[3,4]]], что является списком списков списков, вам придется конкатенировать его дважды.

Выполнить concatMap - это тоже самое что применить функцию к каждому элементу списка и конкатенировать результат.

ghci> concatMap (replicate 4) [1..3]

[1,1,1,1,2,2,2,2,3,3,3,3]

and принимает список булевских значений и возвращает True только если все значения истинны.

ghci> and $ map (>4) [5,6,7,8]

True

ghci> and $ map (==4) [4,4,4,3,4]

False

or принимает тот же параметр, но возвращает True если хотя бы один элемент списка равен True.

ghci> or $ map (==4) [2,3,4,5,6,1]

True

ghci> or $ map (>4) [1,2,3]

False

any и all принимают предикат и проверяют, удовлетворяется ли он хоть для одного (any) или для всех (all) элементов списка. Обычно мы используем эти функции вместо map в которой вызываются and или or.

ghci> any (==4) [2,3,5,6,1,4]

True

ghci> all (>4) [6,9,10]

True

ghci> all (`elem` ['A'..'Z']) "HEYGUYSwhatsup"

False

ghci> any (`elem` ['A'..'Z']) "HEYGUYSwhatsup"

True

iterate принимает функцию и стартовое значение. Функция применяется к стартовому значению, получается результат, функция применяется к результату, получается новый результат, к нему снова применяется функция, и так далее. iterate возвращает все результаты в виде бесконечного списка.

ghci> take 10 $ iterate (*2) 1

[1,2,4,8,16,32,64,128,256,512]

ghci> take 3 $ iterate (++ "haha") "haha"

["haha","hahahaha","hahahahahaha"]

splitAt принимает число и список. Список разделяется на два, в первом списке столько элементов как указано в первом параметре. Списки возвращаются в виде пары.

ghci> splitAt 3 "heyman"

("hey","man")

ghci> splitAt 100 "heyman"

("heyman","")

ghci> splitAt (-3) "heyman"

("","heyman")

ghci> let (a,b) = splitAt 3 "foobar" in b ++ a

"barfoo"

takeWhile очень полезная функция. Она берет элементы из списка пока выполняется предикат. Как только предикат не перестает выполняться, возвращается результат. Как оказалось, это очень удобно.

ghci> takeWhile (>3) [6,5,4,3,2,1,2,3,4,5,4,3,2,1]

[6,5,4]

ghci> takeWhile (/=' ') "This is a sentence"

"This"

Скажем, нам нужно узнать сумму всех третьих степеней меньших 10000. Мы не можем применить map (^3) к [1..], фильтровать, и затем пытаться суммировать, потому что filter на бесконечных списках никогда не завершится. Вы можете знать что все элементы в списке возрастают, но Хаскель этого предполагать не может. Вот почему мы делаем так:

ghci> sum $ takeWhile (<10000) $ map (^3) [1..]

53361

Мы применяем (^3) к бесконечному списку, и как только появится элемент больший 100000, список отсекается. Теперь его можно легко просуммировать.

dropWhile похожа, только она пропускает элементы пока предикат истинен. Как только предикат станет равным False, функция вернет остаток списка. Очень полезная и красивая функция.

ghci> dropWhile (/=' ') "This is a sentence"

Pages: ← previous Ctrl next
1 2 3 4 5 6 7

© Miran Lipovača

Original (English): Learn You a Haskell for Great Good! Modules

Translation: © asinitsyn, JEuler, slayzx, savask, malphunction, neor .

License: creative commons attribution noncommercial blah blah blah ... license

translated.by crowd

Like this translation? Share it or bookmark!