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.
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.
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.For example, It's fine in C to manually allocate/free memory, it's the way you have to write C. It's not fine to do the same thing in Rust. Even though you of course could do that in Rust as well.
That precisely why I think this is a great shortcoming of the language.
The strength of Haskell is that it allows you to achieve the first goal if you want. Most languages don't (pretty much no other language, actually).
head :: [Int] -> Int
But you can also just leave out the type signature all together to let the type
checker figure out the most generic type. You can have your cake and eat it too.You can even almost do what I want with partial type signatures. Just sprinkle it everywhere inside your constraints. GHC will automatically pick the right constraints. At the call site you actually care for the definition you can not use the partial type signature. The great disadvantage of this is that you now introduce ANY constraints into your type signature and you lose your types as documentation.
But that doesn't have to be
You could have a constraint with something like `UseMonadSubset (...)` which works almost like partial type signature. GHC should infer 0 or more of the monads insde `UseMonadSubset` as the actuall constraint. `
Then you could write something like:
fibonacci :: UseMyMonadSubset m => m Int
fibonacci = -- Uses only MonadState (Int, Int, Int)
-- Type checks because type checker can see fibonacci ONLY uses MonadState
foo :: MonadState (Int, Int, Int) m => Int
foo = fibonacci
bar :: UseMonadSubset m => Int
bar = fibonacci
Which allows for precise specification if you want to and if you don't you let the
type checker figure it out. You may even be able to implement this as GHC type
checker plugin. 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.
What I see in the wild is having one huge kitchen sink stack which sucks as well.