Kleisli Arrow в Netwire 5?

Я пытаюсь создать игру, используя Haskell + Netwire 5 (+ SDL). Теперь я работаю над частью вывода, где я хотел бы создать проводники, которые читаются в каком-то игровом состоянии и выводят поверхности SDL для отображения на экране.

Однако проблема в том, что поверхности SDL содержатся в монаде IO, поэтому любая функция, создающая такие поверхности, должна иметь тип a -> IO b. Конечно, arr не создает Wire из a -> m b. Однако, поскольку сигнатура типа провода (Monad m, Monoid e) => Wire s e m a b, он очень похож на Kleisi Arrow, но я не могу найти подходящий конструктор для изготовления такого провода.

Я новичок в FRP и Arrows и не много программировал на Haskell, так что это может быть не лучший способ реализовать вывод графики. Если я ошибаюсь с самого начала, пожалуйста, дайте мне знать.

Некоторые связанные функции SDL:

createRGBSurfaceEndian :: [SurfaceFlag] -> Int -> Int -> Int -> IO Surface

fillRect :: Surface -> Maybe Rect -> Pixel -> IO Bool

blitSurface :: Surface -> Maybe Rect -> Surface -> Maybe Rect -> IO Bool

flip :: Surface -> IO ()

Обновление 1

Этот тип кода проверяет, но теперь я пытаюсь связать его с SDL для тестирования.

wTestOutput :: (Monoid e) => Wire s e IO () SDL.Surface
wTestOutput = mkGen_ $ \a -> (makeSurf a >>= return . Right)
    where
      makeSurf :: a -> IO SDL.Surface
      makeSurf _ = do
        s <- SDL.createRGBSurfaceEndian [SDL.SWSurface] 800 600 32
        SDL.fillRect s (Just testRect) (SDL.Pixel 0xFF000000)
        return s
      testRect = SDL.Rect 100 100 0 0

person Carl Dong    schedule 23.09.2015    source источник
comment
Судя по всему, вы уже ответили на свой вопрос. Поместите это как ответ, а не добавляйте его к своему вопросу. (В частности, вероятно, достаточно упомянуть mkGen_ и его тип в ответе, конкретная реализация, вероятно, менее интересна для будущих читателей). Если у вас есть дополнительные вопросы, задавайте их отдельно, а не меняйте вопросы.   -  person Cubic    schedule 24.09.2015
comment
Нет, я еще не проверял, работает ли он   -  person Carl Dong    schedule 24.09.2015
comment
Теперь я как бы проверил, используя putStrLn в качестве более простого примера.   -  person Carl Dong    schedule 29.09.2015
comment
Провода и FRP в целом — это способ моделирования значений, изменяющихся во времени. Они вообще не должны ничего создавать. Ваша программа, как вы ее написали, будет создавать новую поверхность SDL каждый раз, когда оценивается провод!   -  person Mokosha    schedule 30.09.2015
comment
Будут ли ранее неиспользованные поверхности собирать мусор? Конечно, семантически это то же самое, что и обновление одной и той же поверхности (во всяком случае, это состояние), но я думаю, что это детали реализации.   -  person Carl Dong    schedule 30.09.2015


Ответы (1)


Теперь, поиграв со Стрелками, отвечу на свой вопрос, используя функцию putStrLn. Он имеет тип String -> IO (), то есть a -> m b, поэтому метод следует обобщить на все провода Клейсли. Я также показываю, как управлять проводом, и результат удивительно прост.

Весь код написан на Literate Haskell, так что просто скопируйте его и запустите.

Во-первых, есть некоторые импорты для библиотеки Netwire 5.

import Control.Wire
import Control.Arrow
import Prelude hiding ((.), id)

Теперь это основа изготовления проволоки Клейсли. Предположим, у вас есть функция с типом a -> m b, которую нужно поднять в провод. Теперь обратите внимание, что mkGen_ имеет тип mkGen_ :: Monad m => (a -> m (Either e b)) -> Wire s e m a b

Итак, чтобы сделать провод из a -> m b, нам сначала нужно получить функцию с типом a -> m (Either () b). Обратите внимание, что левый блокирует провод, а правый активирует его, поэтому внутренняя часть имеет номер Either () b вместо Either b (). На самом деле, если вы попробуете последнее, неясная ошибка компиляции скажет вам, что вы сделали это неправильно.

