В целом тем кто читал TAPL чтение данного поста не рекомендуется, так же тем кто хочет знать как делать правильно лучше сразу брать TAPL и читать его. Так же тут не описывается полноценное ООП, для этого можно посмотреть в сторону Object Haskell, где показывается вариант реализации через гетерогенные списки.

В одном из тредов на point.im возник вопрос, каким образом можно сделать “объект” (состояние и функции связанные с ним) с помощью функций.

Здесь и далее будет использоваться haskell и немного комментариев о других языках, так же будет использоваться подход идеоматичный для haskell, другие варианты тоже будут мельком обсуждены.

Итак нам как обычно потребуется немного расширений языка

> {-# LANGUAGE GADTs #-}
> {-# LANGUAGE RankNTypes #-}
> {-# LANGUAGE TypeFamilies #-}
> import System.IO

Общая идея заключается в том, чтобы создать автомат (mealy machine), который на вход получает запросы, а на выходе дает результат (+ новую версию себя). Рассмотрим сначала простейший вариант:

> newtype M1 a = M1 { runM1 :: forall b . a b -> b }

Тут мы создаем тип обёртку, над замыканием, которое принимает на вход запрос типа a b и отдает результат типа b. Таким образом объекты типизируются видом запросов, которые они могут обрабатывать. Можно записать простой пример запросов для системы логирования:

> data LogLevel = Debug | Info | Warn | Error deriving (Eq, Show, Ord)
> data LogRequest a where
>   WriteLog :: LogLevel -> String -> LogRequest (IO ())
>   SetLevel :: LogLevel -> LogRequest LogObject
>   GetLevel :: LogRequest LogLevel
> type LogObject = M1 LogRequest

Здесь мы имеем три типа запроса, первый - записал лог, создает действие типа IO (), т.е. выполняет какой-то эффект, второе - обновляет уровень логирования возвращая новый обьект из текущего, и третье это чистое действие, получение уровня логирования из текущего.

Теперь мы можем создавать различне логгеры, например:

Для того, чтобы не делать копипаст создадим базовый логгер:

> dummyLogger :: LogLevel -> LogObject
> dummyLogger lvl = M1 go where
>   go :: LogRequest a -> a
>   go WriteLog{} = error "dummy logger: write log is not implemented"
>   go (SetLevel l) = dummyLogger l
>   go GetLevel     = lvl

Пример

> stdoutLogger0 :: LogLevel -> LogObject
> stdoutLogger0 lvl = M1 go where
>   go :: LogRequest a -> a
>   go (WriteLog l s)
>      | l >= lvl = hPutStrLn stderr s
>      | otherwise = return ()
>   go (SetLevel l) = stdoutLogger0 l
>   go GetLevel = runM1 (dummyLogger lvl) GetLevel

Абстрактные методы:

> baseLogger :: (LogLevel -> String -> IO ())
>             -> LogLevel
>             -> LogObject
> baseLogger writeLog lvl = M1 go where
>   go :: LogRequest a -> a -- очень важная строчка делающая компилятор счастливым
>   go (WriteLog l s) 
>      | l >= lvl = writeLog l s
>      | otherwise = return ()
>   go (SetLevel l) = baseLogger writeLog l
>   go GetLevel     = lvl

Наследование 1:

> stdoutLogger :: LogLevel -> LogObject
> stdoutLogger lvl = M1 $ go base where
>   base = baseLogger (\_ s -> hPutStrLn stderr s) lvl
>   go :: LogObject -> LogRequest a -> a
>   go b s@(WriteLog Error _) = hPutStrLn stdout "AAAAAAAAAAAAAAA!" >> runM1 b s
>   go b s@(SetLevel l) = stdoutLogger l -- не удобно :)
>   go b s = runM1 base s

Наследование 2:

> type FileLogObject = M1 FileLogRequest
> type family Outer a where
>   Outer LogObject = FileLogObject
>   Outer a         = a
> 
> data FileLogRequest a where
>   SetFile :: FilePath -> FileLogRequest FileLogObject
>   GetFile :: FileLogRequest FilePath
>   AsLogObject :: LogRequest a -> FileLogRequest (Outer a)
> fileLogger :: FilePath -> LogLevel -> FileLogObject
> fileLogger fp lvl = M1 $ go (base lvl) where
>   base = baseLogger (\_ s -> appendFile fp s)
>   go :: LogObject -> FileLogRequest a -> a
>   go _ (SetFile fp') = fileLogger fp' lvl
>   go _ GetFile       = fp
>   go b (AsLogObject s) = case s of
>        (SetLevel l) -> M1 $ go (runM1 b s)
>        p -> runM1 b p
> {-
> baseLogger :: (LogLevel -> String -> IO ())
>            -> (LogLevel -> LogObject)
>            -> LogLevel
>            -> LogObject
> baseLogger writeLog newLogger lvl = M1 go where

И два конкретных

> mkStdErrLogger :: LogLevel -> LogObject
> mkStdErrLogger = baseLogger (\_ s -> hPutStrLn stderr s) mkStdErrLogger
> mkFileLogger :: FilePath -> LogLevel -> LogObject
> mkFileLogger path = baseLogger (\_ s -> appendFile path s) (mkFileLogger path)

Пример использования

*Main> let l1 = mkStdErrLogger Warn
*Main> runM1 l1 (WriteLog Info "info")
*Main> runM1 l1 (WriteLog Error "error")
error
*Main> runM1 l1 GetLevel
Warn
*Main> let l2 = runM1 l1 (SetLevel Info)
*Main> runM1 l2 (WriteLog Info "info")
info

Что тут неудобно:

  1. не всегда возвращение нового объекта может быть удобно, есть несколько выходов или завернуть в State, или использовать изменяемые поля. Но в последнем случае доступы к полям тоже будут в IO монаде, заметьте, что сейчас функции, считывающие состояние без его изменения чистые.

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

Но для многих задач и этого достаточно.

Что можно улучшать, можно, например, разделить все запросы на запросы записи, чтения и выполнения, например как data Action = Read | Write | Execute и пример можно найти где-то здесь.

Абсолютно те же трюки можно провести и в других языках, только вместо pattern-matching будет диспетчеризация по типу, например куча if и проверкой typeof и меньше статических гарантий.

> -}



comments powered by Disqus