{-# OPTIONS_GHC -Wno-unused-imports #-} {-| Let's see how we can implement custom effects. We'll go through a couple of examples of increasing complexity. This part does not require going through part 2. That being said, understanding the details will help with understanding some of the restrictions and will help with compiler errors you might get. -} module Tutorial.T3_CustomEffects ( -- * Files -- $files -- * Implementations -- $implement -- * Print -- $print -- * Fork -- $fork ) where import Control.Effects import Control.Effects.State import Control.Monad.Trans.State as S hiding (State) import Control.Monad.Trans.Reader as R import Control.Monad.Trans as T (lift, MonadTrans) import GHC.Generics import Data.Text import Data.ByteString as B import Control.Monad.IO.Class import Control.Concurrent {- $files To start off, we'll define an effect for working with the filesystem. To keep it simple, we'll define two functions. One for reading the contents of a file, and one for writing them to a file. The first step is to declare a new record data type that holds the methods of our effect. The signatures of our functions will be @'FilePath' -> m 'ByteString'@ and @'FilePath' -> 'ByteString' -> m ()@ respectively. @ data Files m = FilesMethods { _readFile :: 'FilePath' -> m 'ByteString' , _writeFile :: 'FilePath' -> 'ByteString' -> m () } deriving ('Generic') @ Next, we need to provide an instance of the 'Effect' class for our effect. Both of our methods are what we call, in the jargon of this library, a simple effect. It means that they are functions that return a monadic action, and that their arguments don't depend on that monad. As an example, @m Int -> m Int@ isn't a method of a simple effect because the argument depends on @m@. Because of this fact, the instance is super simple: @ instance 'Effect' Files @ The class has two functions: 'liftThrough' and 'mergeContext'. Luckily, because our effect is simple (and mostly they will be), it's enough to just derive 'Generic' for our type. The functions are then defined for us. It's a convention to name the methods starting with an underscore so we can define the following helper functions: @ readFile :: 'MonadEffect' Files m => 'FilePath' -> m 'ByteString' writeFile :: 'MonadEffect' Files m => 'FilePath' -> 'ByteString' -> m () FilesMethods readFile writeFile = 'effect' @ So how does this work, and what does it even do? Well, we defined what a record of methods looks like for our effect, but how is that record actually constructed? If we wanted to /use/ our methods where would we get them from? Enter the 'effect' function. It's type signature is @'MonadEffect' e m => e m@. It means that for every monad @m@ which supports the effect @e@, the 'effect' function gives us an implementation of the effect's methods. Let's say we want to read the contents of a file. First we'd use the 'effect' function to get the methods of our @Files@ effect, then we'd get the @_readFile@ function out of it, and finally we'd use that function. @ myFunction = do let methods = 'effect' let rfFunction = _readFile methods rfFunction "somefile.txt" -- or equivalently _readFile 'effect' "somefile.txt" @ Now since writing @_readFile 'effect'@ gets tedious, we can define new top level helper functions: @ readFile = _readFile 'effect' writeFile = _writeFile 'effect' @ The @_readFile@ and @_writeFile@ functions are nothing more than record selectors that get the appropriate method from our @FilesMethods@ record. With this in mind we can skip them and just directly pattern match on the @FilesMethods@ constructor like @ FilesMethods readFile writeFile = 'effect' @ Check out the previous part if you want to learn more. -} {- $implement We've only defined the 'syntax' of our effect. How do we actually run those functions? This is done through the 'implement' function. It lets us construct the implementations at runtime. Suppose we have a function like @ myFunc :: 'MonadEffect' Files m => m () myFunc = do file <- readFile "file.dat" newFile <- doSomething file writeFile "file.dat" newFile @ To use it in our program we need to handle the 'MonadEffect' constraint. This is how we might do it: @ main :: IO () main = do 'implement' (FilesMethods 'B.readFile' 'B.writeFile') myFunc @ Here we implemented our effect using the 'B.readFile' and 'B.writeFile' functions from the "Data.ByteString" module. Of course we're free to implement them however we want. In a testing environment we might instead just read/write from a @'Map' 'FilePath' 'ByteString'@ and simulate a filesystem. To make it easier to use our effect, it's a good idea to provide one or more default handlers. For example, we might define two handlers: @ implementFilesViaIO :: 'MonadIO' m => 'RuntimeImplemented' Files m a -> m a implementFilesViaMap :: 'Monad' m => 'RuntimeImplemented' Files ('StateT' ('Map' 'FilePath' 'ByteString') m) a -> m a @ This way the users of our effect don't have to implement the handlers themselves, but are still free to implement more specialized ones. -} {- $print For our next effect we'll do logging. Just a simple printing function that takes anything with a 'Show' instance and logs it somewhere. The signature we want is @print :: ('MonadEffect' Print m, 'Show' a) => a -> m ()@. Now here's the main issue. The @a@ variable isn't mentioned anywhere in the effect. After all, we don't want a separate effect for each possible type. We want the @Print@ effect to provide printing /for all/ types with a 'Show' instance. To this end we'll use the @RankNTypes@ extension and define our 'Effect' instance like this: @ newtype Print m = PrintMethods { _print :: forall a. Show a => a -> m () } instance 'Effect' Print where @ Notice we didn't derive 'Generic'. This is because we can't. Despite our effect being simple (the function's parameter doesn't depend on @m@ and it returns a monadic action), the @forall@ in there makes it impossible to derive a 'Generic' instance. Unfortunately, this means that we have to implement the two functions of the 'Effect' class ourselves. Fortunately, it's a very mechanical procedure (that's why they can usually be automatically derived!). The two functions are @ 'liftThrough' :: ('MonadTrans' t, 'Monad' m, 'Monad' (t m)) => e m -> e (t m) @ and @ 'mergeContext' :: 'Monad' m => m (e m) -> e m @ [Note] The 'MonadTrans' part is a slight simplification, but it's an honest one for our current example. You can read more about the actual definitions and the purpose of these two functions in the previous part of the tutorial. The first function, 'liftThrough', takes a record of methods of the effect @e@ for the monad @m@, and is expected to return a new record, but this time for the monad @t m@. Two puzzle pieces make this very easy to do. The first is the fact that the only place where @m@ is mentioned in our effect is in the result of the @_print@ function. The second piece of the puzzle is the @'lift' :: ('MonadTrans' t, 'Monad' m) => m a -> (t m) a@ function of the 'MonadTrans' class. So to construct the new @_print@ method, we just call the old one and 'lift' the result: @ 'liftThrough' (PrintMethods pr) = PrintMethods (\\a -> 'lift' (pr a)) @ This will work for as many methods with as many parameters as you want. The implementation will always look something like @ 'liftThough' (MyMethods m1 m2 m3 m4) = MyMethods (\\a -> 'lift' (m1 a)) (\\a b c -> 'lift' (m2 a b c)) (\\a b -> 'lift' (m3 a b)) ('lift' m4) @ Up next: 'mergeContext'. It says that given the record of methods /inside a monadic context/, give me just the record, somehow pushing that context inside of it. The implementation is again very mechanical. @ 'mergeContext' pm = PrintMethods (\\a -> do PrintMethods p <- pm p a) @ Essentially, we just pass the parameters through to the old record, but first we must actually get the old record out of the monadic context. We can do that because each method's result is a monadic action. Here's how it would look like for a bigger effect: @ 'mergeContext' mm = MyMethods (\\a -> do MyMethods m _ _ _ <- mm m a) (\\a b c -> do MyMethods _ m _ _ <- mm m a b c) (\\a b -> do MyMethods _ _ m _ <- mm m a b) (do MyMethods _ _ _ m <- mm m) @ [Note] Instead of pattern matching, we can use the name of our method as a field selector: @ 'mergeContext' pm = PrintMethods (\\a -> do m <- pm _print m a) @ As you can see, there isn't much to these implementations. Just boilerplate. Also note that while our @Print@ effect may seem simple, it's actually more complicated than it needs to be. Instead we could have defined the whole thing like this: @ data Print m = PrintMethods { _printString :: 'String' -> m () } deriving ('Generic') instance 'Effect' Print where print :: ('MonadEffect' Print m, 'Show' a) => a -> m () print = _printString 'effect' . 'show' @ That way we still get a nice polymorhpic function, but the effect itself is monomorphic and lets us get away with just deriving 'Generic'. Next, we'll look at a non-simple effect. One for which the 'liftThrough' method can't be derived because there isn't just a single valid implementation. -} {- $fork Here's the challenge. There's a 'forkIO' function in base with the following signature @ 'forkIO' :: IO () -> IO 'ThreadId' @ We want to generalize this function to work with monads other than 'IO'. Essentially, we want @ fork :: 'MonadEffect' Fork m => m () -> m ('Maybe' 'ThreadId') @ [Note] The 'Maybe' part is so we don't get tied down to 'IO'. Don't worry about it for now. Notice that this isn't a simple effect as the parameter is a monadic action. Anyways, let's try defining our effect and see where we get stuck: @ data Fork m = ForkMethods { _fork :: m () -> m ('Maybe' 'ThreadId') } instance 'Effect' Fork where 'mergeContext' mm = ForkMethods (\\a -> do ForkMethods m <- mm m a) 'liftThrough' (ForkMethods f) = ForkMethods (\\a -> 'lift' (f a)) @ Simple right? Well, unfortunately it doesn't typecheck. The problem is in the 'liftThrough' function. Here are the relevant types: @ f :: m () -> m 'ThreadId' a :: t m () @ The result we need is of type @t m 'ThreadId'@. If we could somehow get a @m 'ThreadId'@ we'd be fine since just 'lift'ing that does the trick. The problem is, the only way to get a @m 'ThreadId'@ is by calling @f@ with a @m ()@, and we don't have that. What we do have is @a :: t m ()@ so it seems that we need a function that's opposite of 'lift'. Something like @unlift :: t m a -> m a@. Turns out, that's not so simple to do. Imagine you have a function of type @a -> m b@. In this case the @a ->@ part is @t@. If we specialize @unlift@ to that we get @unlift :: (a -> m b) -> m b@. There's no way to implement that function. To get @m b@ we need to have an @a@, but none are given to us. In any case, even if @unlift@ was possible to implement, it's not like we could use it. The only thing we know about @t@ is that it has a 'MonadTrans' instance (that's where we get the 'lift' function from)... Well, not exactly. Remember that note about 'MonadTrans' being a simplification? The 'Effect' class actually has an additional associated type. It lets us require a custom constraint for our transformer so we can actually require @t@ to be an instance of anything we like. [Note] This extra power doesn't come for free, though. Stricter requirements mean that your effect can't be used in certain situations. What this means exactly is a bit out of the scope of this tutorial, but here's a quick rundown. Handling effects requires monad transformers. Each effect handled will result in at least one extra transformer on your transformer stack. Those transformers are the types that need to satisfy the requirements of your effect. Having 'MonadTrans' as a requirement is basically free since each transformer has a 'MonadTrans' instance, kind of by definition. Anything extra and things get a bit more complicated. To \"lift\" functions like 'forkIO' into other transformers, people usually use the 'MonadTransControl' class from the "monad-control" package. Pretty much all the standard transformers are instances of that class, with one exception: 'ContT' isn't an instance of 'MonadTransControl'. 'ContT' is a pretty exotic transformer though. What we're going to use here is the 'RunnableTrans' class. This is an alternative to the 'MonadTransControl' class that's hopefully a bit easier to use. It lets us \"run\" a transformer if we give it the right state value. It also lets us get the current state value from the context and it can restore the context from the result of running the transformer. To cut through the confusion (or perhaps to introduce more of it) here's the code: @ data Fork m = ForkMethods { _fork :: m () -> m ('Maybe' 'ThreadId') } instance 'Effect' Fork where type 'CanLift' Fork t = 'RunnableTrans' t 'mergeContext' mm = ForkMethods (\\a -> do ForkMethods m <- mm m a) 'liftThrough' (ForkMethods f) = ForkMethods (\\a -> do st <- 'currentTransState' 'lift' (f ('void' ('runTransformer' a st))) ) @ Essentially what we do here is get the current state from the main computation (the one doing the forking), using that state to run the forked computation, discard both its result /and/ it's state using the 'void' function, then we finally call the original function and 'lift' the whole thing. [Note] Since we're discarding the result and the state of the forked computation, and this will happen for each transformer/effect in the stack, it means that only effects that \"survive\" are the final 'IO' ones. The state of the original computation does get shared with the forked one, so that's pretty useful, but if we care about what the forked computation /did/ with that state, we need to communicate with the original thread manually through some 'IO' mechanism like 'MVar's. Check out the 'Async' effect that this library provides for an alternative. What about the effect handlers? How do we write one for our @Fork@ effect? Here's one that ignores completely what the intended semantics were and just runs the thing sequentially: @ implementForkSequentially :: Monad m => RuntimeImplemented Fork m a -> m a implementForkSequentially = 'implement' (ForkMethods (\c -> c >> 'return' 'Nothing')) @ But to /really/ fork the computation we have to use the original 'forkIO' function like this: @ implementForkIO :: RuntimeImplemented Fork IO a -> IO a implementForkIO = 'implement' (ForkMethods ('fmap' 'Just' . 'forkIO')) @ Notice that this forces our monad to IO. This means no other effects can be handled after it. This is manageable if @Fork@ is the only effect with that condition, but what if there are more 'IO' based ones? A solution is to provide a @'MonadEffect' Fork IO@ instance directly @ instance 'MonadEffect' Fork IO where effect = ForkMethods ('fmap' 'Just' . 'liftIO') @ Now we can write @implementForkIO@ like this @ implementForkIO :: IO a -> IO a implementForkIO = 'id' @ As you can see, the function doesn't do anything, but it does force whatever we give it to be in the 'IO' monad. This again means that we must handle this effect after all others, but if there are other effects with the same requirement, they can all be handled at the end. -}