Ранее я уже упоминал о библиотеках, пришло время познакомиться с ними поближе, ведь в последующих главах мы будем использовать их постоянно.
За годы существования Haskell разработчики со всего мира создали множество библиотек. Библиотеки избавляют нас от необходимости вновь и вновь писать то, что уже написано до нас. Для любого живого языка программирования написано множество библиотек. В мире Haskell их, конечно, не такая туча, как для той же Java, но порядочно: стабильных есть не менее двух тысяч, многие из которых очень качественные и уже многократно испытаны в серьёзных проектах.
С модулями — файлами, содержащими Haskell-код, — мы уже знакомы, они являются основным кирпичом любого Haskell-проекта. Библиотека, также являясь Haskell-проектом, тоже состоит из модулей (не важно, из одного или из сотен). Поэтому использование библиотеки сводится к использованию входящих в неё модулей. И мы уже неоднократно делали это в предыдущих главах.
Вспомним пример из главы про ФВП:
import Data.Char
toUpperCase :: String -> String
toUpperCase str = map toUpper str
main :: IO ()
main = putStrLn . toUpperCase $ "haskell.org"
Функция toUpper
определена в модуле Data.Char
, который, в свою очередь, живёт в стандартной библиотеке. Библиотек есть множество, но стандартная лишь одна. Она содержит самые базовые, наиболее широко используемые инструменты. А прежде чем продолжить, зададимся важным вопросом: «Где живут все эти библиотеки?» Они живут в разных местах, но главное из них — Hackage.
Hackage — это центральный репозиторий Haskell-библиотек, или, как принято у нас называть, пакетов (англ. package). Название репозитория происходит от слияния слов Haskell
и package
. Hackage существует с 2008 года и живёт здесь. Ранее упомянутая стандартная библиотека тоже живёт в Hackage и называется она base
. Каждой библиотеке выделена своя страница.
Каждый из Hackage-пакетов живёт по адресу, сформированному по неизменной схеме: http://hackage.haskell.org/package/ИМЯПАКЕТА
. Так, дом стандартной библиотеки — http://hackage.haskell.org/package/base
. Hackage — открытый репозиторий: любой разработчик может добавить туда свои пакеты.
Стандартная библиотека включает в себя более сотни модулей, но есть среди них самый известный, носящий имя Prelude
. Этот модуль по умолчанию всегда с нами: всё его содержимое автоматически импортируется во все модули нашего проекта. Например, уже известные нам map
или операторы конкатенации списков живут в модуле Prelude
, поэтому доступны нам всегда. Помимо них (и многих-многих десятков других функций) в Prelude
располагаются функции для работы с вводом-выводом, такие как наши знакомые putStrLn
и print
.
Hackage весьма большой, поэтому искать пакеты можно двумя способами. Первый — на единой странице всех пакетов. Здесь перечислены все пакеты, а для нашего удобства они расположены по тематическим категориям.
Второй способ — через специальный поисковик, коих существует два:
Эти поисковики скрупулёзно просматривают внутренности Hackage, и вы будете часто ими пользоваться. Лично я предпочитаю Hayoo!. Пользуемся оным как обычным поисковиком: например, знаем мы имя функции, а в каком пакете/модуле она живёт — забыли. Вбиваем в поиск — получаем результаты.
Чтобы воспользоваться пакетом в нашем проекте, нужно для начала включить его в наш проект. Для примера рассмотрим пакет text
, предназначенный для работы с текстом. Он нам в любом случае понадобится, поэтому включим его в наш проект незамедлительно.
Открываем сборочный файл проекта real.cabal
, находим секцию executable real-exe
и в поле build-depends
через запятую дописываем имя пакета:
build-depends: base -- Уже здесь!
, real
, text -- А это новый пакет.
Файл с расширением .cabal
— это обязательный сборочный файл Haskell-проекта. Он содержит главные инструкции, касающиеся сборки проекта. С синтаксисом сборочного файла мы будем постепенно знакомиться в следующих главах.
Как видите, пакет base
уже тут. Включив пакет text
в секцию build-depends
, мы объявили тем самым, что наш проект отныне зависит от этого пакета. Теперь, находясь в корне проекта, выполняем уже знакомую нам команду:
$ stack build
Помните, когда мы впервые настраивали проект, я упомянул, что утилита stack
умеет ещё и библиотеки устанавливать? Она увидит новую зависимость нашего проекта и установит как сам пакет text
, так и все те пакеты, от которых, в свою очередь, зависит пакет text
. После сборки мы можем импортировать модули из этого пакета в наши модули. И теперь пришла пора узнать, как это можно делать.
Когда мы пишем:
import Data.Char
в имени модуля отражена иерархия пакета. Data.Char
означает, что внутри пакета base
есть каталог Data
, внутри которого живёт файл Char.hs
, открыв который, мы увидим:
module Data.Char
...
Таким образом, точка в имени модуля отражает файловую иерархию внутри данного пакета. Можете воспринимать эту точку как слэш в Unix-пути. Есть пакеты со значительно более длинными именами, например:
module GHC.IO.Encoding.UTF8
Соответственно, имена наших собственных модулей тоже отражают место, в котором они живут. Так, один из модулей в моём рабочем проекте носит название Common.Performers.Click
. Это означает, что живёт этот модуль здесь: src/Common/Performers/Click.hs
.
Вернёмся к нашему примеру:
import Data.Char
Импорт модуля Data.Char
делает доступным для нас всё то, что включено в интерфейс этого модуля. Откроем наш собственный модуль Lib
:
module Lib
( someFunc
) where
someFunc :: IO ()
someFunc = putStrLn "someFunc"
Имя функции someFunc
упомянуто в интерфейсе модуля, а именно между круглыми скобками, следующими за именем модуля. Чуток переформатируем скобки:
module Lib (
someFunc
) where
В настоящий момент только функция someFunc
доступна всем импортёрам данного модуля. Если же мы определим в этом модуле другую функцию anotherFunc
:
module Lib (
someFunc
) where
someFunc :: IO ()
someFunc = putStrLn "someFunc"
anotherFunc :: String -> String
anotherFunc s = s ++ "!"
она останется невидимой для внешнего мира, потому что её имя не упомянуто в интерфейсе модуля. И если в модуле Main
мы напишем так:
module Main
import Lib
main :: IO ()
main = putStrLn . anotherFunc $ "Hi"
компилятор справедливо ругнётся, мол, не знаю функцию anotherFunc
. Если же мы добавим её в интерфейс модуля Lib
:
module Lib (
someFunc,
anotherFunc
) where
тогда функция anotherFunc
тоже станет видимой всему миру. Интерфейс позволяет нам показывать окружающим лишь то, что мы хотим им показать, оставляя служебные внутренности нашего модуля тайной за семью печатями.
В реальных проектах мы импортируем множество модулей из различных пакетов. Иногда это является причиной конфликтов, с которыми приходится иметь дело.
Вспомним функцию putStrLn
: она существует не только в незримом модуле Prelude
, но и в модуле Data.Text.IO
из пакета text
:
-- Здесь тоже есть функция по имени putStrLn.
import Data.Text.IO
main :: IO ()
main = putStrLn ... -- И откуда эта функция?
При попытке скомпилировать такой код мы упрёмся в ошибку:
Ambiguous occurrence ‘putStrLn’
It could refer to either ‘Prelude.putStrLn’,
imported from ‘Prelude’ ...
or ‘Data.Text.IO.putStrLn’,
imported from ‘Data.Text.IO’ ...
Нам необходимо как-то указать, какую из функций putStrLn
мы имеем в виду. Это можно сделать несколькими способами.
Можно указать принадлежность функции конкретному модулю. Из сообщения об ошибке уже видно, как это можно сделать:
-- Здесь тоже есть функция по имени putStrLn.
import Data.Text.IO
main :: IO ()
main = Data.Text.IO.putStrLn ... -- Сомнений нет!
Теперь уже сомнений не осталось: используемая нами putStrLn
принадлежит модулю Data.Text.IO
, поэтому коллизий нет.
Впрочем, не кажется ли вам подобная форма слишком длинной? В упомянутом ранее стандартном модуле GHC.IO.Encoding.UTF8
есть функция mkUTF8
, и представьте себе:
import GHC.IO.Encoding.UTF8
main :: IO ()
main =
let enc = GHC.IO.Encoding.UTF8.mkUTF8 ...
Слишком длинно, нужно укоротить. Импортируем модуля под коротким именем:
import Data.Text.IO as TIO
включить этот модуль как это
main :: IO ()
main = TIO.putStrLn ...
Вот, так значительно лучше. Короткое имя может состоять даже из одной буквы, но как и полное имя модуля, оно обязательно должно начинаться с большой буквы, поэтому:
import Data.Text.IO as tIO -- Ошибка
import Data.Text.IO as i -- Тоже ошибка
import Data.Text.IO as I -- Порядок!
Иногда, для большего порядка, используют qualified-импорт:
import qualified Data.Text.IO as TIO
Ключевое слово qualified
используется для «строгого» включения модуля: в этом случае мы обязаны указывать принадлежность к нему. Например:
import qualified Data.Text as T
main :: IO ()
main = T.justifyLeft ...
Даже несмотря на то, что функция justifyLeft
есть только в модуле Data.Text
и никаких коллизий с Prelude
нет, мы обязаны указать, что эта функция именно из Data.Text
. В больших модулях qualified-импорт бывает полезен: с одной стороны, гарантированно не будет никаких конфликтов, с другой, мы сразу видим, откуда родом та или иная функция.
Впрочем, некоторым Haskell-программистам любое указание принадлежности к модулю кажется избыточным. Поэтому они идут по другому пути: выборочное включение/выключение. Например:
import Data.Char
import Data.Text (pack) -- Только её!
main :: IO ()
main = putStrLn $ map toUpper "haskell.org"
Мы подразумеваем стандартную функцию map
, однако в модуле Data.Text
тоже содержится функция по имени map
. К счастью, никакой коллизии не будет, ведь мы импортировали не всё содержимое модуля Data.Text
, а лишь одну его функцию pack
:
import Data.Text (pack)
импортируем отсюда только
это
Если же мы хотим импортировать две или более функции, перечисляем их через запятую:
import Data.Text (pack, unpack)
Существует и прямо противоположный путь: вместо выборочного включения — выборочное выключение. Избежать коллизии между функциями putStrLn
можно было бы и так:
import Data.Text.IO hiding (putStrLn)
main :: IO ()
main = putStrLn ... -- Сомнений нет: из Prelude.
Слово hiding
позволяет скрывать кое-что из импортируемого модуля:
import Data.Text.IO hiding (putStrLn)
импортируем всё отсюда кроме этого
Можно и несколько функций скрыть:
import Data.Text.IO hiding ( readFile
, writeFile
, appendFile
)
При желании можно скрыть и из Prelude
:
import Prelude hiding (putStrLn)
import Data.Text.IO
main :: IO ()
main = putStrLn ... -- Она точно из Data.Text.IO.
Общая рекомендация такова — оформляйте так, чтобы было легче читать. В реальном проекте в каждый из ваших модулей будет импортироваться довольно много всего. Вот кусочек из одного моего рабочего модуля:
import qualified Test.WebDriver.Commands as WDC
import Test.WebDriver.Exceptions
import qualified Data.Text as T
import Data.Maybe (fromJust)
import Control.Monad.IO.Class
import Control.Monad.Catch
import Control.Monad (void)
Как полные, так и краткие имена модулей выровнены, такой код проще читать и изменять. Не все программисты согласятся с таким стилем, но попробуем убрать выравнивание:
import qualified Test.WebDriver.Commands as WDC
import Test.WebDriver.Exceptions
import qualified Data.Text as T
import Data.Maybe (fromJust)
import Control.Monad.IO.Class
import Control.Monad.Catch
import Control.Monad (void)
Теперь код выглядит скомканным, его труднее воспринимать. Впрочем, выбор за вами.