The most apparent innovation noticed by people new to Haskell is that there is a separation between the impure world that is concerned with communicating with the outside world, and the pure world of computation and algorithms. A frequent beginner question is "How can I get rid of IO, i.e., convert IO a into a?" The way to to it is to use monads (or other abstractions) to write code that performs IO and chains effects. This code gathers data from the outside world, creates a model of it, does some computation, possibly by employing pure code, and outputs the result.
As far as the above model is concerned, I don't see anything terribly wrong with manipulating GUIs in the IO monad. The largest problem that arises from this style is that modules are not composable anymore, i.e., I lose most of my knowledge about the global execution order of statements in my program. To recover it, I have to apply similar reasoning as in concurrent, imperative GUI code. Meanwhile, for impure, non-GUI code the execution order is obvious because of the definition of the IO monad's >== operator (at least as long as there is only one thread). For pure code, it doesn't matter at all, except in corner cases to increase performance or to avoid evaluations resulting in ⊥.
控制台IO和图形化IO之间最大的哲学区别在于,实现前者的程序通常是用同步风格编写的。这是可能的,因为(撇开信号和其他打开的文件描述符不谈)只有一个事件源:通常称为stdin的字节流。gui本质上是异步的,必须对键盘事件和鼠标点击做出反应。
A popular philosophy of doing asynchronous IO in a functional way is called Functional Reactive Programming (FRP). It got a lot of traction recently in impure, non-functional languages thanks to libraries such as ReactiveX, and frameworks such as Elm. In a nutshell, it's like viewing GUI elements and other things (such as files, clocks, alarms, keyboard, mouse) as event sources, called "observables", that emit streams of events. These events are combined using familiar operators such as map, foldl, zip, filter, concat, join, etc., to produce new streams. This is useful because the program state itself can be seen as scanl . map reactToEvents $ zipN <eventStreams> of the program, where N is equal to the number of observables ever considered by the program.
Working with FRP observables makes it possible to recover composability because events in a stream are ordered in time. The reason is that the event stream abstraction makes it possible to view all observables as black boxes. Ultimately, combining event streams using operators gives back some local ordering on execution. This forces me to be much more honest about which invariants my program actually relies on, similar to the way that all functions in Haskell have to be referentially transparent: if I want to pull data from another part of my program, I have to be explicit ad declare an appropriate type for my functions. (The IO monad, being a Domain-Specific language for writing impure code, effectively circumvents this)