Skip to content

Instantly share code, notes, and snippets.

@blazern blazern/Config1.hs forked from denisshevchenko/Config1.hs
Last active Mar 21, 2018

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
-- 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

This comment has been minimized.

Copy link

denisshevchenko commented Mar 20, 2018

-- 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

This comment has been minimized.

Copy link
Owner 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

This comment has been minimized.

Copy link
Owner 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
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.