Чтобы получить a -> m (Either () b), сначала рассмотрим, как получить m (Either () b) из m b, мы извлекаем значение из монады (m b), поднимаем ее вправо, затем возвращаемся к монаде m. Короче: mB >>= return . Right. Поскольку здесь нет значения «mB», мы делаем лямбда-выражение, чтобы получить a -> m (Either () b):

liftToEither :: (Monad m) => (a -> m b) -> (a -> m (Either () b))
liftToEither f = \a -> (f a >>= return . Right)

Теперь мы можем сделать проволоку Клейсли:

mkKleisli :: (Monad m, Monoid e) => (a -> m b) -> Wire s e m a b
mkKleisli f = mkGen_ $ \a -> (f a >>= return . Right)

Итак, давайте попробуем каноническую проводку «hello, world»!

helloWire :: Wire s () IO () ()
helloWire = pure "hello, world" >>> mkKleisli putStrLn

Теперь идет основная функция, чтобы проиллюстрировать, как управлять проводом. Обратите внимание, что по сравнению с исходным кодом testWire в the Control.Wire.Run из библиотеки Netwire, здесь нет необходимости использовать liftIO: внешняя программа ничего не знает о внутренней работе проводов. Он просто шагает по проводам, игнорируя то, что в нем находится. Maybe это Just означает лучшую композицию, чем использование Nothing о Kleisli Wires? (Не каламбур!)

main = go clockSession_ helloWire
    where
      go s w = do
        (ds, s') <- stepSession s
        (mx, w') <- stepWire w ds (Right ())
        go s' w'

А вот и код. К сожалению, StackOverflow не очень хорошо работает с Literate Haskell...

{-# LANGUAGE Arrows #-}

module Main where

import Control.Wire
import Control.Monad
import Control.Arrow
import Prelude hiding ((.), id)

mkKleisli :: (Monad m, Monoid e) => (a -> m b) -> Wire s e m a b
mkKleisli f = mkGen_ $ \a -> liftM Right $ f a

helloWire :: Wire s () IO () ()
helloWire = pure "hello, world" >>> mkKleisli putStrLn

main = go clockSession_ helloWire
    where
      go s w = do
        (ds, s') <- stepSession s
        (mx, w') <- stepWire w ds (Right ())
        go s' w'

Обновить

Благодаря вдохновению Cubic. liftToEither на самом деле может быть записано, как вы уже догадались, liftM:

liftToEither f = \a -> liftM Right $ f a
mkKleisli f = mkGen_ $ \a -> liftM Right $ f a
person Carl Dong    schedule 29.09.2015
comment
Обратите внимание, что m >>= return . f — это просто fmap f m. Также я не уверен, что вы имеете в виду, когда говорите, что putStrLn обобщает все провода? - person Cubic; 29.09.2015
comment
Когда я говорю, что это распространяется на все проводники, я на самом деле имею в виду, что аналогичные вещи применимы к переносу всех a -> m b функций в Wire s e m a b. - person Carl Dong; 29.09.2015
comment
Верно, \a -> fmap Right $ f a тоже работает. Но я думаю, что \a -> (f a >>= return . Right), вероятно, лучше подходит для этой цели? - person Carl Dong; 29.09.2015
comment
Нет, извините, fmap не работает с Monads. Компилятор говорит мне добавить Functor m, но это неправильно. - person Carl Dong; 29.09.2015
comment
Хорошо, это должно быть liftM вместо fmap. Я добавлю это в свой ответ - person Carl Dong; 29.09.2015
comment
Вы используете старую версию ghc. Начиная с 7.10, монада подразумевает функтор (и аппликатив). Даже до этого большинство библиотек, определяющих монады, уже определили соответствующие экземпляры функторов/приложений. О, и я не уверен, гарантируется ли это где-либо, но вы обнаружите, что liftm почти всегда просто Gmail fmap с более строгой подписью типа - person Cubic; 29.09.2015
comment
Я понимаю. Я на Ubuntu 14.04, поэтому у меня только 7.6. Поскольку мне нравится использовать общий (Monad m => ...), и я не хочу слишком сильно взламывать свою систему, я бы оставил ее такой. - person Carl Dong; 29.09.2015
comment
Установка программного обеспечения не взламывает вашу систему. Я обнаружил, что репозитории пакетов Ubuntu совершенно бесполезны для вещей, которые активно разрабатываются (что, безусловно, верно для Haskell). Вы обнаружите, что существуют PPA, предоставляющие последние версии ghc. Вы также можете использовать стек и вместо этого позволить ему управлять ghc. - person Cubic; 29.09.2015