May. 1st, 2008

migmit: (Default)

   


В рассылке SPbHUG подняли тему, которую мы недообсуждали на очередном заседании - тему утечек памяти в монадических вычислениях. Поскольку тут надо писать приличное количество кода, выношу это всё в отдельный пост, точнее, в два.

Итак, собственно утечки памяти в монадах не слишком-то существенны. Точнее, я бы сказал, вообще не существенны - сборщик мусора подберёт, когда надо. Иное дело, если эта самая память находится там, куда сборщику хода нет.

Предположим, мы хотим вычислять некоторые значения, по ходу дела обращаясь к пользователю с вопросами и выслушивая его бредни ответы. Допустим, также, что обмен информацией с пользователем должен быть stateless - то есть, задав пользователю вопрос, мы тут же завершаемся, и в следующий раз запускаем функцию заново. При этом, конечно, некоторая информация - некий State - должна передаваться пользователю вместе с вопросом, и возвращаться назад неизменённой вместе с ответом. Придётся поверить пользователю в том, что он таки будет возвращать эту информацию без изменений. В реальности, конечно, пользователь будет пользоваться каким-то клиентом, который позаботится об этом сам - например, веб-браузером.

> {-# LANGUAGE Arrows #-}

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

> module Test where
> import Control.Arrow
> import Control.Monad
> import Data.Either
> import Data.Maybe

Пока ничего интересного.

Далее, заведём тип для передаваемой пользователю информации. Этот тип будет содержать а) задаваемый пользователю вопрос (строку) - или получаемый от него ответ (тоже строку), и б) кое-какую информацию о прошлых состояниях - эту информацию пользователь обязуется не изменять.

> data StateM = StateM String [Maybe StateM] deriving Show

Желающие сообщить мне, что тут я заново изобрёл Лисп, могут не трудиться. Я, типа, знаю.

Попробуем, для начала, уложить всё в монаду. Наше монадическое значение будет представлять собой функцию, которая на вход принимает последний ответ пользователя (если таковой вообще был), а на выход выдаёт новый вопрос для пользователя - или же результирующее значение.

> newtype M a = M (Maybe StateM -> Either StateM a)

Для удобства придумаем своего рода "клиента" для пользователя - функцию, которая возьмёт на себя задачу следить за сохранностью State.

> runM :: M a -> IO a
> runM mx = runM' Nothing where
>     runM' s =
>         case mx $: s of
>           Left (StateM str ss) ->
>               do putStrLn str
>                  putStrLn $ "debug: " ++ show ss -- эта строка здесь стоит исключительно в целях отладки. Её можно убрать, не нарушив основную функциональность "клиента".
>                  putStr ">>> "
>                  str' <- getLine
>                  runM' $ Just $ StateM str' ss
>           Right x -> return x

Для простоты, я введу оператор, аналогичный ($), работающий с нашими функциями.

> infixr 0 $:
> M t $: s = t s

Теперь объявим нашу монаду монадой.

> instance Monad M where

Ну, return x никакого обмена с пользователем не ведёт вообще, сразу возвращая x.

>     return x = M $ \Nothing -> Right x

Ситуация с bind интереснее. Выполняя mx >>= f, мы, конечно, должны сначала выполнить mx и получить x. Затем, мы должны выполнить f x. Но, выполняя f x, мы должны каждый раз запоминать значение x, чтобы в следующий раз, когда придёт ответ пользователя, дёрнуть именно f x, а не что-нибудь вроде f (_|_). Поэтому, мы должны запомнить последний State, после которого mx вернула значение, и использовать его для вычисления x снова и снова.

Конечно, если mx вообще никак с пользователем не связывается, мы можем всех этих "запоминаний" не делать.

>     mx >>= f =
>         case mx $: Nothing of
>           Right x -> f x
>           _ -> M t'
>         where

Функция handle выполняет основную работу. У неё есть два параметра (типа Maybe StateM). Первый из них передаётся в mx, второй - в (f x), если, конечно, mx завершила работу и вернула x. Заметим, что, если mx не вернула значение, handle запоминает этот факт, приписывая Nothing к стеку состояний - чтобы не перепутать в следующий раз. Если же mx вернула значение, handle приписывает к стеку состояний то состояние, которое было передано mx - чтобы в следующий раз его вспомнить.

