Hi all. Some weeks ago I jumped onto the effectful ecosystem. Things are great! I like the emphasis on semantics between effects. Reinterpret is such a nice function and now I don’t need to scope runXXXT call on the code to get scoped effects. More detailed effects are easy to add.
So at least for code that uses IO, I don’t see the reason to use transformers anymore. What about monad transformers stacks that have identity at the bottom? That usually is pure code modelling some process: state machines, early termination, implicit state passing. There the two options are on a more even ground as exceptions are not present.
The real question is: if you use effect systems, do you still use transformers/MTL somewhere in your code? If so, where?
For architecture, yes. I don’t think I’d choose mtl over effectful/cleff for a new project (and I ripped mtl happily in my personal projects).
mtl isn’t going anywhere at my work codebase and I bet that’s common. mtl had the ubiquity and benefit of being in common books used for training. In a vacuum, all mtl production code I’ve seen would be better with effects though, but alas.
@eddiemundo Can you elaborate? I don’t understand this sentence I thought effectful worked fine with streamly as you should be able to just put e.g. SerialT on top of Eff es.
In my experience, existing code and libraries are the biggest barrier to going full effectful. I’ve been rewriting some code to effectful, and found that free monad code is the easiest to rewrite.
I haven’t tried rewriting MTL heavy code, but I got the feeling that it would be much more effort. I may be wrong, it may be that particular project that I was eyeballing for a rewrite, but I got the feeling that an effectful architecture would look way different than an MTL architecture.
In the software that has been rewritten to effectful, MTL still remains in use in libraries that are coupled to it. Persistent for example demands a ReaderT when executing queries, and conduits have the ConduitT and ResourceT.
What I then really like about effectful, though, is that you can often limit those MTLs to some effect, in such a way that it doesn’t feel awkward that both are present in the code base. When the transformer is a class, effectful even has a fancy guide for it.
With Persistent you can just make an effect constructor that runs a transaction. That transaction will be wrapped in that ReaderT, because all query functions demand it, but that’s only in the type signature. For all the caller cares, they’re still in Eff.
Here’s an example with sqlite, which demands not only a ReaderT, but also a logging and resource:
Here’s the call site, which has no mentions of any transformers:
The real question is: if you use effect systems, do you still use transformers/MTL somewhere in your code? If so, where?
In the code bases where I’ve introduced Effectful, no, there are no more 1st-party transformers/MTL classes. Thanks to Effectful’s compatibility with 3rd-party transformers, I can write effects for those quite easily.
Regarding IO, I think you do not usually need additional layers above that. It already provides states and exceptions, and you can pass environments as arguments.
IO provides states and exceptions, but also everything apart from these. Once you have IO/MonadIO in the stack, anything can happen. You forfeit any guarantees about what your program does.
One of the biggest selling points of Haskell is purity, and control over effects. If you have a function using constraints (MonadDatabase m, MonadLogging m) you know it doesn’t explicitly throw exceptions, or send HTTP requests. It can use some general database and logging, a specific implementation to be determined later. With all functions like a -> IO b, we are back to Python land.
I think I meant what you said. I never really understood what under, or on top, inside, or outside meant. When I said inside I was thinking of Stream (Eff es) a being a series of eff computations producing as so the Eff es is “inside” the stream (or list). I don’t think I said that didn’t work.
On the other hand I feel like it’s not possible to have the other way, a streamly-like effect, or at least weird/hard.
That example of SqliteEffect.hs is great! I was feeling off that the documentation for effectful tells you to provide canonical orphan instances if the library you want to use uses MTL style type classes. But with your example I can fix a concrete monad stack and use that inside an interpreter, as most of those classes are to be run on some kind of stack with IO at the bottom. No instances have to be provided.
If IORefs are the only reason why the IO type is being used, I would be interested in seeing if there’s much of a difference for STRef-based versions of countdown.1000.cleff - if that difference is trivial, the choice of using a pure, ST, or IORef-based approach would seem to be a matter of personal preference e.g. one isn’t happy with using something that has an implementation but no denotation (at least in Haskell).
Having said that, if this thread is any measure, it isn’t so much a choice between being effect or IO-based, but whether it’s monadic or ordinary (Haskell) syntax: over one-quarter century after it officially arrived in Haskell (v. 1.3), more that a few still find the monadic interface irritating to deal with…
I did a similar thing when I rewrote my game “engine” to use cleff, except for apecs. It’s a cool trick for interop with any transformer!
import Cleff
import Apecs qualified as Upstream
data ApecsE world :: Effect where
LiftApecs :: Upstream.SystemT world m a -> ApecsE world m a
runApecs :: IOE :> es => world -> Eff (ApecsE world : es) a -> Eff es a
runApecs w = interpret $ \case
LiftApecs st -> withToIO $ \toIO -> do
toIO $ Upstream.runSystem st w
makeEffect ''ApecsE
I also wrote a wrapper around every apecs combinator (cmap etc) that used Eff instead of SystemT. I did need an inference helper due to the polymorphism but it works like a dream!
Is effect system qualitatively more principled than IO monad?
Yes, of course, because of
As far as I’m concerned the main point of effect tracking in Haskell is that you know from the types when there are not effects in a piece of code. If you use IO everywhere you can’t tell that! You can never remove/handle an "IO effect".
…apart from the obvious case where the code isn’t monadic to begin with.
…if the code still uses IO directly - another option is to use a newtype declaration as the basis for a new abstract monadic type, one that only allows certain IO effects to be used.
But this assumes that all the allowed effects are all I/O-centric, which e.g. isn’t the case for encapsulated state like ST, so a combination of those types is needed. But not all monadic types (and their effects) can be combined directly. It is because of that inability that Haskell now has monad transformers and effect systems.