In this post I start to explore whether it could make sense to work with categories instead of monads in Haskell.
More details (but way less commentary) can be found at marcosh/category-transformers: Category transformers - Codeberg.org
In this post I start to explore whether it could make sense to work with categories instead of monads in Haskell.
More details (but way less commentary) can be found at marcosh/category-transformers: Category transformers - Codeberg.org
I’m no category theorist but I enjoyed this elaboration on Chris Penner’s excellent posts. It’s cool to see where we can go further.
There are a couple of typos jsyk; you start using ProKleisliT when the type is initially defined as ProKleisli.
The end types are a little unwieldy, but I do note that they should stack fairly easily; I wonder whether an Effectful-style type level list would let you abstract that horrid stacking. I also like that you can put IO anywhere in the stack, which just seems nice and symmetrical.
Do the effects of “later” computations get skipped if you have an earlier failing computation? For example, in the transformer example you ask for the environment variable then the user input which you subsequently parse; if you asked for user input, then parsed, then asked for the environment variable, would that last computation not be run?
Thanks for mentioning the typo, I just fixed it.
In the example, the two Int are actually read in parallel (notice the usage of &&&), so they will both be run independently of the result of the other. If you compose them sequentially, then yes, the second will be skipped if the first one fails, as it happens with monad transformers
Concisely: Many monad transformers are based on CatFunctors on Kleisli categories.
Look at the type of Kleisli . mapM . runKleisli.
Should we stop using monads?
For a long time I believed that “lambda calculus enriched with effect E” would have semantics in the Kleisli category of E. But that can’t be true, since models of lambda calculus are cartesian closed categories and Kleisli categories are almost never cartesian closed. This hints at why Kleisli arrows might not be an ergonomic way to program. Instead, theoreticians painstakingly examine questions like “Is the cartesian closed sub-category C of D also closed under the monad E: D → D?”
I once tried using arrows for a model that used the RWST monad as effect, hoping that arrows would make documentation of data flow neater. But it turned out that do-notation is far more ergonomic in Haskell. For example, you can sprinkle let-bindigs and shared variables anywhere, have multi-parameter monadic functions, versus tedious funneling of data with (&&&) and (***).
Counter-counterpoint: why are monad cakes even viewed as a good foundation?
The example in this article could be naively written as
isEnvGreaterThanInput :: Handle -> Int -> IO (Maybe Bool)
isEnvGreaterThanInput handle env = do
input <- hGetLine handle
case readMaybe input of
Nothing -> pure Nothing
Just n -> pure $ Just (env > n)
It’s shorter than the other two versions, it doesn’t do magic (can’t read from a Handle if it was never passed to you), and it trivially composes with any other plain IO function.
Agreed. Lambda calculus is a foundation that is hard to surpass in beauty and simplicity. But why have monads been so successful compared to profunctors and arrows? Certainly not only because of clever marketing.
Monad stacks are successful because they allow overloading IO with magical properties, chiefly global contexts and early exit. These are preferred over argument passing and pattern matching respectively, removing both the logarithmic avalanche of upstream adjustments caused by function type changes and the annoying rightward code drift. This style of programming apes imperative languages, so it’s already familiar to most newcomers, thus job-friendly, and jobs feed back into the ecosystem.
The goal of arrows seems to be to specify computations more granularly, but that’s not a common concern, especially among those using monad stacks.