>           handle toFirst toSecond =
>               case mx $: toFirst of
>                 Left (StateM str ss) -> Left $ StateM str $ Nothing:ss
>                 Right x ->
>                     case f x $: toSecond of
>                       Left (StateM str ss) -> Left $ StateM str $ toFirst:ss
>                       Right y -> Right y

Наконец, mx >>= f, начав работу, передаёт сигнал начать работу mx, а затем, если надо, и f x (это на случай, если mx завершится, а не потребует задать пользователю вопрос). Приняв ответ от пользователя, она для начала определяет, был ли вопрос задан внутри mx или внутри f x. Для этого, она смотрит на первый элемент в стеке состояний. Если этот первый элемент - Nothing, значит, вопрос был задан внутри mx, и надо запустить её заново, сообщив ей ответ пользователя, а в f x передать сигнал начала работы (если mx завершится и выдаст значение x). Если же первый элемент - некоторое состояние, значит, вопрос был задан внутри f x. Поэтому, нужно по этому первому элементу определить x - для чего придётся снова запустить mx - а затем снова запустить f x, передав этой функции ответ пользователя.

>           t' Nothing = handle Nothing Nothing
>           t' (Just (StateM str (Nothing:ss))) = handle (Just $ StateM str ss) Nothing
>           t' (Just (StateM str (s:ss))) = handle s $ Just $ StateM str ss

Не помешает и строительный кирпичик - функция, задающая вопрос пользователю, и возвращающая ответ.

> askM s = M ask' where
>     ask' Nothing = Left $ StateM s []
>     ask' (Just (StateM str [])) = Right str

Напишем тестовую функцию, в духе того, что предлагалось в SPbHUG:

> testM =
>     do a1 <- askM "1?"
>        a2 <- askM $ "You said: " ++ a1 ++ "; 2?"
>        a3 <- askM $ "You said: " ++ a2 ++ "; 3?"
>        a4 <- askM $ "You said: " ++ a3 ++ "; 4?"
>        a5 <- askM $ "You said: " ++ a4 ++ "; 5?"
>        askM $ "You said: " ++ a5
>        return ()

Думаю, смысл этой функции всем понятен. Запускаем:

*Test> runM testM
1?
debug: [Nothing]
>>> a
You said: a; 2?
debug: [Just (StateM "a" []),Nothing]
>>> b
You said: b; 3?
debug: [Just (StateM "a" []),Just (StateM "b" []),Nothing]
>>> c
You said: c; 4?
debug: [Just (StateM "a" []),Just (StateM "b" []),Just (StateM "c" []),Nothing]
>>> d
You said: d; 5?
debug: [Just (StateM "a" []),Just (StateM "b" []),Just (StateM "c" []),Just (StateM "d" []),Nothing]
>>> e
You said: e
debug: [Just (StateM "a" []),Just (StateM "b" []),Just (StateM "c" []),Just (StateM "d" []),Just (StateM "e" []),Nothing]
>>>

Получается вот что. Работает-то наша монада хорошо, но вот отладочная печать показывает, что, чем дальше мы идём, тем больше информации передаём пользователю. Фактически, мы передаём ему снова и снова все ответы, которые он уже дал - и заставляем повторять их заново. Хотя, заметим, уже не нуждаемся в них - ну на фиг, скажите, знать ответ на первый вопрос, когда мы задаём пятый? Таким образом, получается та самая утечка - информация, идущая пользователю и не нужная ему, оказывается недоступной сборщику мусора (поскольку он никаким образом не может ручаться, что эта информация таки не нужна - это знает программист, но не компилятор и не рантайм). Соответственно, мусорная куча всё растёт и растёт.

Можно переписать этот пример в соответствии с тем, что предлагалось в SPbHUG.

> testM' =
>     do a5 <- do a4 <- do a3 <- do a2 <- do a1 <- askM "1?"
>                                            askM $ "You said: " ++ a1 ++ "; 2?"
>                                   askM $ "You said: " ++ a2 ++ "; 3?"
>                          askM $ "You said: " ++ a3 ++ "; 4?"
>                 askM $ "You said: " ++ a4 ++ "; 5?"
>        askM $ "You said: " ++ a5
>        return ()

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

