I wrote a tiny post showing a low cost way to escape “always being in the IO / Effect monad” in Haskell & PureScript.
I think it can be useful to new users and prospective users and somehow even to people who despair in production Haskell code bases over having every function returning IO a.
That’s a really well-written blog post! I feel this is an important program design topic, and one which the PureScript language punts to user-land without any advisement. I’m happy to see it discussed here!
I saw Matt Parsons updated an older blog post of his on this topic. I found it a bit hard to understand his current thinking, so if anyone wants to digest it and write it as a blog post, I would read it! I think he landed at “capability-sets as records are a good idea, and having them be implicit parameters to functions using type classes is a good idea.”, so correct me if that’s wrong.
At the very least I succeeded in being brief, which was a goal. I’m worried that it’ll appeal only to people who already know about this, but I decided to not try to talk down to the reader too much.
It’s becoming clear to me from engaging more in the Twitter community that there’s a lot of despair going around regarding this very topic and since you can hit both languages for free in one go I decided to also write it with PureScript in mind.
As a point in the design space, I think this transfers beautifully into one of the most common patterns of ReaderT env m a as long as one is willing to say “Yes, we can use mutable variables of different kinds (TVar, MVar, IORef, etc.) and limit their exposure via ad-hoc constraints”. The real value here is being able to be 100% precise in signalling what you’re doing, not in never using mutable state or side-effects, etc.
That’s potentially a follow-up post (and could’ve been covered in this one had I not wanted to keep it brief) but I’ve yet to be able to package it up as neatly as this one.
I sort of get it, which I think means I don’t really get it. If anyone has a neat example that concretely shows exactly what we’re solving and why I’d also be really grateful. ReaderT + constraints on state variables + effect functions seems a very low bar to limbo under and I’m not sure I even get the value proposition.
Relevant: More discussion about Matt’s updated post on Twitter:
Looks like the reason he updated his design recommendation is that he thinks using type classes for this is too complicated:
From his blog post:
Instead (of type classes), we will create a record-of-functions for the type class, and create two values:
data Http m = Http
{ get :: Url -> m ByteString
, post :: forall a. ToJSON a => Url -> a -> m ByteString
}
prodHttp :: Http IO
prodHttp = Http
{ get = fmap (view responseBody) . Wreq.get
, post = \url -> fmap (view responseBody) . Wreq.post url . toJson
}
mockHttp :: IORef (Map String ByteString) -> Http IO
mockHttp env = Http
{ get = \url -> do
state <- readIORef env
pure (Map.lookup url state)
, post = \url body -> do
state <- liftIO (readIORef env)
liftIO (writeIORef env (Map.insert url (encode body) state))
pure "200 OK"
}
and we include the record of functions directly into Services :
data Services eff = Services { http :: Http eff }
type Application eff = ReaderT (Services eff) eff
This gives us the same expressive power without having to deal with type classes, instances, and any of that other hassle. You construct plain values and pass them around.