An [operational monad](http://apfelmus.nfshost.com/articles/operational-monad.html) is a data type parameterised by a set of operations `t`, and it gives a `Monad` instance for free. It upgrades extensible sums, aka. [open unions](http://okmij.org/ftp/Haskell/extensible/index.html#open-union), into an extensible effect monad (Oleg Kiselyov and Hiromi Ishii. [Freer Monads, More Extensible Effects](http://okmij.org/ftp/Haskell/extensible/more.pdf), 2015). Extensible effects release you from the obligation to write a lot of instances to make your monadic actions lift-free (see also [Named extensible effects](https://www.schoolofhaskell.com/user/fumieval/extensible/named-extensible-effects)). I uploaded a new version of [monad-skeleton-0.1.4](http://hackage.haskell.org/package/monad-skeleton-0.1.4), an operational monad library. As a result of performance optimisation in the new version, [extensible](https://hackage.haskell.org/package/extensible), the extensible effects library based on it, is now much faster than the other well-known libraries. I [benchmark](https://github.com/fumieval/extensible/blob/3d9030397ab39ec539b30ffd73804784386b94bb/benchmarks/eff-comparison.hs)ed several libraries using equivalents of the following code: ```haskell testMTL :: (MonadReader Int m, MonadState Int m, MonadWriter (Sum Int) m) => m () testMTL = replicateM_ 100 $ do r <- ask s <- get tell (Sum s) put $! s + r ``` Here's the result. `extensible` is twice as fast as `freer-effects`: ``` extensible-0.4.2: benchmarks Running 1 benchmarks... Benchmark eff-comparison: RUNNING... benchmarking rws/extensible time 9.752 μs (9.714 μs .. 9.785 μs) 1.000 R² (1.000 R² .. 1.000 R²) mean 9.767 μs (9.733 μs .. 9.820 μs) std dev 142.7 ns (95.88 ns .. 202.9 ns) variance introduced by outliers: 11% (moderately inflated) benchmarking rws/mtl time 794.5 ns (791.9 ns .. 797.1 ns) 1.000 R² (1.000 R² .. 1.000 R²) mean 802.1 ns (795.9 ns .. 810.3 ns) std dev 24.06 ns (16.43 ns .. 30.87 ns) variance introduced by outliers: 42% (moderately inflated) benchmarking rws/mtl-RWS time 586.4 ns (581.8 ns .. 591.7 ns) 1.000 R² (0.999 R² .. 1.000 R²) mean 585.2 ns (582.6 ns .. 590.2 ns) std dev 10.94 ns (7.718 ns .. 17.81 ns) variance introduced by outliers: 22% (moderately inflated) benchmarking rws/exteff time 130.3 μs (127.5 μs .. 134.0 μs) 0.997 R² (0.993 R² .. 1.000 R²) mean 128.4 μs (127.6 μs .. 130.7 μs) std dev 3.986 μs (1.817 μs .. 8.008 μs) variance introduced by outliers: 28% (moderately inflated) benchmarking rws/effin time 29.71 μs (29.62 μs .. 29.81 μs) 1.000 R² (1.000 R² .. 1.000 R²) mean 29.74 μs (29.67 μs .. 29.83 μs) std dev 260.9 ns (208.8 ns .. 328.2 ns) benchmarking rws/freer-effects time 19.64 μs (19.59 μs .. 19.69 μs) 1.000 R² (1.000 R² .. 1.000 R²) mean 19.63 μs (19.58 μs .. 19.69 μs) std dev 167.0 ns (133.9 ns .. 203.3 ns) Benchmark eff-comparison: FINISH Completed 3 action(s). ``` To try it as a replacement for mtl, import [Data.Extensible.Effect.Default](http://hackage.haskell.org/package/extensible-0.4.2/docs/Data-Extensible-Effect-Default.html) along with `Data.Extensible`. `runWriterDef`, `runStateDef`, `runReaderDef` are similar to the runners of monad transformers, but you need to apply `leaveEff :: Eff '[] a -> a` to extract the result. ```haskell runExtensible :: Eff '[ReaderDef Int, StateDef Int, WriterDef (Sum Int)] a -> ((a, Int), Sum Int) runExtensible = leaveEff . runWriterDef . flip runStateDef 0 . flip runReaderDef 1 ``` If you seek for faster extensible effects, this definitely is an option. Under the hood ---- The definition of `Skeleton` used to be ```haskell newtype Skeleton t a = Skeleton { unSkeleton :: Spine t (Skeleton t) a } ``` where ```haskell data Spine t m a where Spine :: MonadView t m a -> Cat (Kleisli m) a b -> Spine t m b data Cat k a b where Empty :: Cat k a a Leaf :: k a b -> Cat k a b Tree :: Cat k a b -> Cat k b c -> Cat k a c ``` The new representation is somewhat simpler, and quite similar to what [freer-effects](http://hackage.haskell.org/package/freer-effects-0.3.0.1/docs/src/Control-Monad-Freer-Internal.html#Eff) has: ```haskell data Skeleton t a where ReturnS :: a -> Skeleton t a BindS :: t a -> Cat (Kleisli (Skeleton t)) a b -> Skeleton t b data Cat k a b where Leaf :: k a b -> Cat k a b Tree :: Cat k a b -> Cat k b c -> Cat k a c ``` I'm guessing removing `Empty` reduced the number of case analyses `debone` does.