Instantly share code, notes, and snippets.

Embed
What would you like to do?
{-
Вот это наш .yaml-конфиг:
---
API_URL: https://api.nightscout/v2
API_Secret: asdLKJHh0987ljkhLKJlkjhLKJ
-}
-- Это для того, чтобы сырые литералы "" могли автоматически превращаться, например, в Text.
-- Без них литералы станут типом String, а этот тип неэффективный и считается уже антипаттерном.
{-# LANGUAGE OverloadedStrings #-}
-- Из некоторых импортируемых модулей мы можем взять только то, что хотим...
import Data.Maybe ( maybe )
-- ... в то время как из других берём неявно всё.
import Data.Yaml
import Data.Text ( Text, unpack )
import Network.URI
-- Просто псевдоним для существующего типа URI.
-- Мы хотим, чтобы наш APIUrl был не просто тупым текстом,
-- а именно валидным URI, с возможностью манипулировать его частями.
type APIUrl = URI
-- Чтобы было понятно, что это не просто текст, а именно API Secret.
newtype APISecret = APISecret Text
deriving Show
-- Тип, отражающий всю нашу конфигурацию.
data ReceiverConfig = ReceiverConfig APIUrl APISecret
deriving Show
-- Экземпляр класса типов FromJSON (не удивляйтесь имени, этот код может парсить и JSON).
-- Экземпляр нужен для того, чтобы объяснить, как парсить конфиг, переложив сырой текст из
-- файла на наш тип ReceiverConfig.
instance FromJSON ReceiverConfig where
-- Значение values - это словарь, в котором уже лежат все извлечённые из .yaml-файла
-- значения. С помощью оператора (.:) мы извлекаем нужные нам значения, подразумевая,
-- что они точно должны быть в файле.
parseJSON (Object values) = do
url <- values .: "API_URL" -- Явно указываем имя ключа в файле.
secret <- values .: "API_Secret" -- И здесь тоже.
-- Формируем значение типа ReceiverConfig из уже полученных url и secret.
-- Слово return возвращает это значение в "парсинговый контекст", мы как будто
-- объясняем парсеру, мол, если достанешь нужные нам значения, просто сформируй
-- из них ReceiverConfig и держи у себя, пока не попросим.
return $ ReceiverConfig url secret
-- Существует ещё аппликативный стиль парсинга, но это в других примерах...
-- Когда парсер дойдёт до значений URL (см. строку 36)
instance FromJSON URI where
parseJSON (String rawText) =
-- Функция maybe - пример ФВП: она работает с другими функциями как с аргументами.
-- parseURI возвращает опциональное значение типа Maybe URI, то есть либо URI, либо ничего
-- (в том случае, если в конфиге вместо URL будет какой-то хлам).
-- И вот если она вернёт URI, то просто сохраняем его в парсере, пока не попросим.
-- Если же там хлам, тогда сообщаем о проблеме с помощью fail, мол, ну не смогла я!
maybe (fail "Cannot parse URL, please fix it.") -- Не удалось распарсить!
return -- Всё ок, URI уже здесь.
(parseURI . unpack $ rawText) -- С помощью unpack мы превращаем Text в String.
instance FromJSON APISecret where
parseJSON (String rawText) =
-- Мы знаем, что это будет просто текст, но мы создаём на его основе
-- значение типа APISecret, чтобы эта часть конфигурации была
-- самодокументируемой.
return $ APISecret rawText
-- Пытаемся декодировать .yaml-файл. Функция decodeFileEither
-- гарантирует нам, что исключений она не выкинет, а о возможных
-- ошибках конфига она сообщит через Left-значение (см. ниже).
getReceiverConfig :: IO (Either ParseException ReceiverConfig)
getReceiverConfig = decodeFileEither "/tmp/ns.yaml"
main :: IO ()
main = do
result <- getReceiverConfig
-- Получили результат, но пока не знаем, какой он, нужно проверить...
case result of
-- Left-значение говорит о том, что произошла беда, и внутри лежит её описание.
-- Мы окажемся здесь и в том случае, если YAML-структура битая, и в том случае,
-- если со значениями что-то не так (см. fail при парсинге URL).
Left problem -> print problem
-- Right-значение сообщает, что всё ок, и значение типа ReceiverConfig уже здесь.
Right config -> putStrLn "Great, ReceiverConfig is here!"
@ChShersh

This comment has been minimized.

ChShersh commented Mar 19, 2018

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

-- Это для того, чтобы сырые литералы "" могли автоматически превращаться, например, в Text.

Любому программисту из популярного языка программирования это покажется каким-то бредом, ибо в нормальных языках строковые литералы и так автоматически имееют некоторый строковый тип. Я бы добавил, что по умолчанию этот тип будет String, но он не очень эффективный. Если хотим Text, то надо расширение.

type APIUrl = URI

Почему это не newtype, если APISecretnewtype? Как минимум не очень понятно, когда newtype, а когда type использовать. Я понимаю, что это не туториал по Haskell, но может быть что-то можно с этим сделать? Ибо чем меньше синтаксических конструкций, тем меньше плотность текста и тем проще понять.

parseJSON (Object values) = do

Я бы добавил расширение -XInstanceSigs и написал явно типы методов. Мне нравится -XInstanceSigs.

`Экземпляр класса типов FromJSON (не удивляйтесь имени, этот код может парсить и JSON).

Может быть имелось в виду этот код может парсить и YAML?

maybe (fail "Cannot parse URL, please fix it.")

Во всех функциях parseJSON используется non-exhaustive pattern-matching, из-за чего будут проблемы в реальной жизни... Лучше использовать стандартные функции библиотеки withObject, withText, которые обрабатывают случаи с другими паттернами.

Ещё: может быть надо добавить пример .yaml-файла, который будет распарсен этой программой? Чтобы отображение из типов данных в конкретный файл было бы более ясным.

@denisshevchenko

This comment has been minimized.

Owner

denisshevchenko commented Mar 19, 2018

Любому программисту из популярного языка программирования это покажется каким-то бредом, ибо в нормальных языках строковые литералы и так автоматически имееют некоторый строковый тип. Я бы добавил, что по умолчанию этот тип будет String, но он не очень эффективный. Если хотим Text, то надо расширение.

Да, соглашусь.

type APIUrl = URI

Почему это не newtype, если APISecret — newtype? Как минимум не очень понятно, когда newtype, а когда type использовать. Я понимаю, что это не туториал по Haskell, но может быть что-то можно с этим сделать? Ибо чем меньше синтаксических конструкций, тем меньше плотность текста и тем проще понять.

Да, ты прав. Просто я привёл type как пример псевдонима для существующего типа, в то время как newtype отвечает за создание нового типа (да, без накладных расходов, но для тайпчекера сущность-то отдельная).

Может быть имелось в виду этот код может парсить и YAML?

Нет, именно так. Пакет-то называется YAML, то есть для парсинга YAML. Но, из-за родственности с Aeson, он может парсить и JSON в том числе. Я проверял. :-)

maybe (fail "Cannot parse URL, please fix it.")

Во всех функциях parseJSON используется non-exhaustive pattern-matching, из-за чего будут проблемы в реальной жизни...

Да, ты прав, я видел это на --pedantic-компиляции. :-D

Ещё: может быть надо добавить пример .yaml-файла, который будет распарсен этой программой? Чтобы отображение из типов данных в конкретный файл было бы более ясным.

Так есть же, в самом вверху, в многострочном комментарии. ;-)

@ChShersh

This comment has been minimized.

ChShersh commented Mar 19, 2018

Так есть же, в самом вверху, в многострочном комментарии. ;-)

Прошу прощения, не заметил!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment