Skip to content

Instantly share code, notes, and snippets.

@denisshevchenko
Last active March 20, 2018 06:46
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save denisshevchenko/e9412ff0b84fb3cb0946e099a95df6fa to your computer and use it in GitHub Desktop.
Save denisshevchenko/e9412ff0b84fb3cb0946e099a95df6fa to your computer and use it in GitHub Desktop.
{-
Вот это наш .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
Copy link

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

-- Это для того, чтобы сырые литералы "" могли автоматически превращаться, например, в 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
Copy link
Author

Любому программисту из популярного языка программирования это покажется каким-то бредом, ибо в нормальных языках строковые литералы и так автоматически имееют некоторый строковый тип. Я бы добавил, что по умолчанию этот тип будет 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
Copy link

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

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

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