@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
.
I don’t use effect system and I’m not planning too. I’m happy with yesod like Handler.
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 a
s 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.
Just a thought:
Is effect system qualitatively more principled than IO monad?
If so, having both existing as a way to tackle RealWorld effects may be an analogue to unprincipled FP language versus Haskell purity approach.
Is it a crazy idea, some day, to tag library packages IO monad free in order to give principled effect approach some preferences?
I like mtl-style for published libraries, and plain IO with a reader for apps.
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.
You are awesome!
@arybczak’s set of benchmarks mentioned in another post is also worth looking at - note the entries for:
-
countdown.1000.reference (ST)
- 6.55 microseconds
and:
-
countdown.1000.cleff (IORef).shallow
- 25.6 microseconds -
countdown.1000.cleff (IORef).deep
- 25.9 microseconds
If IORef
s 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.
I quickly realized that my musing was a moot, since we have already been doing effect systems without IO pollution. That is mtl for long time.
But mtl vs. effects seem a dividing point for the ecosystem at some point
But division might not be a bad thing if we have enough energy in the ecosystem.
E.g. the Js/Ts ecosystem have multiple active front-end back-end frameworks at any given time.
It depends on what is required:
-
If you need the effects to be nested in some precise way, you can use monad transformers to build up the desired monadic type “layer by layer”.
-
If that level of detail isn’t needed, then you would probably opt for an effect system.
But this is just a heuristic - in reality, you would use what’s more appropriate for the current situation.
I second all what you’ve been saying there.
The only comment I would add is that, while I certainly enjoy and long for the “I make my own choice” culture in Haskell myself, I also would advocate for more visible and opinionated approaches by leaders. This way, people who are new to the community or prioritizing following over making own choices could find a way forward.
The whole ReaderT Design Pattern is a great example. We could continue to argue about which effect system is better. But someone needs to ultimately show the way.
And back to OP, I see a call for visible guidance in the community. There are lots of people are silent, and if they don’t see clarity, they leave and we will never see them again.
To me anyway, the current proliferation of effect systems is much like the current proliferation of FRP/DCTP systems - what’s needed for both of them is that unifying insight (the proverbial “Aha! moment” ) upon which they (to varying extents) can all be based on. Of course, if those insights could lead to the formation of single “grand unified” systems for effects and FRP, that would be even better…
Heh, I do love the learning experience listening to what Haskell Masters are talking about.
I am just conscious of the potential “leaving the new comer confused” scenario; while the masters are still figuring out things (which will be forever anyways…) I’d like to see more people using Haskell, but not wanting us to oversell things that are not ready neither. Not sure where the balance is, but I guess this is what this discourse is for.