Добро пожаловать в завершение нашей серии статей о типах данных Haskell! В этой серии статей мы рассмотрели множество вещей, демонстрирующих простоту Haskell. Мы сравнили Haskell с другими языками, где мы увидели более громоздкий синтаксис. В этой заключительной части мы увидим кое-что посложнее. Мы быстро исследуем идею типовых семейств. Мы начнем с отслеживания эволюции некоторых идей связанных типов, а затем рассмотрим небольшой пример.
Семейства типов - довольно продвинутое понятие. Но если вы новичок, у нас есть много других ресурсов, которые могут вам помочь! Взгляните на наш Контрольный список для начала работы или нашу серию отрывков!
Различные типы типовых отверстий
До сих пор в этой серии статей мы видели пару различных способов заткнуть дыру в том, что касается определения типа или класса. В третьей части этой серии мы исследовали параметрические типы. В их определении есть переменные типа. Мы можем рассматривать каждую переменную типа как дыру, которую нам нужно заполнить другим типом.
Затем в четвертой части мы исследовали концепцию классов типов. Для любого экземпляра класса типов мы закрываем дыры в определениях функций этого класса. Мы заполняем каждую дырку реализацией функции для этого конкретного типа.
На этой неделе мы собираемся объединить эти идеи, чтобы получить типовые семейства! Семейство типов - это расширенный класс, в котором одна или несколько «дыр», которые мы заполняем, на самом деле являются типом! Это позволяет нам связывать разные типы друг с другом. В результате мы можем писать специальные виды полиморфных функций.
Базовый регистратор
Во-первых, вот надуманный пример для использования в этой статье. Мы хотим иметь класс типов журналирования. Назовем это MyLogger
. В этом классе у нас будет две основные функции. Мы должны иметь возможность получать все сообщения в журнале в хронологическом порядке. Тогда мы сможем регистрировать новое сообщение, отправляя какой-то эффект. Первый проход в этом классе может выглядеть так:
class MyLogger logger where
prevMessages :: logger -> [String]
logString :: String -> logger -> logger
Мы можем сделать небольшое изменение, которое будет использовать монаду State
вместо передачи логгера в качестве аргумента:
class MyLogger logger where
prevMessages :: logger -> [String]
logString :: String -> State logger ()
Но этот класс имеет существенный недостаток. У нас не будет никаких эффектов, связанных с нашим ведением журнала. Что, если мы хотим сохранить сообщение журнала в базе данных, отправить его через сетевое соединение или записать его в консоль? Мы могли бы допустить это, сохранив prevMessages
чистоту вот так:
class MyLogger logger where
prevMessages :: logger -> [String]
logString :: String -> StateT logger IO ()
Теперь наша logString
функция может использовать произвольные эффекты. Но у этого есть очевидный недостаток: он вынуждает нас вводить IO
монадные места там, где они нам не нужны. Если нашему регистратору не нужен IO
, он нам и не нужен. Так что же нам делать?
Основы семейства типов
Один из ответов - сделать наш класс семейством типов. Мы сделаем это с помощью ключевого слова type
в определении класса. Во-первых, нам нужно несколько языковых прагм, чтобы это сделать:
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
Теперь мы создадим type
в нашем классе, который относится к монадическому типу эффекта logString
функции. Мы должны описать «вид» типа с помощью определения. Поскольку это монада, ее вид * -> *
. Это означает, что требуется другой параметр типа. Вот как выглядит наше определение:
class MyLogger logger where
type LoggerMonad logger :: * -> *
prevMessages :: logger -> [String]
logString :: String -> (LoggerMonad logger) ()
Некоторые простые примеры
Теперь, когда у нас есть класс, давайте создадим экземпляр, НЕ использующий IO
. Мы будем использовать простой тип оболочки для нашего регистратора. Наша «монада» будет содержать регистратор в State
. Затем все, что мы делаем при записи строки, - это меняем ее состояние!
newtype ListWrapper = ListWrapper [String]
instance MyLogger ListWrapper where
type (LoggerMonad ListWrapper) = State ListWrapper
prevMessages (ListWrapper msgs) = reverse msgs
logString s = do
(ListWrapper msgs) <- get
put $ ListWrapper (s : msgs)
Теперь мы можем сделать версию этого, которая начинает включать IO
, но без каких-либо дополнительных эффектов «журналирования». Вместо того, чтобы использовать список для нашего состояния, мы будем использовать отображение временных меток на сообщения. Когда мы регистрируем строку, мы будем использовать IO
, чтобы получить текущее время и сохранить строку на карте с этим временем.
newtype StampedMessages = StampedMessages (Data.Map.Map UTCTime String)
instance MyLogger StampedMessages where
type (LoggerMonad StampedMessages) = StateT StampedMessages IO
prevMessages (StampedMessages msgs) = Data.Map.elems msgs
logString s = do
(StampedMessages msgs) <- get
currentTime <- lift getCurrentTime
put $ StampedMessages (Data.Map.insert currentTime s msgs)
Больше ввода-вывода
Теперь пара примеров, которые используют IO
в традиционном способе ведения журнала, а также сохраняют сообщения. Наш первый пример - это ConsoleLogger
. Он сохранит сообщение в своем State
, но также запишет сообщение в консоль.
newtype ConsoleLogger = ConsoleLogger [String]
instance MyLogger ConsoleLogger where
type (LoggerMonad ConsoleLogger) = StateT ConsoleLogger IO
prevMessages (ConsoleLogger msgs) = reverse msgs
logString s = do
(ConsoleLogger msgs) <- get
lift $ putStrLn s
put $ ConsoleLogger (s : msgs)
Другой вариант - записать наши сообщения в файл! Мы сохраним имя файла как часть нашего состояния, хотя при желании могли бы использовать Handle
.
newtype FileLogger = FileLogger (String, [String])
instance MyLogger FileLogger where
type (LoggerMonad FileLogger) = StateT FileLogger IO
prevMessages (FileLogger (_, msgs)) = reverse msgs
logString s = do
(FileLogger (filename, msgs)) <- get
handle <- lift $ openFile filename AppendMode
lift $ hPutStrLn handle s
lift $ hClose handle
put $ FileLogger (filename, s : msgs)
И мы можем представить, что у нас была бы аналогичная ситуация, если бы мы хотели отправлять журналы по сети. Мы бы использовали наш State
для хранения информации о конечном сервере. Или мы могли бы добавить что-то вроде монады Слуги ClientM
в наш стек в определении type
.
Использование нашего регистратора
Определив наш класс таким образом, мы теперь можем написать полиморфную функцию, которая будет работать с любым из наших регистраторов!
runComputations :: (Logger logger, Monad (LoggerMonad logger)) => InputType -> (LoggerMonad logger) ResultType
runComputations input = do
logString "Starting Computation!"
let x = firstFunction input
logString "Finished First Computation!"
let y = secondFunction x
logString "Finished Second Computation!"
return y
Это здорово, потому что наш код теперь абстрагируется от необходимых эффектов. Мы могли бы вызвать это с монадой IO
или без нее.
Сравнение с другими языками
Честно говоря, это одна из областей системы типов Haskell, которая делает ее немного сложнее в использовании, чем другие языки. Произвольные эффекты могут происходить где угодно в Java или Python. Благодаря этому нам не нужно беспокоиться о сопоставлении эффектов с типами.
Но не будем забывать о преимуществах! Для всех частей нашего кода мы знаем, какие эффекты мы можем использовать. Это позволяет нам определять во время компиляции, где могут возникнуть определенные проблемы.
И типовые семьи дают нам лучшее из обоих миров! Они позволяют нам писать полиморфный код, который может работать как с IO
эффектами, так и без них!
Вывод
Это все, что касается нашей серии статей о системе данных Haskell! Мы увидели широкий спектр элементов, от простых до сложных. Мы сравнили Haskell с другими языками. Опять же, простота, с которой можно объявлять данные в Haskell и использовать их полиморфно, была для меня ключевым аргументом в пользу продаж!
Надеюсь, эта серия статей вдохновила вас начать работу с Haskell, если вы еще этого не сделали! Загрузите наш Контрольный список для начала работы или прочтите нашу Серии отрывков, чтобы приступить к работе!