Hackage и библиотеки

Ранее я уже упоминал о библиотеках, пришло время познакомиться с ними поближе, ведь в последующих главах мы будем использовать их постоянно.

Библиотеки большие и маленькие

За годы существования 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

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 весьма большой, поэтому искать пакеты можно двумя способами. Первый — на единой странице всех пакетов. Здесь перечислены все пакеты, а для нашего удобства они расположены по тематическим категориям.

Второй способ — через специальный поисковик, коих существует два:

  1. Hoogle
  2. Hayoo!

Эти поисковики скрупулёзно просматривают внутренности 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)

Теперь код выглядит скомканным, его труднее воспринимать. Впрочем, выбор за вами.