Skip to content

Instantly share code, notes, and snippets.

@blazern
Forked from denisshevchenko/Config1.hs
Last active March 21, 2018 07:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save blazern/4ca22e170f1311e8e8fd28396d609d0a to your computer and use it in GitHub Desktop.
Save blazern/4ca22e170f1311e8e8fd28396d609d0a 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
-- 1!!! получается, что ReceiverConfig - tuple, состоящий из APIUrl и APISecret?
-- 2!!! Как имея объект типа ReceiverConfig можно достучаться до его состовляющих? Что-нибудь вроде myConfig.0, myConfig.1?
-- Экземпляр класса типов FromJSON (не удивляйтесь имени, этот код может парсить и JSON).
-- Экземпляр нужен для того, чтобы объяснить, как парсить конфиг, переложив сырой текст из
-- файла на наш тип ReceiverConfig.
--- 3!!! Почему это именно экземпляр, если по объявлению выглядит похожим на наследование из ООП-языков?
--- 4!!! Т.е. тут объявляется экземпляр типа, но кроме объявления мы определяем тело его метода -
--- 5!!! очень похоже на наследование\реализацию интерфейса.
instance FromJSON ReceiverConfig where
-- Значение values - это словарь, в котором уже лежат все извлечённые из .yaml-файла
-- значения. С помощью оператора (.:) мы извлекаем нужные нам значения, подразумевая,
-- что они точно должны быть в файле.
parseJSON (Object values) = do -- 6!!! Тело этой функции выполняется последовательно засчёт `do`?
url <- values .: "API_URL" -- Явно указываем имя ключа в файле.
secret <- values .: "API_Secret" -- И здесь тоже.
-- Формируем значение типа ReceiverConfig из уже полученных url и secret.
-- Слово return возвращает это значение в "парсинговый контекст", мы как будто
-- объясняем парсеру, мол, если достанешь нужные нам значения, просто сформируй
-- из них ReceiverConfig и держи у себя, пока не попросим.
return $ ReceiverConfig url secret
-- Существует ещё аппликативный стиль парсинга, но это в других примерах...
-- Когда парсер дойдёт до значений URL (см. строку 36)
-- 7!!! В строке 36 оригинального файла находится объявление инстанса: `instance FromJSON ReceiverConfig where`
-- 8!!! Что тогда значит "Когда парсер дойдёт"? Имеется в виду строка 41 (ориг. файла)? Выглядит так, что в 41 строке
-- 9!!! происходит создание объекта JSON-типа по ключу "API_URL", а в 47-строке неявное кастование этого JSON к URL.
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.
-- 10!!!Тут String rawText с помощью unpack трансформируется в Text, затем этот Text передаётся в parseURI?
instance FromJSON APISecret where
parseJSON (String rawText) =
-- Мы знаем, что это будет просто текст, но мы создаём на его основе
-- значение типа APISecret, чтобы эта часть конфигурации была
-- самодокументируемой.
return $ APISecret rawText
-- Пытаемся декодировать .yaml-файл. Функция decodeFileEither
-- гарантирует нам, что исключений она не выкинет, а о возможных
-- ошибках конфига она сообщит через Left-значение (см. ниже).
getReceiverConfig :: IO (Either ParseException ReceiverConfig) -- 11!!! Что такое IO?
getReceiverConfig = decodeFileEither "/tmp/ns.yaml" -- 12!!! Строкой выше мы объявляем сигнатуру функции, а в этой тело функции?
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!"
@denisshevchenko
Copy link

-- 1!!! получается, что ReceiverConfig - tuple, состоящий из APIUrl и APISecret?

Нет, это не tuple. Если бы tuple, это писалось бы так:

type ReceiverConfig = (APIUrl, APISecret)

А в данном случае ReceiverConfig - это новый тип, с двумя полями.

-- 2!!! Как имея объект типа ReceiverConfig можно достучаться до его состовляющих? Что-нибудь вроде myConfig.0, myConfig.1?

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

extractAPISecret :: ReceiverConfig -> ...
extractAPISecret (ReceiverConfig url secret) =
    -- Что-то делаем с secret...

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

data ReceiverConfig = ReceiverConfig
    { apiURL :: APIUrl
    , apiSecret :: APISecret
    }

В этом случае имена полей могут быть использованы как getter-ы, например:

extractAPISecret :: ReceiverConfig -> ...
extractAPISecret myConfig =
    let myAPISecret = apiSecret myConfig
    -- Что-то делаем с myAPISecret...

--- 3!!! Почему это именно экземпляр, если по объявлению выглядит похожим на наследование из ООП-языков?

Экземпляр - это связь между классами типов и типами. Класс определяет, скажем так, характеристики, справедливые для некоторого множества типов. Соответственно, если некий тип хочет "войти в контекст класса С", он обязан предоставить свой экземпляр класса С.
Подробнее объяснять здесь не стоит, слишком длинно получится, почитать можно здесь: http://learnyouahaskell.com/types-and-typeclasses.

