3 ms·
Pretending that modern type systems provide anything near an adequate control of effects is misleading at best. Controlled side effects are an excellent goal I
by Chris_Newton 11y ago
Pretending that modern type systems provide anything near an adequate control of effects is misleading at best. Controlled side effects are an excellent goal
I completely agree. But, as I suspect you’d agree, it’s not an easy problem even to specify the behaviour we would ideally like to see here.
For example, one of the important things about side effects is not only their existence or lack thereof, but their ordering (e.g. you must write to a socket only after you open it and only before you close it).
When I’m designing software, I tend to think of effects at three distinct levels: can effects happen at all, what relative timing (i.e., ordering) do they need to have, and what absolute timing do they need to have?
The most basic question is whether certain effects or effects related to a certain resource happen at all. This includes distinguishing pure functions from code with side-effects, but it could also be about, say, having a function that appears pure from the outside but is implemented using mutable state internally. If that state is stored in internal memory that is only acquired and released within that function and no reference to it is accessible outside the function, then the scope of the effects is limited and the usual advantages of pure functions still apply viewed from outside. If the state is stored externally, say in a file or database, then potentially it could interact with other parts of the system because they could also acquire a reference to it via another channel, so we might want to know about that.
My current thinking is that in terms of modelling the program behaviour we want to see, the most important thing is probably whether any given resource is transient/unique vs persistent/shared, not whether it's internal (memory) or external. For example, we might also want to model a temporary file with a unique OS-provided name or a temporary database table as being completely private state for programming logic purposes. We still need mechanisms to recover from errors by, say, asking the OS to delete that temporary file if we abort with an exception, but then for true robustness we need to deal with things like failing to allocate dynamic memory as well, and in any case these are ultimately still effects that can be confined to the area of the program where those resources are used without interfering elsewhere. I’d like to have a language where interfaces could indicate how any persistent/shared resources might be affected and pass the references identifying those resources around explicitly so we can’t do things like accidentally duplicating a file handle and having two threads concurrently writing to it without proper synchronisation. I’d also like to have a language where implementations could work with private resources internally without needing to do anything special to protect other parts of the code, but with errors being generated if anything could leak a reference to a private resource that might result in unwanted interactions.
We can also be interested in the relative timing of when effects happen, i.e., the possible orders in which multiple effects happen. I reckon this usually only matters to the extent that the same resource(s) are affected; two sequences of effects that relate to completely independent resources can probably be interspersed arbitrarily without any real harm being done. There are a lot of practical error classes that fall into this category, and this is where I’m with you in not being entirely convinced about the monadic model. If I’m working with two files, I probably care about opening, then reading/writing, and then closing each of them in the correct order, but not about whether I closed the first before I opened the second. If we start needing some sort of IO monad to capture each sequence and then stacking them up, this could easily become a chore. It’s a little like the problem of using too much hierarchy in OO design: you wind up codifying some arbitrary nesting of the things you care about, but the hierarchy is illusory and navigating it becomes a burden when the nesting isn’t convenient (cf. 27,534 variants of lifting in Haskell).
I suspect more effective tools will eventually come from incorporating effects and the resources they affect as true first class citizens in programming languages. I’m intrigued by the kind of ideas we see in substructural type systems, and wonder whether a system built on similar principles might let us constrain the things that actually matter safely but without imposing all kinds of arbitrary prioritisation and/or boilerplate code that is more trouble than it’s worth. Some of the discussions during the development of Rust in recent years have been interesting in this regard[1] as have some of the discussions about the monadic model in Haskell and its limitations.
As a final comment, I think the other downside of the pure functions and monads model is that it tends to put all the emphasis on relative ordering. We might also care about absolute timing of effects if we’re interacting with a real time clock or other physical system. If the only way to get that information into your model is to shoehorn in another level of monad, again with the complications of stacking transformers possibly in arbitrary orders, this isn’t ideal. If a clock were just one more stateful resource we have access to in certain parts of our program, much as it would be in typical imperative languages today but with the improved effect/resource usage safeguards, that would be helpful.
[1] For example, see http://pcwalton.github.io/blog/2012/12/26/typestate-is-dead/ http://pcwalton.github.io/blog/2012/12/26/typestate-is-dead/