*Test> runM testM'
1?
debug: [Nothing,Nothing,Nothing,Nothing,Nothing]
>>> a
You said: a; 2?
debug: [Nothing,Nothing,Nothing,Nothing,Just (StateM "a" [])]
>>> b
You said: b; 3?
debug: [Nothing,Nothing,Nothing,Just (StateM "b" [Just (StateM "a" [])])]
>>> c
You said: c; 4?
debug: [Nothing,Nothing,Just (StateM "c" [Just (StateM "b" [Just (StateM "a" [])])])]
>>> d
You said: d; 5?
debug: [Nothing,Just (StateM "d" [Just (StateM "c" [Just (StateM "b" [Just (StateM "a" [])])])])]
>>> e
You said: e
debug: [Just (StateM "e" [Just (StateM "d" [Just (StateM "c" [Just (StateM "b" [Just (StateM "a" [])])])])]),Nothing]
>>>

Продолжение следует.

Originally posted on migmit.vox.com

migmit: (Default)

Продолжаем. Попробуем решить эту проблему при помощи стрелок. Я буду использовать немного другой тип состояния - различия чисто технические, ничего особенного.

> data StateA = StateA (Maybe String) [StateA] deriving Show

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

> data A a b = A ((StateA -> a) -> (StateA, Maybe [Bool]) -> (StateA, Maybe [Bool])) ((StateA -> a) -> (StateA -> b))

Значения типа Maybe [Bool] понадобятся нам для отслеживания, в какой части стрелки был задан вопрос. В частности, значение Nothing на входе сигнализирует о том, что работа только что началась, а на выходе - о том, что работа окончена и можно забирать выходное значение.

Как и для монад, мы соорудим "клиента" для пользователя.

> runA :: A a b -> (a -> IO b)
> runA (A t f) x = runA' (StateA Nothing [], Nothing) where
>     h (StateA Nothing []) = x -- функция, по начальному состоянию (StateA Nothing []) устанавливающая значение x.
>     runA' c =
>         case t h c of
>           (s, Nothing) -> return $ f h s
>           (StateA (Just str) ss,Just bs) ->
>               do putStrLn str
>                  putStrLn $ "debug: " ++ show (ss,bs) -- эта строка тоже стоит здесь исключительно в целях отладки.
>                  putStr ">>> "
>                  str' <- getLine
>                  runA' (StateA (Just str') ss,Just bs)

Объявим нашу стрелку стрелкой.

Во-1, стрелка arr f не задаёт пользователю никаких вопросов и не выслушивает никаких ответов.

Во-2, стрелка A1 >>> A2 выполняет вначале A1 - приписывая к каждому вопросу False (функцией liftM (False:)) для индикации того, что выполняется первая стрелка - после чего выполняет A2, приписывая к каждому вопросу True. Когда же от пользователя приходит ответ, то первая часть в Maybe [Bool] используется для определения того, был ли задан вопрос внутри A1 или внутри A2, после чего убирается функцией liftM tail.

В-3, стрелка first A при первом вызове запоминает текущее состояние, и его же передаёт в стрелку A; при каждом последующем вызове, первый элемент в стеке состояний снимается, остаток обрабатывается стрелкой A, и снова навешивается первый элемент. Таким образом, первоначальное состояние проходит неизменным через всю цепочку вопросов-ответов, и в конце используется для определения той части, которая должна пройти неизменной через стрелку.

