6 ms·
You should generally be writing code against typeclasses, not a particular monad transformer stack. For example: fibonacci :: MonadState (Int, Int, Int) m
by consilient 3y ago
You should generally be writing code against typeclasses, not a particular monad transformer stack. For example:
fibonacci :: MonadState (Int, Int, Int) m => m Int
fibonacci = do
(prev, prev2, n) <- get
if n > 0 then
put (prev + prev2, prev, n - 1) >> fibonacci
else
return prev2
concreteFib :: ReaderT String (StateT (Int, Int, Int) (ExceptT String Identity)) Int
concreteFib = fibonacci
- rowanG077 3y agoYou misunderstand my problem. Add a logger to that fibonacci function. Potentially EVERY usage site now has to change, maybe even multiple layers. Adding a log in most languages is a local transformation. In Haskell it isn't, it can have codebase wide consequences.
- consilient 3y agoIf you just want to add logging to existing operations, reinterpret them at the call site. Something like this newtype LoggedStateT s m a = LoggedStateT (WriterT s (StateT s m) a) instance (Monoid s, Monad m) => MonadState s (LoggedStateT s m) where get = LoggedStateT $ do val <- lift get tell val return val put s = LoggedStateT $ tell s >> lift (put s) (which is basically an ad hoc effect system) If on the other hand you want to reproduce the behavior of other languages, throw everything in `MyAppMonad` give it whatever capabilities you need.
- rowanG077 3y agoWhich requires sweeping changes... In most other languages it's literally a one liner where you want to log something.
- consilient 3y agoIt requires changing the places where you instantiate your monad transformer stack, which you should have very few of.
- rowanG077 3y agoI don't think having very few is a good scenario. I have written a compiler and had about 10 different stacks. Changing every single one just to be able to add a logger to a single function somewhere is honestly insane. What I see in the wild is having one huge kitchen sink stack which sucks as well.
- doyougnu 3y agoDoes every usage site have to change? You would alter fibonacci to be: fibonacci :: (MonadLogger m, MonadState (Int, Int, Int) m) => m Int fibonacci ... and now of course all callers must support MonadLogger. But instead of using the MonadLogger (or any mtl constraint directly) you should just be constructing an abstraction boundary with a type class synonym: class (MonadLogger m, MonadState s m) => MyMonads s m and now you change fibonacci: fibonacci :: MyMonads (Int, Int, Int) m => m Int fibonacci ... And now if you need to add a monad or add Eq or whatever you just have to change your type class synonym rather than every function. Its not a problem with the language its just programing with modularity in mind, even in the type system.
- rowanG077 3y agoI have seen this in the wild. The result often is that every function has a kitchen sink MyMonads constraint of which it only uses a tiny subset. It's death by a thousand cuts. If you make such a class for every monad combination you get insanely large amount of classes. It's simply unworkable. Which is why you get the kitchen sink monad pattern.
- kccqzy 3y agoAnd what's wrong with the kitchen sink monad pattern? I've certainly used exactly that. And I have no problems with it.
- rowanG077 3y agoBecause your code is very much overconstrained at that point. For the same reason you don't add a `Num a` constraint to list `head` function. You have now essentially fused your function to your codebase.
- kccqzy 3y agoThat's not a problem in business logic heavy code. Requirements change and you could use previously unnecessary constraints at any time.
- icrbow 3y agoDebug.Trace has a lots of stuff for that. You can even generate charts from that when using eventlog-enabled runtime.
- jaspervdj 3y agoIf you wanted to log from fibonacci, you would pass a some logger instance down to this function. In Haskell, this could be a record or a typeclass instance. In other languages, it could be an object or a struct. There is no fundamental difference. All the layers above would still have to pass this through; explicitly or implicitly.
- rowanG077 3y agoThe difference is most language have global state. See rust for example. It's not common to inject a logger, a "world" object for IO or anything for which you need monads in Haskell at all. You are arguing from a concrete technical standpoint: "But you need to do the same thing in other language if you want to mirror Haskell monads". Sure, you are right. That also completely glosses over the point I'm making. I simply feel like the way haskell does it is unergonomic, it would also be unergonomic to something equivalent in other languages. I don't know what a good solution would be, maybe a constrained partial type signature? Let the compiler pick the smallest constraint from a larger space of available constraints that fits with the usage and simply let the type checker bubble it up until you actually care to specify it? GHC doesn't support this but it should be possible in theory.
- runeks 3y agoThat's because most languages allow arbitrary IO in all functions. You can achieve this in Haskell too if you want, by just using IO as your monad everywhere. But most people don't do that because then it becomes really hard to reason about your code, so spending the extra time propagating a MonadLog constraint up your stack is actually worth it.