Skip to content

the "alternative to monad transformers" ... as a monad transformer

License

Notifications You must be signed in to change notification settings

michaelt/extensible-streaming

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

extensible-streaming

It is an apriori of the extensible effects campaign that we are trying to avoid monad transformers.

But why would I want to resist monad transformers?

On the other hand, though, what could be more pleasing than to work with an extensible sum of functors? The Effs fs defined here is such a sum, implemented more or less following the freer package.

Actually, the items in Effs fs needn't be functors, nevertheless, by familiar tricks, Eff fs always is, and so we can stream such a bouquet of effects as we please.

There isn't any need for a special extensible effects library, in particular for a special monad. We just need a sum-of-functors (or rather -non-functors) functor containing scrutinize and inject to put things into the sum and extract them. Such a thing should be put alongside e.g. Data.Functor.Sum in the transformers library, or something like that. Then any construction anywhere that can make use of a functor constraint can make use of this sort of sum or union of functors and quasi-functors. Here, we are using Stream f m r, and just need a few combinators, like liftEff and runEffects and foldEffect to help with the construction of proper effects and with folding over the stream of them.

It is the experience of this writer - perhaps I got something wrong - that type-aligned is of value only where one is operating with a functor that splits, like

data Arith a = Plus a a | Times a a

then for sure you will use a right fold from the leaves in interpreting, and there is no hope of streaming. Where you are using functors that admit streaming, and comprehend the discipline of properly streaming program composition, it is generally a loss. (Similarly, I think there is no reason to use Data.Sequence and engage in elaborate construction of a Seq if you are proposing a strict left fold over the elements as they come.) In streaming program composition, especially with a transformer like FreeT Stream or Coroutine, one systematically avoids "re-traversing binds", and retaining references to items extracted, and accumulation in general. Everything must be destroyed as soon as it is created.

Whatever the merits of those remarks, here is a simple test program exhibiting the convenient definition of a few effects, summing them together in a single do block and running them together. It

  • maintains two different internal state calculations, one for Int, one for Integer
  • tweets occasional nonsense
  • yields both Strings and (Int,String) pairs
  • makes http get and post requests