> instance Arrow A where
>     arr f = A (\_ (s, Nothing) -> (s, Nothing)) (\h s -> f $ h s)
>     ~(A t1 f1) >>> ~(A t2 f2) = A t $ f2 . f1
>         where
>           secondPart h p = let (s, mbs) = t2 (f1 h) p in (s, liftM (True:) mbs)
>           t h (s, Just (True:bs)) = secondPart h (s, Just bs)
>           t h (s, mbs) =
>               case t1 h (s, liftM tail mbs) of
>                 (s', Nothing) -> secondPart h (s', Nothing)
>                 (s', Just bs) -> (s', Just $ False:bs)
>     first ~(A t f) = A t' f'
>         where
>           t' h (s@(StateA str ss), mbs) =
>               let (s1, s2) =
>                       case mbs of
>                         Nothing -> (s,s)
>                         Just _ -> (StateA str $ tail ss, head ss)
>                   (StateA str' ss', mbs') = t (fst . h) (s1, mbs)
>               in (StateA str' (s2:ss'), mbs')
>           f' h (StateA str (s:ss)) = (f (fst . h) $ StateA str ss, (snd . h) s)

В принципе, в first открыта дорога для оптимизаций - например, first (first ...) будет сохранять исходное состояние ДВАЖДЫ, что излишне - но эти оптимизации для дальнейшего несущественны.

Как и раньше, нам нужен строительный блок - стрелка, задающая пользователю вопрос, и возвращающая ответ.

> askA = A t $ const $ \(StateA (Just str) _) -> str
>     where
>       t h (s, Nothing) = (StateA (Just $ h s) [], Just [])
>       t h (s, Just []) = (s, Nothing)

И снова тестируем. На сей раз, мы будем передавать в каждый следующий запрос только ту информацию, которая ему нужна.

> testA = arr (\() -> "1?") >>> askA >>>
>         arr (\a1 -> "You said: " ++ a1 ++ "; 2?") >>> askA >>>
>         arr (\a2 -> "You said: " ++ a2 ++ "; 3?") >>> askA >>>
>         arr (\a3 -> "You said: " ++ a3 ++ "; 4?") >>> askA >>>
>         arr (\a4 -> "You said: " ++ a4 ++ "; 5?") >>> askA >>>
>         arr (\a5 -> "You said: " ++ a5) >>> askA >>>
>         arr (\_ -> ())

Проверяем:

*Test> runA testA ()
1?
debug: ([],[True,False])
>>> a
You said: a; 2?
debug: ([],[True,True,True,False])
>>> b
You said: b; 3?
debug: ([],[True,True,True,True,True,False])
>>> c
You said: c; 4?
debug: ([],[True,True,True,True,True,True,True,False])
>>> d
You said: d; 5?
debug: ([],[True,True,True,True,True,True,True,True,True,False])
>>> e
You said: e
debug: ([],[True,True,True,True,True,True,True,True,True,True,True,False])
>>>

Всё отлично работает. Конечно, довольно много передаётся как Maybe [Bool], но это тип сам по себе маленький, и несколько бит нас не волнуют. Главное, предыдущие ответы пользователя НЕ протаскиваются через всю последовательность вопросов-ответов.

А теперь напишем примерно то же самое в стрелочном do-синтаксисе:

> testA' =
>     proc () ->
>         do a1 <- askA -< "1?"
>            a2 <- askA -< "You said: " ++ a1 ++ "; 2?"
>            a3 <- askA -< "You said: " ++ a2 ++ "; 2?"
>            a4 <- askA -< "You said: " ++ a3 ++ "; 2?"
>            a5 <- askA -< "You said: " ++ a4 ++ "; 2?"
>            askA -< "You said: " ++ a5
>            returnA -< ()

Проверяем:

*Test> runA testA' ()
1?
debug: ([StateA Nothing []],[True,False,True,False,True])
>>> a
You said: a; 2?
debug: ([StateA (Just "a") [StateA Nothing []]],[True,True,False,True,False,True])
>>> b
You said: b; 2?
debug: ([StateA (Just "b") [StateA (Just "a") [StateA Nothing []]]],[True,True,True,False,True,False,True])
>>> c
You said: c; 2?
debug: ([StateA (Just "c") [StateA (Just "b") [StateA (Just "a") [StateA Nothing []]]]],[True,True,True,True,False,True,False,True])
>>> d
You said: d; 2?
debug: ([StateA (Just "d") [StateA (Just "c") [StateA (Just "b") [StateA (Just "a") [StateA Nothing []]]]]],[True,True,True,True,True,False,True,False,True])
>>> e
You said: e
debug: ([StateA (Just "e") [StateA (Just "d") [StateA (Just "c") [StateA (Just "b") [StateA (Just "a") [StateA Nothing []]]]]]],[True,True,True,True,True,True,False,True,False,True])
>>>

Упс. Работать-то оно работает, но... мы снова получаем ту же проблему, что и с монадами. Старые, ненужные ответы пользователя снова начали протаскиваться через всю цепочку. Почему? Потому что препроцессор стрелочного синтаксиса глуп. Нет особых проблем преобразовать testA' в testA автоматически. Увы, но даже при помощи Template Haskell это не сделать - Template Haskell не знает о стрелочном do-синтаксисе ничего. Нужен внешний препроцессор.

Резюме. Это - одна из тех вещей, за которые я не люблю стрелки. Уж слишком в их реализации виден подход "абы как, лишь бы работало".

Originally posted on migmit.vox.com