-- 6!!! Тело этой функции выполняется последовательно засчёт do?

do - это так называемая do-нотация. По сути она есть синтаксический сахар, позволяющий проще работать в монадическом контексте. Дело в том, что метод parseJSON работает в контексте Parser, который, в свою очередь, является монадическим типом. А do-нотация позволяет нам временно вытаскивать значения из данного монадического контекста, с использованием <-, см. строки 46 и 47.

-- 7!!! В строке 36 оригинального файла находится объявление инстанса: instance FromJSON ReceiverConfig where
-- 8!!! Что тогда значит "Когда парсир дойдёт"? Имеется в виду строка 41 (ориг. файла)?

Да, ваша правда. Номера строк поползли, когда я сверху комментарии дописал. :-)

Выглядит так, что в 41 строке происходит создание объекта JSON-типа по ключу "API_URL", а в 47-строке неявное кастование этого JSON к URL.

Нет, неявного кастования типов в Haskell, к великому счастью, не существует.

В строке 41 мы говорим парсеру, мол, вытащи значение, соответствующее ключу "API_URL", которое, по сути, просто текст, а затем - внимание! - преврати этот текст в значение типа URI. Но парсер сам по себе понятия не имеет, как превратить текст в значение типа URI, поэтому мы должны научить его делать это. А учим мы его на строке 51, где предоставляем экземпляр класса FromJSON для типа URI. И конкретно метод parseJSON на строке 52 говорит парсеру, мол, когда ты там, на строке 41, увидишь извлечение url (которое, будучи вторым полем в ReceiverConfig, имеет тип APIUrl, то есть URI, см. строку 23), то посмотри сюда, на строку 60, и попытайся превратить тот сырой текст в значение типа APIUrl.

-- 10!!!Тут String rawText с помощью unpack трансформируется в Text, затем этот Text передаётся в parseURI?

Нет, наоборот. Функция unpack берёт Text и превращает его в String, который уже и передаётся в parseURI. К сожалению, по историческим причинам, ещё не все Haskell-библиотеки перешли на хороший Text и пока что продолжают использовать плохой String.

-- 11!!! Что такое IO?

IO - это контекст вычислений, взаимодействующих с внешним миром (IO - от Input/Output). Если функция хочет получить стандартный ввод, или записать файл, или отправить запрос по сети - она обязана находится в IO-контексте. Соответственно, если функция находится вне IO-контекста, взаимодействовать с внешним миром она не способна. Таким образом, глядя на объявление функции, мы, видя (или не видя) там IO, сразу понимаем, может или не может эта функция выглядывать во внешнюю вселенную.

-- 12!!! Строкой выше мы объявляем сигнатуру функции, а в этой тело функции?

Да, именно так. Сигнатура функции, строго говоря, необязательна, потому что в Haskell типы выводятся автоматически. Но по соображениям самодокументируемости кода настоятельно рекомендуется объявлять все функции (за исключением простых локальных).

@blazern
Copy link
Author

blazern commented Mar 21, 2018

В строке 41 мы говорим парсеру, мол, вытащи значение, соответствующее ключу "API_URL", которое, по сути, просто текст, а затем - внимание! - преврати этот текст в значение типа URI. Но парсер сам по себе понятия не имеет, как превратить текст в значение типа URI, поэтому мы должны научить его делать это. А учим мы его на строке 51, где предоставляем экземпляр класса FromJSON для типа URI. И конкретно метод parseJSON на строке 52 говорит парсеру, мол, когда ты там, на строке 41, увидишь извлечение url (которое, будучи вторым полем в ReceiverConfig, имеет тип APIUrl, то есть URI, см. строку 23), то посмотри сюда, на строку 60, и попытайся превратить тот сырой текст в значение типа APIUrl.

Возможно плохо разбираюсь в терминологии, но всё равно звучит как неявный каст. Ну или проявление полиморфизмъа.
В месте, где получается из конфига URL (41 строка ориг. файла), нет явного указния компилятору как конвертировать строку в APIUrl. Компилятор сам находит нужный тип (класс?) чуть ниже.
Т.е. получается что-то вроде неявного кастования - компилятор знает, что строка не подойдёт, ищет операцию каста (которая класс\тип), и использует её, чтобы преобразовать одно в другое.

@blazern
Copy link
Author

blazern commented Mar 21, 2018

IO - это контекст вычислений, взаимодействующих с внешним миром (IO - от Input/Output). Если функция хочет получить стандартный ввод, или записать файл, или отправить запрос по сети - она обязана находится в IO-контексте. Соответственно, если функция находится вне IO-контекста, взаимодействовать с внешним миром она не способна. Таким образом, глядя на объявление функции, мы, видя (или не видя) там IO, сразу понимаем, может или не может эта функция выглядывать во внешнюю вселенную.

В первый раз в жизни вижу такую конструкцию в ЯП. :)

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