Ни одно достаточно крупное приложение не обходится без конфигурационных файлов. Сегодня мы познакомимся с пакетом configurator , который позволяет не только банально парсить конфиги в Haskell, но и делать другие интересные вещи. Например, узнавать об изменении конфигов и автоматически перечитывать их. Автором пакета является широко известный в узких кругах Bryan O’Sullivan .

Создадим у себя в домашнем каталоге простой конфиг с именем test.conf:

database {
host = «127.0.0.1»
name = «mydatabase»
user = «admin»
pass = «qwerty»
}

Как видите, синтаксис конфига прост и понятен. Также в конфиге можно импортировать другие конфиги:

import «$(HOME)/someapp.conf»

Основные функции для работы с конфигурационными файлами следующие:

empty :: Config

Создаем новый пустой конфиг.

load :: [ Worth FilePath ] -> IO Config

Читаем конфигурационные файлы с диска. Тип Worth определяется примерно как data Worth a = Required a | Optional a .

subconfig :: Name -> Config -> Config

Получение части конфига.

reload :: Config -> IO ( )

Перечитываем конфиги. При этом конфиги, полученные с помощью функции subconfig, также обновляются.

lookup :: Configured a => Config -> Name -> IO ( Maybe a )

Чтение параметра из конфига.

lookupDefault :: Configured a => a -> Config -> Name -> IO a

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

require :: Configured a => Config -> Name -> IO a

Аналогично lookup, только в случае отсутствия параметра вместо того, чтобы вернуть Nothing, будет брошено исключение .

display :: Config -> IO ( )

Вспомогательная функция для вывода конфига.

getMap :: Config -> IO ( HashMap Name Value )

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

Теперь дружно скажем cabal install configurator и запустим ghci:

ghci > import qualified Data . Configurator as C
ghci > import qualified Data . Text as T

Загрузим наш конфиг:

ghci > conf <- C . load [ C . Required «$(HOME)/test.conf» ]

Выведем его на экран:

ghci > C . display conf
( «» , fromList [ ( «database.user» , String «admin» ) , ( «database.name» , String «mydatabase» ) , ( «database.pass» , String «qwerty» ) , ( «database.host» , String «127.0.0.1» ) ] )

Получим имя пользователя:

ghci > let key = T . pack «database.user»
ghci > user <- C . lookup conf key :: IO ( Maybe String )
ghci > user
Just «admin»

Попробуем поработать с сабконфигом:

ghci > let subconf = C . subconfig ( T . pack «database» ) conf
ghci > pass <- C . require subconf ( T . pack «pass» ) :: IO String
ghci > pass
«qwerty»

… и использовать значения по умолчанию:

ghci > port <- C . lookupDefault 5432 subconf ( T . pack «port» ) :: IO Int
ghci > port
5432

Совсем не сложно, правда?

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

data AutoConfig = AutoConfig {
interval :: Int
, onError :: SomeException -> IO ( )
}

Этот тип определяет, как часто следует перечитывать конфиг и что делать в случае возникновения ошибки.

autoConfig :: AutoConfig

Возвращает значение по умолчанию типа AutoConfig. Интервал равен одной секунде, ошибки игнорируются.

autoReload :: AutoConfig -> [ Worth FilePath ] -> IO ( Config , ThreadId )

Создает автоматически обновляемый конфиг.

Посмотрим на все это хозяйство в действии. Говорим:

ghci > let paths = [ C . Required «/home/eax/test.conf» ]
ghci > ( conf2 , _ ) <- C . autoReload C . autoConfig paths
ghci > C . getMap conf2
fromList [ ( «database.user» , String «admin» ) ,

Открываем конфиг, заменяем имя пользователя на «guest», снова говорим:

ghci > C . getMap conf2
fromList [ ( «database.user» , String «guest» ) ,

Кажется, работает! Ну, почти. На момент написания этих строк в configurator было несколько неисправленных багов. Например, если использовать в пути к конфигу переменную $(HOME) , он не будет обновляться. Ознакомиться со списком известных багов можно здесь .

Также configurator позволяет подписаться на обновления определенных частей конфига.

prefix :: Text -> Pattern

Возвращает шаблон для подписки на определенную часть конфига.

exact :: Text -> Pattern

Создает шаблон для подписки на один конкретный параметр.

type ChangeHandler = Name -> Maybe Value -> IO ( )

Тип функции, вызываемой при внесении изменений в конфиге.

subscribe :: Config -> Pattern -> ChangeHandler -> IO ( )

Подписаться на изменения.

Проверяем:

ghci > : {
Prelude C T | let changeHandler name val = do
Prelude C T | putStrLn $ show name ++ » changed to » ++ show val
Prelude C T | : }
ghci > let prfx = ( C . prefix $ T . pack «database» )
ghci > let exct = ( C . exact $ T . pack «database.port» )
ghci > C . subscribe conf2 prfx changeHandler
ghci > C . subscribe conf2 exct changeHandler

Открываем конфиг и дописываем в него строчку:

port = 5432

В ghci видим:

ghci > «database.port» changed to Just ( Number ( 5432 % 1 ) )
«database.port» changed to Just ( Number ( 5432 % 1 ) )

Вот такой занятный пакет этот configurator. А чем вы парсите конфиги в Haskell?

Дополнение: Haskell и хождение в базы данных с помощью HDBC

EnglishRussianUkrainian