What I'm not liking about this line of reasoning is that in practice Haskell seems to not go far enough[1].
Yes, you have to declare your effects. In practice that means that most of your code returns IO, and isn't constrained anymore. I don't know if this is a library feature, or an essential feature of the language[2], but it would be very interesting for example to put a GUI together by computing events in functions that returned an "Event" monad, widgets in functions that returned a "GUI" monad, database access in functions in a "DB" monad, etc. Instead, all of those operate on IO.
[1] A completely subjective assessment.
[2] I've though for a short while on how to code that, but didn't got any idea I liked.
It's not clear that that's the distinction that makes the most sense. I think typing based on capability makes more sense than typing based on purpose. So you would have a type like FSRead and FSWrite which could only read and write from the filesystem, for example. (Ideally with a nice way to combine the two!)
Of course, you can do this as a library. In fact, this is an example use case[1] for Safe Haskell which also prevents people from circumventing your types with unsafePerformIO and friends.
Moreover, some existing libraries already take similar approaches. FRP libraries extract reactive systems (like events but also continuously changing signals) into their own types. A button gives you a stream of type Event () rather than an explicit callback system using the IO type. Check out reactive-banana[2] (my favorite FRP library from the current crop) for a nice example.
Similarly, people use custom monads to ensure things get initialized correctly, which has a similar effect to what you're talking about. The Haskell DevIL bindings for Repa[3] come to mind because they have an IL type which lets you load images and ensures the image loader is initialized correctly exactly once.
Sure, in the end, everything will need to be threaded through IO and main to actually run, but you can—and people do—make your intermediate APIs safer by creating additional distinctions between effects.
I think there's quite a bit more of this going on than you seem to be aware of. In addition to the things mentioned in a few siblings to this comment, there is STM for software transactional memory, several different DB monads provided by persistent, Query and Update in atomic-state, Hander and Widget in yesod, Sh in shelly... In my experience, very little of my code runs in an unadorned IO monad. Some of these have holes punched in them with liftIO to let me run arbitrary IO - whether that's appropriate depends on the particular context.
I'm currently working on a UI library similar to what you describe. When you've got a situation where "everything just ends up in IO" you probably just have an early design iteration on that space of possible libraries.
Yes, you have to declare your effects. In practice that means that most of your code returns IO, and isn't constrained anymore. I don't know if this is a library feature, or an essential feature of the language[2], but it would be very interesting for example to put a GUI together by computing events in functions that returned an "Event" monad, widgets in functions that returned a "GUI" monad, database access in functions in a "DB" monad, etc. Instead, all of those operate on IO.
[1] A completely subjective assessment. [2] I've though for a short while on how to code that, but didn't got any idea I liked.