diff --git a/drafts/2018-01-18-effects-haskell.en.md b/drafts/2018-01-18-effects-haskell.en.md new file mode 100644 index 00000000..d51d0675 --- /dev/null +++ b/drafts/2018-01-18-effects-haskell.en.md @@ -0,0 +1,598 @@ +--- +author: Юрий Сыровецкий +title: Effects in Haskell +tags: effects, purity +description: Effects implementation in Haskell. +--- + +In this article we will demonstrate that procedures (functions with effects) +do not require special support from the programming language and that +regular "pure" functions can be used to handle these procedures with effects. + +As an example we will use Haskell, a pure functions language, which does not have the embedded effects in a way "impure" C, OCaml, JavaScript do. +However, we can build pure, controllable effects in Haskell, which +serve our goals quite similarly as "impure" effects. + + + + + + + +_Замечание о терминах._ +Я позволю себе называть параметрические типы с опущенными параметрами просто +_типами_, +хотя строгий хаскелит употребил бы термин _конструктор типов_. +Конструктор типов сам типом не является, но принадлежит языку типов. +В разговорной речи и не очень строгих статьях, как эта, +такая вольность вполне допустима, чтобы речь звучала не так тяжело +(«ехал конструктор через конструктор...»), +благо, ни к каким противоречиям при программировании это не приводит. +Если параметр не указан, то это конструктор типа, +а если по контексту подразумевается всё-таки конкретный тип, +значит, речь идёт о конструкторе с произвольными значениями параметров. + +## 0. No effects + +We can represent a pure funciton `f :: a -> b` as a black box that takes in a value of type `a` and returns a value of type `b`: + +
![](../../../../../files/posts/2018-01-18/pure.svg)
+ +## 1. Partiality effect + +
![](../../../../../files/posts/2018-01-18/partial.svg)
+ +A partial fucntion either returns a result or it doesn't. +A sum data type well captures this behaviour. + +```haskell +data Maybe a = Nothing | Just a +``` + +Value of type `Maybe a` either contains a value +of type `a`, or there is no value. + +We can describe a partial procedure that optionally +returns type `b` as a function that always returns `Maybe b` type. + +
![](../../../../../files/posts/2018-01-18/partial-pure.svg)
+ +```haskell +p :: a -> Maybe b +``` + +Here is an example. + +```haskell +headM :: [a] -> Maybe a +headM [] = Nothing -- cannot take a head of empty list +headM (x:_) = Just x -- head of non-mepty list - here it is! +``` + +Please note that `Maybe` type belongs to `Functor`, `Applicative`, +`Monad` and other interesting and useful typeclasses. + +In practice we can also use `Either` and `Except` types. They +implement similar effect as `Maybe`, but +also add additional information why a computation was not completed. + +In example below we introduce an error data type +`MyError` with a sole value `EmptyList`. +`MyError` will be used in other code examples below. + +```haskell +data Either a b + = Left a -- considered an error + | Right b -- considered success + +data MyError = EmptyList -- denotes an custom error name + +headE :: [a] -> Either MyError a +headE [] = Left EmptyList +headE (x:_) = Right x +``` + + + +Partiality can be combined with other effects. Below we use +`MonadError` from [mtl](https://hackage.haskell.org/package/mtl-2.2.2/docs/Control-Monad-Except.html) package for exception processing: + +```haskell +headE + :: MonadError MyError m -- 'm' type constructor supports "error" effects + => [a] -> m a +headE (x:_) = pure x +headE [] = + throwError EmptyList -- we add an error effect, it terminates further computations +``` + +Partiallity is the only effect that can arise from inside Haskell. +The rest of the effects require some act of the outside world, while +partiality situation can be constructed by means of Haskell itself +as we show in examples below. + + + + + + +Going into an infinte loop is one of partiality effect: + + +```haskell +-- a simplistic infinite loop +x = x -- computation of 'x' requires 'x' again and again + +-- a slightly longer example: +-- takeWhile (< 10) will require a value 10 or above +-- to stop, there will be no such value in +-- [2, 1 ..], so length of infinite list +-- will never be computed +n = length $ takeWhile (< 10) [2, 1 ..] +``` + + + + +In Turing-complete languages the possibility of an infinite loop +cannot be avoided. There are non-Turing-complete programming +languages, where the programs will avoid partiality effect +and the program 'totality' is be garanteed, but the drawback +is an increase in langauge complexity. [This link](https://stackoverflow.com/questions/315340/practical-non-turing-complete-languages) provides a discussion of several non-Turing-complete languages +with respect to program termination guarantee. + +Luckily in Haskell we can use controlled partiallity and +cases of uncontrolled partiallity are rather rare. + +## 2. Indeterminism (uncertainty) effect + +### 2.1. Instability effect + +
![](../../../../../files/posts/2018-01-18/unstable.svg)
+ +Если процедура для одного и того же значения аргумента может вернуть от раза +к разу разные результаты, +это значит, что на самом деле результат зависит от чего-то ещё. + +Даже генератор случайных чисел (настоящий, аппаратный) — это «чистая» функция, зависящая от состояния источника энтропии. + +Чтобы представить этот эффект чистой функцией, +надо всего лишь неявную зависимость сделать явной. + +
![](../../../../../files/posts/2018-01-18/unstable-pure.svg)
+ +```haskell +p :: a -> r -> b +``` + +Пример. + + + + +```haskell +getDataDir :: Config -> FilePath +getDataDir Config{dataDir} = dataDir +``` + +Для удобства рассуждений об эффектах удобно ввести синоним + +EP (дополнение, для редактирования): ...синоним для типа `r -> b`. Мы назовем этот новый тип `Reader`. Этот тип задается с помощью двух параметров типов `r` и `b`. Типу `Reader`, +согласно нашему опрделению, будут соответстовать функции, аргументом +которых является значение типа `r`, а результатом функции является значение типа `b`. + + +```haskell +type Reader r b = r -> b +p :: a -> Reader r b -- EP: это эквивалент a -> r -> b ? +``` + +```haskell +-- процедура, читающая неявное значение и возвращающая его +-- EP: почему значения являются неявным? +ask :: Reader r r +ask = id + +getDataDir :: Reader Config FilePath +getDataDir = do + Config{dataDir} <- ask -- читаем конфиг, переданный неявно + pure dataDir +``` + +Можно комбинировать неявную зависимость с другими эффектами: + +```haskell +data MyError = DataDirNotSpecified + +getDataDir + :: ( MonadError MyError m + , MonadReader Config m + -- 'm' поддерживает эффект неявной зависимости от 'Config' + ) + => m FilePath +getDataDir = do + Config{mDataDir} <- ask -- читаем конфиг, переданный неявно + case mDataDir of + Nothing -> throwError DataDirNotSpecified + Just dataDir -> pure dataDir +``` + +Обратите внимание, что тип `Reader r` +(конструктор типа `Reader`, частично применённый к одному аргументу) +принадлежит `Functor`, `Applicative`, +`Monad` и многим другим интересным и полезным классам. + +### 2.2. Эффект множественности + +
![](../../../../../files/posts/2018-01-18/many.svg)
+ +Здесь всё просто и очевидно. +Функция, дающая много ответов сразу — это функция, +имеющая единственный ответ-множество. + +В Хаскеле есть тип `Set` для множеств, +но для моделирования эффекта множественности оказывается более удобным список — +`[]`. + +
![](../../../../../files/posts/2018-01-18/many-pure.svg)
+ +```haskell +p :: a -> [b] +``` + +Пример. + +```haskell +rollADie :: Int -> [Int] +rollADie n = [1..n] + +rollTwoDiceAndSum :: Int -> [Int] +rollTwoDiceAndSum n = do + a <- rollADie n + b <- rollADie n + pure $ a + b +``` + +Обратите внимание, что тип `[]` (конструктор типа списка) +принадлежит `Functor`, `Applicative`, +`Monad` и многим другим интересным и полезным классам. + +Посколько множество результатов может быть и пустое, +то множественность можно рассматривать как частный случай случай частичности, — +функция, возвращающая множество ответов, +может для некоторых значений аргумента вернуть пустое множество, +то есть не вернуть ни одного ответа. + +Таким образом, тип `[]` реализует и эффект частичности. + +## 3. Побочный эффект + +
![](../../../../../files/posts/2018-01-18/side.svg)
+ +Побочный эффект — это просто неявный результат. Сделаем же неявное явным! + +
![](../../../../../files/posts/2018-01-18/side-pure.svg)
+ +```haskell +p :: a -> (b, s) +``` + +Для удобства рассуждений об эффектах удобно ввести обёртку + +EP: ... которую мы назовем `Putter`. Конструктор `Putter (b, s)` +будет содержать кортеж из значений типа `b` и типа `s`, а обозначаться +это тип будет `Putter s b`. + +```haskell +newtype Putter s b = Putter (b, s) +p :: a -> Putter s b +``` + +Обратите внимание, что тип `Putter s` +(конструктор типа `Putter`, частично применённый к одному аргументу) +принадлежит `Functor`, `Applicative`, +`Monad` и многим другим интересным и полезным классам. + + + + +На практике чаще применяется тип `Writer`, структурно идентичный, +но с более полезными свойствами: все побочные эффекты собираются в моноид. + + + + + +... , который позволяет "склеивать" побочные эффекты в длинную колбасу, + например, в список сообщений логгирования +или сообщений об ошибке. + +Пример. + +```haskell +-- процедура, откладывающая побочное значение +tell :: w -> Writer w () +tell w = Writer ((), w) + +-- процедура с побочным эффектом журналирования +sumWithLog :: Int -> Int -> Writer String Int +sumWithLog x y = do + tell $ "sum: x = " ++ show x ++ "\n" -- запишем в лог аргументы процедуры + tell $ "sum: y = " ++ show y ++ "\n" + let result = x + y + tell $ "sum: result = " ++ show result ++ "\n" -- и результат запишем + pure r -- *** должно быть result ? *** + +-- функция склейки процедур, спрятанная в do-синтаксисе +-- тут-то и происходит сборка моноида +(>>) :: Monoid w => Writer w a -> Writer w b -> Writer w b +Writer (_, w1) >> Writer (b, w2) = Writer (b, w1 <> w2) +``` + +Можно комбинировать! + +```haskell +data MyError = DataDirNotSpecified +type AccessCounter = Sum Int + +-- процедура с побочным эффектом подсчёта количества вызовов +getDataDir + :: ( MonadError MyError m + , MonadReader Config m + , MonadWriter AccessCounter m + -- 'm' поддерживает побочный эффект счётчика + ) + => m FilePath +getDataDir = do + tell 1 -- добавить 1 ко счётчику обращений + Config{mDataDir} <- ask + case mDataDir of + Nothing -> throwError DataDirNotSpecified + Just dataDir -> pure dataDir +``` + +## 2 + 3. Эффект состояния + +
![](../../../../../files/posts/2018-01-18/state.svg)
+ +Если соединить результат побочного эффекта и источник нестабильности, +из их комбинации (композиции) получается эффект состояния — процедура, +которая может и зависеть от текущего состояния «переменной», +и задавать ей новое состояние. + +Проведя рассуждения, аналогичные случаям `Reader` и `Putter`, получим + +
![](../../../../../files/posts/2018-01-18/state-pure.svg)
+ +```haskell +p :: a -> s -> (b, s) +``` + +Для удобства рассуждений об эффектах удобно ввести обёртку + +```haskell +newtype State s b = State (s -> (b, s)) +p :: a -> State s b +``` + +Пример. + +```haskell +-- получить значение внутренней переменной +get :: State a a +get = State $ \s -> -- старое значение + ( s -- результат + , s -- новое значение переменной совпадает со старым + ) + +-- присвоить значение внутренней переменной +put :: a -> State a () +put s = State $ \_ -> -- старое значение игнорируем + ( () -- результат + , s -- новое значение переменной + ) + +-- изменить значение внутренней переменной +modify :: (a -> a) -> State a () +modify f = State $ \s -> -- старое значение + ( () -- результат + , f s -- новое значение переменной + ) + +-- процедура-генератор псевдослучайных чисел по простейшей формуле +prng + :: State + Int -- тип внутренней переменной + Int -- тип результата +prng = do + modify $ \s -> s * 23 + 97 + get +``` + +Комбинируем с предыдущими эффектами. + +```haskell +type Storage = Map FilePath Value -- имитация файловой системы для демонстрации + +-- процедура с побочным эффектом подсчёта количества вызовов +putData + :: ( MonadError MyError m + , MonadReader Config m + , MonadWriter AccessCounter m + , MonadState Storage m -- 'm' поддерживает эффект изменения 'Storage' + ) + => m FilePath +putData key value = do + dataDir <- getDataDir + modify $ Map.insert (dataDir key) value -- вносим изменения в Storage +``` + +Не будет сюрпризом, что тип `State s` +(конструктор типа `State`, частично применённый к одному аргументу) +принадлежит `Functor`, `Applicative`, +`Monad` и многим другим интересным и полезным классам. + +## 0. Отсутствие эффектов (продолжение) + +Рассмотрим тип + +```haskell +newtype Identity a = Identity a +``` + +Тип `Identity a` полностью аналогичен типу `a`. +То есть это своего рода функция `id`, только на уровне типов. + +Тип `Identity` не может выражать никаких эффектов. +С другой стороны, можно сказать, что он выражает отсутствие эффектов. + +Конечно же, конструктор типа `Identity` принадлежит `Functor`, `Applicative`, +`Monad` и многим другим интересным и полезным классам. + +## Эффекты! Эффекты повсюду! + +В Хаскеле есть специальный тип `IO`, реализующий сразу все возможные эффекты. +В нём можно прерывать программу, обмениваться данными с ресурсами, +не указанными явно в аргументах и возвращаемом значении. +В `IO` нет ограничений, доступен на чтение и запись весь мир, +в том числе ядерные ракеты. +Как если бы у нас был в программе объект типа `RealWorld` и мы могли бы изменять +его, как переменную под `State`. + +Реализация `IO` не определена в спецификации языка. +Если вы заглянете в исходники стандартной библиотеки, скорее всего, +действительно увидите что-то подобное `State RealWorld`, +но эта реализация нужна только для внутренних нужд компилятора и пропадает при +компиляции, +так что верить ей не стоит. + +Вы уже догадались, что конструктор типа `IO` принадлежит `Functor`, +`Applicative`, `Monad` и многим другим интересным и полезным классам. + +Выше я утверждал, что в Хаскеле никаких побочных эффектов нет, +а сейчас рассказываю, что в `IO` всё можно — только руку протяни. +Этот парадокс разрешается просто: запускает ракеты не сам язык, а тип `IO`, +который в некотором смысле отделён от языка и подчиняется ему. +Хаскель управляет `IO`, а не наоборот. +Нет возможности запустить `IO` в произвольном месте программы +(ну ладно, есть пара грязных хаков, но честных способов нет), +только в специально отведённых. + +Иными словами, `IO` даёт доступ к неуправляемым эффектам, +но доступ к самому `IO` управляемый. + +## Процедуры и чистые функции + +Вернёмся к нашей дихотомии. + +С одной стороны, функция — это просто процедура без эффектов. + +С другой стороны, нам удалось все процедуры выразить в типах вида + +```haskell +p :: a -> f b +``` + +где _b_ — тип результата, он может быть любым, _f_ — тип, реализующий эффект. + +Иными словами, процедура — это чистая функция, вычисляющая эффект. + +Обратите внимание, что все упомянутые типы эффектов принадлежат `Functor`, +`Applicative`, `Monad` и многим другим интересным и полезным классам +(разве что `Writer` с некоторыми ограничениями). +Эти классы предоставляют общий механизм для работы с эффектами. +Подробнее можно почитать в +[www.staff.city.ac.uk/~ross/papers/Applicative](http://www.staff.city.ac.uk/~ross/papers/Applicative). + +Существуют и другие типы, реализующие эти эффекты иными, +более сложными и полезными способами. +Они всегда являются аппликативными функторами и почти всегда монадами. + +## Управляемые эффекты + +Что даёт возможность управлять эффектами? +Переводя процедуры с эффектами в пространство чистых функций, мы, +не теряя выразительности, получаем возможности + +- рассуждать о процедурах и их эффектах как о сущностях внутри программы, + - в частности, буквально читать в типе функции эффекты, возможные в ней, +- проверять их корректность системой типов компилятора, +- в каждой функции ограничивать пространство эффектов только необходимыми для + решения задачи, +- создавать столь же управляемые комбинации эффектов + (это уже тема для отдельной статьи или даже книги). + +## Заключение + +Соберём в таблицу аспекты чистоты, эффекты, нарушающие эти аспекты, и типы, +моделирующие эти эффекты. + +| Свойство чистой функции | Эффект | Тип | +|---------------------------|----------------------|---------------------------| +| Все | Нет | `Identity` | +| Тотальность | Частичность | `Maybe`, `Either e`, `[]` | +| Детерминированность | Нестабильность | `Reader r` | +| Детерминированность | Множественность | `[]` | +| Нет побочных эффектов | Побочный | `Putter s`, `Writer w` | +| Детерминированность и нет побочных | Состояние | `State s` | +| Любое | Любой | `IO` | + +All above mentioned types are embedded into the language +and can be found in easily in standard packages like `base` and `mtl`, except `Putter`. I created `Putter` as an illustration, but one can envisage it as `State`, contrained to operation `put` only. + +To run the examples in this arctile I attach their code in [effects.hs](effects.hs). + +We can make a follow-up on effects implementation through types +in our further publications, based on reader interest. \ No newline at end of file diff --git a/drafts/effects.hs b/drafts/effects.hs new file mode 100644 index 00000000..f99c9ff3 --- /dev/null +++ b/drafts/effects.hs @@ -0,0 +1,2 @@ +getDataDir :: Config -> FilePath +getDataDir Config{dataDir} = dataDir