the "interpreters" then put perfectly trivial interpretations on these 'effects'

    {-# LANGUAGE GADTs #-}
    {-# LANGUAGE TypeFamilies #-}
    {-# LANGUAGE TypeOperators #-}
    {-# LANGUAGE DataKinds, PolyKinds #-}
    {-# LANGUAGE ScopedTypeVariables #-}
    {-# LANGUAGE MultiParamTypeClasses, FlexibleInstances, FlexibleContexts #-}

    module Main (main) where
    import Streaming
    import Streaming.Extensible
    import qualified Streaming.Prelude as S
    import Control.Monad
    import Data.Function ((&))
    import qualified Data.Map as M

    main =  do
        let effects = do                                               -- SUPERIMPOSED EFFECTS:
             yield "I am a String; I was yielded."                          -- yield String
             n <- get                                                       -- get Int state
             tweet ("Tweet: I used `get` and got an Int: " ++ show n)       -- tweet
             _PUT "comments" "Nice comment page."                           -- http put
             bytes <- _GET "comments"                                       -- http get
             tweet ("Tweet: Check out this comments page: " ++ show bytes)  -- tweet
             put (n+1 ::Int)                                                -- put Int state
             yield ("A (String, Int) pair is hereby yielded: ",n+1::Int)    -- yield (String,Int)
             n <- get                                                       -- get Integer state
             _PUT "comments" $ "I just got the number " ++ show n           -- http put
             put (n+1 ::Integer)                                            -- put Integer state
        effects
        effects
                                                                       -- SUCCESSIVE INTERPRETERS:
      & pureHttp site                                                       -- realize site as a Map
      & ioTweetInterpreter                                                  -- render Tweets to stdout
      & runState (2::Integer)                                               -- initialize Integer state
      & runState (2::Int)                                                   -- initialize Int state
      & S.stdoutLn' . extrudeYieldsAt ""                                    -- interpret yields at String
      & S.print . extrudeYieldsAt ("",0::Int)                               -- interpret yields at (Int,String)
      & runEffects                                                          -- kill vestigial wrapping
      & (>>= io)                                                            -- report on final value returned
     where
      io (int,(integer,(site,()))) = do
        putStrLn "\n-------\nFinished\n-------"
        putStr "Final Int state:  "
        print int
        putStr "Final Integer state:  "
        print integer
        putStrLn "Current site: "
        mapM_ print (M.toList site)
                                                                       -- OUTPUT:
                                                                       
        -- >>> main
        -- I am a String; I was yielded.
        -- Tweet: I used `get` and got an Int: 2
        -- Tweet: Check out this comments page: "Nice comment page."
        -- ("A (String, Int) pair is hereby yielded: ",3)
        -- I am a String; I was yielded.
        -- Tweet: I used `get` and got an Int: 3
        -- Tweet: Check out this comments page: "Nice comment page.\nI just got the number 2\nNice comment page."
        -- ("A (String, Int) pair is hereby yielded: ",4)
        --
        -- -------
        -- Finished
        -- -------
        -- Final Int state:  4
        -- Final Integer state:  4
        -- Current site:
        -- ("comments","Nice comment page.\nI just got the number 2\nNice comment page.\nI just got the number 3")
        -- ("welcome","hello")



    --------------------
    -- `tweet` effect
    -------------------- 

    data Twitter a where 
      Twitter :: String -> Twitter ()

    tweet str = liftEff (Twitter str) (\() -> ()) 


    handleTweets :: (Monad m)  => (String -> m ()) -> Effects (Twitter ': fs) m r -> Effects fs m r
    handleTweets act = mapMEffect $ \(Lan (Twitter str) out) -> act str >> return (out ())


    extrudeTweets 
      :: Monad m => Effects (Twitter ': fs) m r -> Stream (Of String) (Effects fs m) r
    extrudeTweets = maps (\(Lan (Twitter s) out) -> (s :> out ()) ) . extrudeLan

    ioTweetInterpreter = handleTweets (liftIO . putStrLn)
    pureTweetInterpreter = S.toList . extrudeTweets


    --------------------------
    -- `get` and `put` effects
    -------------------------- 

    data State s r where
      Get :: State s s
      Put :: s -> State s ()

    get :: (Monad m, Elem (State s) fs) => Effects fs m s
    get = liftEff Get (\s -> s)

    put :: (Monad m, Elem (State s) fs) => s -> Effects fs m ()
    put s = liftEff (Put s) (\() -> ())

    runState :: Monad m => s -> Stream (Effs (State s ': fs)) m r -> Stream (Effs fs) m (s,r)
    runState = loop where
      loop :: Monad m => s -> Stream (Effs (State s ': fs)) m r -> Stream (Effs fs) m (s,r)
      loop s str = do
        e <- lift $ inspect str
        case e of
          Left r -> return (s,r)
          Right fs -> case scrutinize fs of
            InL (Lan (Put s) out) -> loop s (out ()) 
            InL (Lan Get out)     -> loop s (out s)
            InR fs                -> wrap (fmap (loop s) fs)

    -- runState' :: Monad m => s -> Stream (Effs (State s ': fs)) m r -> Stream (Effs fs) m (s,r)

    --------------------
    -- `yield` effect
    --------------------             

    -- we just yield with `Of a r` from Streaming.Prelude

    yield :: (Monad m, Elem (Of a) fs) =>  a -> Effects fs m ()
    yield x = liftEff (x:> ()) id  -- compare Streaming.yield x = wrap (x :> id)

    extrudeYields  :: (Monad m)
      => Effects (Of a ': fs) m r
      -> Stream (Of a) (Effects fs m) r
    extrudeYields = extrude

    extrudeYieldsAt  :: (Monad m)
      => a -> Effects (Of a ': fs) m r
      -> Stream (Of a) (Effects fs m) r
    extrudeYieldsAt a = extrude 


    --------------------
    -- HTTP effects
    --------------------      

    type Bytes = String
    type Path = String

    data Http a where -- compare to the 'normal' (functor) type in http://degoes.net/articles/modern-fp/
      GET :: Path -> Http Bytes
      PUT :: Path -> Bytes -> Http Bytes
      POST :: Path -> Bytes -> Http Bytes
      DELETE :: Path -> Http Bytes

    _GET :: (Monad m, Elem Http fs) => Path -> Effects fs m Bytes
    _GET path = liftEff (GET path) id 

    _PUT :: (Monad m, Elem Http fs) =>
         Path -> Bytes -> Effects fs m Bytes
    _PUT path bytes = liftEff (PUT path bytes) id

    _POST :: (Monad m, Elem Http fs) =>  Path -> Bytes -> Effects fs m Bytes
    _POST path bytes = liftEff (POST path bytes) id

    _DELETE :: (Monad m, Elem Http fs) => Path -> Effects fs m Bytes
    _DELETE path = liftEff (DELETE path) id

    site = M.fromList [("welcome","hello")]

    -- this in dire need of a combinator ...
    pureHttp = loop where
      loop :: Monad m   -- the type signature is needed here
            => M.Map Bytes Bytes 
            -> Stream (Effs (Http ': fs)) m r 
            -> Stream (Effs fs) m (M.Map Bytes Bytes,r)
      loop m str = do
        e <- lift $ inspect str
        case e of
          Left r -> return (m, r)
          Right fs -> case scrutinize fs of
            InL (Lan (GET p) out) -> case M.lookup p m of
              Nothing -> loop m (out "404") 
              Just b  -> loop m (out b) 
            InL (Lan (POST p b) out) -> loop (M.insert p b m) (out $ "posted " ++ p) 
            InL (Lan (DELETE p) out) -> case M.lookup p m of
              Nothing -> loop m (out $ p ++ " doesn't exist")
              Just b  -> loop (M.delete p m) (out $ "deleted" ++ p)
            InL (Lan (PUT p b) out)  -> case M.lookup "comments" m of
              Nothing -> loop (M.insert "comments" b m) (out "comments page created")
              Just a  -> loop (M.insert "comments" (a++"\n" ++ b) m) (out "comment added")
            InR fs                   -> wrap (fmap (loop m) fs)

About

the "alternative to monad transformers" ... as a monad transformer

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published