Ранее я уже упоминал о библиотеках, пришло время познакомиться с ними поближе, ведь в последующих главах мы будем использовать их постоянно.
За годы существования 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)Теперь код выглядит скомканным, его труднее воспринимать. Впрочем, выбор за вами.