Написание монады с внеполосными данными (иначе говоря, составление монад параллельно) - PullRequest
3 голосов
/ 10 февраля 2012

В настоящее время я пишу монадный пакет OpenGL с именем GL, и я хочу иметь возможность запросить вычисления, чтобы получить список всех текстур, которые могут понадобиться.

Это решенная проблема? У меня много проблем с написанием экземпляра Monad для GL.

Это то, что я пробовал до сих пор:

-- GL should be able to be inspected for its HashSet without running the computation.
newtype GL a = GL (S.HashSet String) (IO a)

instance Monad (GL a) where
    return = GL S.empty . return -- Calls IO.return
    (>>=) (GL textures action) f = -- What goes here?

но я лаю не на том дереве? Это на самом деле не работает как монада, так как я должен запросить его перед запуском. Что я должен использовать вместо этого? Мне очень нравится использовать нотацию.

Я думаю, это сводится к следующему: как мне составить две монады параллельно, а затем запустить их независимо?

Ответы [ 2 ]

7 голосов
/ 10 февраля 2012

Проблема с вашим типом GL в том, что «результат вычисления» a зависит от действий ввода-вывода, и поэтому вы не можете реализовать экземпляр монады, где вы могли бы вычислить окончательный HashSet текстуры без запуска ввода-вывода.-actions.

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

type GL a = WriterT (Set String) (Writer (IO ())) a

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

К сожалению, Writer работает только для моноидов, поэтому нам нужно определить экземпляр Monoid дляIO () first.

{-# LANGUAGE FlexibleInstances #-}

import Data.Monoid

instance Monoid (IO ()) where
    mempty = return ()
    mappend = (>>)

Теперь вы можете написать функцию, которая регистрирует новую текстуру, например:

addTexture :: String -> GL ()
addTexture = tell . S.singleton

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

addIO :: IO () -> GL ()
addIO = lift . tell

Вот вспомогательная функция для запуска монады GL

runGL :: GL a -> (a, Set String, IO ())
runGL gl = let iow = runWriterT gl
               ((a, textures), io) = runWriter iow
            in (a, textures, io)

Повторяет кортеж из трех элементов: значение результата вычисления, набор накопленных текстур и накопленный текст.действия.Обратите внимание, что на данный момент значение IO () в кортеже просто описывает действие, и ничего (например, операции рисования) еще не были выполнены.

Я не уверен, охватывает ли это ваш сценарий использования, но, надеюсь, это даст вам некоторые идеи о том, как построить подходящий стек монад.Если вам нужна дополнительная помощь, приведите несколько примеров того, как вы на самом деле хотите использовать монаду GL.

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

{-# LANGUAGE FlexibleInstances #-}

import Control.Monad.Writer
import Data.Monoid
import Data.HashSet (Set)
import qualified Data.HashSet as S

instance Monoid (IO ()) where
    mempty = return ()
    mappend = (>>)

type GL a = WriterT (Set String) (Writer (IO ())) a

addTexture :: String -> GL ()
addTexture = tell . S.singleton

addIO :: IO () -> GL ()
addIO = lift . tell

runGL :: GL a -> (a, Set String, IO ())
runGL gl = let iow = runWriterT gl
               ((a, textures), io) = runWriter iow
            in (a, textures, io)

РЕДАКТИРОВАТЬ: Вы также можете избежать расширения языка, если вы оберните эффекты ввода-вывода в новый тип, как предложено dave4420.

import Control.Monad.Writer
import Data.Monoid
import Data.HashSet (Set)
import qualified Data.HashSet as S

newtype WrapIO = WrapIO { unwrapIO :: IO () }

instance Monoid WrapIO where
    mempty = WrapIO $ return ()
    WrapIO a `mappend` WrapIO b = WrapIO $ a >> b

type GL a = WriterT (Set String) (Writer WrapIO) a

addTexture :: String -> GL ()
addTexture = tell . S.singleton

addIO :: IO () -> GL ()
addIO = lift . tell . WrapIO

runGL :: GL a -> (a, Set String, IO ())
runGL gl = let iow = runWriterT gl
               ((a, textures), WrapIO io) = runWriter iow
            in (a, textures, io)
3 голосов
/ 10 февраля 2012

Когда вам нужно сделать вывод о ваших вычислениях, фактически не выполняя их, аппликативные функторы, как правило, работают лучше, чем монады, поскольку их эффекты имеют статическую структуру.

Это потому, что с аппликативными функторами ваш метод секвенированияДействия ограничены (<*>) :: f (a -> b) -> f a -> f b, поэтому функция в первом аргументе не может изменить, какие побочные эффекты произойдут, в отличие от (=<<) :: (a -> m b) -> m a -> m b, где аргумент функции свободен в выборе любого побочного эффекта, поэтому для извлечения информации об этих побочных эффектах,Вы должны оценить функцию, которая, в свою очередь, требует результата предыдущего действия и т. д., пока вы в значительной степени не будете вынуждены выполнить все это.

Быстрая аппликативная реализация будет выглядеть примерно так:

data GL a = GL (S.HashSet String) (IO a)

instance Functor GL where
  fmap f (GL s x) = GL s (fmap f x)

instance Applicative GL where
  pure x = GL S.empty (pure x)
  (GL t0 f) <*> (GL t1 x) = GL (t0 `S.union` t1) (f <*> x)

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

whenGL :: GL Bool -> GL () -> GL ()
whenGL (GL t0 cond) (GL t1 body) = GL (t0 `S.union` t1) (cond >>= \b -> if b then body else return ())

В целом, я думаю, что можно использовать аппликативы для выполнения того, что вы пытаетесь сделать, но программировать с этим может быть несколько громоздко.Особенно потому, что вы теряете такие вещи, как do-notation и различные управляющие структуры в Control.Monad.

...