Kleisli Arrow in Netwire 5?
Asked Answered
C

1

3

I am trying to create a game using Haskell + Netwire 5 (+ SDL). Now I am working on the output part, where I would like to create wires that read in some game state and output the SDL surfaces to be blitted on screen.

However, the problem is that SDL surfaces are contained in IO monad, so any function that creates such surfaces must have type a -> IO b. Of course, arr does not construct a Wire from a -> m b. However, since the type signature of a wire is (Monad m, Monoid e) => Wire s e m a b, it looks quite like a Kleisi Arrow, but I cannot find a suitable constructor for making such a wire.

I am new to FRP and Arrows, and have not programmed a lot in Haskell, so this may not be the best way to implement the graphics output. If I am wrong from the beginning, please let me know.

Some SDL functions related:

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 ()

Update 1

This code type checks, but now I am trying to interface it with SDL for testing

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
Chancellery answered 23/9, 2015 at 17:33 Comment(5)
Apparently you've already answered your own question. Put it as an answer, instead of adding it to your question. (In particular, it's probably enough to mention mkGen_ and it's type in the answer, the particular implementation is likely less interesting for future readers). If you have more questions, ask them separately instead of changing your questions.Page
No, I have not verified whether it works yetChancellery
Now I kind of verified using putStrLn as a simpler example.Chancellery
Wires, and FRP in general, are a way to model time-varying values. They shouldn't really "create" anything ever. Your program, as you have it written, will create a new SDL surface every time the wire is evaluated!Tonsorial
Will the previous unused surfaces be garbage collected? Of course, semantically it is the same to update the same surface (it is state, anyway), but I think those are implementation details.Chancellery
C
1

Now, after playing around with Arrows, I will answer my own question using the function putStrLn. It has type String -> IO (), which is a -> m b, so the method should generalize to all Kleisli wires. I also illustrate how to drive the wire, and the result is amazingly simple.

The entire code is written in Literate Haskell, so just copy it and run.

First, there are some imports for the Netwire 5 library

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

Now, this is the core of making a Kleisli Wire. Assume you have a function with type a -> m b that needs to be lifted into a wire. Now, notice that mkGen_ has type mkGen_ :: Monad m => (a -> m (Either e b)) -> Wire s e m a b

So, to make a wire out of a -> m b, we first need to get a function with type a -> m (Either () b). Notice that Left inhibits the wire, while Right activates it, so the inner part is Either () b instead of Either b (). Actually, if you try the latter, an obscure compile error will tell you get this in the wrong way.

To get a -> m (Either () b), first consider how to get m (Either () b) from m b, we extract the value from the monad (m b), lift it to Right, then return to the monad m. In short: mB >>= return . Right. Since we don't have the value "mB" here, we make a lambda expression to get a -> m (Either () b):

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

Now, we can make a Kleisli wire:

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

So, let's try the canonical "hello, world" wire!

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

Now comes the main function to illustrate how to drive the wire. Note that comparing to the source of testWire in the Control.Wire.Run from the Netwire library, there is no use of liftIO: the outer program knows nothing about how the wires work internally. It merely steps the wires ignoring what is in it. Maybe this Just means better composition than using Nothing about Kleisli Wires? (No pun intended!)

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

Now here comes the code. Unfortunately StackOverflow does not work quite well with 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'

Update

Thanks to Cubic's inspiration. liftToEither can actually be written in, you guess it, liftM:

liftToEither f = \a -> liftM Right $ f a
mkKleisli f = mkGen_ $ \a -> liftM Right $ f a
Chancellery answered 29/9, 2015 at 2:42 Comment(8)
Note that m >>= return . f is just fmap f m. Also I'm not sure what you mean when you say putStrLn generalizes to all wires?Page
When I say it generalizes to all wires, I actually mean that similar things applies to lifting all a -> m b functions into Wire s e m a bChancellery
Right, \a -> fmap Right $ f a works too. But I think \a -> (f a >>= return . Right) probably expresses better for this purpose?Chancellery
No, sorry, fmap doesn't seem to work with Monads. The compiler tells me to add Functor m, but that is not correct.Chancellery
OK, that should be liftM instead of fmap. I will add that in my answerChancellery
You're using an old version of ghc. Starting with 7.10, monad implies functor (and applicative). Even before that, most libraries defining monads already defined corresponding functor/applicative instances. Oh and I'm not sure if that's guaranteed anywhere but you'll find that liftm is pretty much always just Gmail fmap with a stricter type signaturePage
I see. I am on Ubuntu 14.04, so I only get 7.6. Since I like to use the general (Monad m => ...), and don't want to hack my system too much, I would keep it like that.Chancellery
Installing software isn't "hacking your system". I found that ubuntus package repositories are pretty worthless for things that are actively being developed (which is certainly true for Haskell). You'll find that there are ppas providing up to date versions of ghc. You could also use stack and let that manage ghc for you instead.Page

© 2022 - 2024 — McMap. All rights reserved.