Monadic interfaces in the context of non-deterministic effects are a consolation prize. They represent a way to combine effectful code, but ideally your code would have almost no effects at all.
As far as I can tell, the idealized version of this talk is a batch interface: one effect to grab all the data you need, transform the data, and then one effect to "flush" the transformed data (where flush could mean to persist the data in a database, send it out as commands to control a robot, etc).
Tracking side effects in your types (maybe what you were going for?) is helpful for measuring to what degree your code fails to adhere to this idealized model. If most of your code has an effect type, that's probably a sign to refactor. It also keeps you honest as to the infectious nature of effectful code by propagating the type as necessary.
Then look at Elm code. Elm does not have any imperative hatches. The monad that runs everything is at the very top level (“shell” as the article calls it) and hidden.
As such Elm code is forced to use functional decomposition resulting in very easy to follow, refactor and maintain designs.
If you're working with a free monad, or if you don't specify IO (just some of the generic IO like typeclasses like say MonadError), you can still choose your own interpreter for the monad and "program" the semicolon. Which means you get back all the benefits of testability etc.
To get a similar effect in an imperative language, you would use e.g. coroutines and `yield` every side effect to the execution engine. The engine will take the action "specs" (probably a data structure describing the action to perform, e.g. set some value in memory) and decide what to do with them, and you can swap the real engine with a test/mock engine in your tests.
It is pity that modern conveniences like polimorphic record types with nice syntax for record updates were not invented earlier. With those even with complex code monads can be used only at the top level when the sugar of do blocks is not even necessarily.
If one looks at the desugared version one can see where the trouble comes. Functional code using monadic style depends on the state of the monad interpreter that can be arbitrary complex and spread over many closures with many interdependencies. It can be rather hard to uncover what exactly is going on, precisely in the same way as with imperative code it models.
Monads exist exactly because mutation is a reality. Monads do not defy the "mutation reality", nor try to encourage programmers to never look inside them. They are a means of dealing with the "mutation reality" by encouraging to separate pure and impure parts properly and while still making functional composition possible. The image you create for monads is a straw man. Monads ARE a kind of "disciplined mutation" as you put it.
You don't have to like them nor prefer them. But they are clearly a great and established abstraction loved and used by many. You may prefer Clojure, I get it, but I see no reason to talk shit about monads in this way. Have you ever used monads and similar abstractions extensively?
> Monads is taking it too far.
> Mathematical purity of programs is a myth propagated by Type theorists dont buy into it.
Those are big words. Are you some kind of authority? You could have at least prepended "I think" to those phrases.
> Those are big words. Are you some kind of authority?
Yrs of writing programs have taught me that programming functions are not equivalent to mathematical functions, there is no equivalence that exist stop pretending that it does.
Monads exist independently from Haskell and are not about "things that change".
You didn't respond to anything they said and you doubled down with your nonsense about "Monads exist because because haskell people want to pretend that there is this ideal mathematical world where things dont change."
That monads ignore the "mutation reality" isn't a very strong point when monads are a concession for the "mutation reality." Unless you want to repeat yourself a third time, the ball is in your court to bring concrete supporting arguments since you're making the extreme and somewhat self-aggrandizing claim that these other people don't really see the mutation reality of the world like you do, thus they are using inferior tools.
I'd say that anyone specifically trying to corral/isolate their I/O code (monads or not) are so "enlightened" about the mutation reality of the world that they use specific abstractions to address it.
If you want to see code that tries to paper over I/O, look at a program where you can't even tell when and where the I/O is performed because it just looks like any other function call. Active Record in Ruby on Rails might be a good candidate in its effort to make DB access opaque to the programmer.
Large projects inevitably benefit from static guarantees enforced automatically by your environment. That can be a 3rd party static code analysis tool or the compiler. Even just a linter will improve code quality and thus developer happiness and productivity.[] Having your compiler enforce* the functional core/imperative shell, and exposing your business logic only through functional components is what makes a strongly typed language of the ML family stand out over, say Clojure.
Mutating state is no problem in a strongly typed functional language. In Haskell, just put your computation in an ST Monad. You can even still expose a functional signature that doesn't leak the ST monad if your algorithm is faster with mutation.
[*] Overall. Some people will probably be unhappier, because they have to follow "arbitrary" rules now, but those would usually have been the worst offenders.
Immutability also makes you code better but it's an orthogonal concern and utilising both is a smart move.
That works reasonably well in some situations, but not all.
We often work with local, temporary state, meaning something mutable that is only referenced within one function and only needs to be maintained through a single execution/evaluation of that function. (Naturally this extends to any children of that function, if the parent passes the state down.)
If that function happens to be at a high level in our design, this can feel like global state, but fundamentally it’s still local and temporary. I/O with external resources like database connections and files typically works the same way.
We can also have this with functions at a lower level in the design. An example would be using some local mutable storage for efficiency within a particular algorithm.
However, not all useful state is local and temporary in this sense. We can also have state that is only needed locally in some low-level function but must persist across calls to that function. A common example is caching the results of relatively expensive computations on custom data types that recur throughout a program. A related scenario is logging or other instrumentation, where the state may be shared by several functions but still only needed at low levels in our design.
Now we have a contradiction, because the persistence implies a longer lifetime for that state, which in turn naturally raises questions of initialisation and clean-up. We can always deal with this by elevating the state to some common ancestor function at a higher level, but now we have to pass the state down, which means it infects not just the ancestor but every intermediate function as well. While theoretically sound in a purely functional world, in practice this is a very ugly solution that undermines modularity and composability, increases connectedness and reduces cohesion. And weren’t those exactly the kinds of benefits we hoped to achieve from a functional style of programming?
If anyone would like to read more about this, we had an interesting discussion about these issues and how people are working around them in practice over on /r/haskell a couple of years ago:
https://www.reddit.com/r/haskell/comments/4srjcc/architectur...
Spoiler: We didn’t find any easy answers, and everyone is compromising somewhere.