8 ms·
The Case for Controlled Side Effects
- wheaties 11y agoThat is a symptom of mutable code. In that case, side-effects are nearly a red herring because if the object were immutable, then you would really only ever have to check once.
- overgard 11y agoI totally agree with all of this, but I think the hard part of writing side effect free code is that it often involves more allocations. (Since you're constantly constructing new values and objects, and transferring them around on the stack). I think this is why you see a lot more mutation (and bugs) in C: managing allocations is annoying. Having a garbage collected language mitigates the burden on the programmer, but if you're writing performance sensitive code that means you're putting pressure on the garbage collector so you're going to get more pauses. Not to say you shouldn't write in this style, you should, but there's a pretty big opportunity for languages to get better in this regard.
- cr__ 11y agoSome of the languages have gotten better! Clojure, for example, has smart data structures that share elements to speed up operations and minimize copying: http://hypirion.com/musings/understanding-persistent-vector-pt-1 http://hypirion.com/musings/understanding-persistent-vector-...
- lectrick 11y ago> I think the hard part of writing side effect free code is that it often involves more allocations Are you saying this in spite of the existing implementation of decent persistent data structures, at least in some languages? https://en.wikipedia.org/wiki/Persistent_data_structure https://en.wikipedia.org/wiki/Persistent_data_structure I think the cost in more memory (and with persistent data structures, only the difference in memory of the change) is more than made up for by fewer bugs, easier concurrency, and a host of other benefits. In the light of "mutability always produces more bugs," mutation should be seen as an optimization, after tests are written which cover behavior and side effects.
- tomp 11y ago> Are you saying this in spite of the existing implementation of decent persistent data structures, at least in some languages? No, s/he's saying that because of the existing implementations of decent persistent data structures. Practically all of them allocate way more than mutable data structures - e.g. trie-based vectors/arrays, trie-base hashmaps, tree-based ordered maps, etc. Essentially each new addition, modification or removal results in one or more allocations.
- sanderjd 11y agoAlso, those allocations imply linked, rather than contiguous, data structures, which (often? always?) imply poor cache interaction, which is a major source of inefficiency. Would love some more perspectives on this, as I've just watched a couple cppcon talks ([0], [1]) and I'm feeling a bit drunk on the kool-aid. [0]: https://www.youtube.com/watch?v=fHNmRkzxHWs https://www.youtube.com/watch?v=fHNmRkzxHWs [1]: https://www.youtube.com/watch?v=rX0ItVEVjHc https://www.youtube.com/watch?v=rX0ItVEVjHc
- eldude 11y agoI agree with the need/value of side-effect/pure annotations. From a debugging perspective though, there are 2 alternative solutions: Aspect Oriented Programming (i.e., mutation observations) and Immutable data types (e.g., Object freezing) to cause an exception on mutation. Depending on the language, you can replace the value with a watcher or proxy and add a break point to know if it was mutated between those lines. Alternatively, you could freeze the object or property or make it immutable between those lines to cause an exception to be thrown. Both will require triggering the case in question live so annotations still superior, especially compared to manually reading code.
- joeblubaugh 11y agoI didn't see this addressed in the article: Do these annotations have force? I've run into plenty of annotations that lie - how can I trust that code that's annotated as being side-effect-free actually is?
- kqr 11y ago> Not only do these type systems allow you to annotate things as "doing side effects"; they specifically disallow doing side effects unless you have annotated the thing as such. Just below the "Controlled Side Effects" heading.
- lmm 11y agoAny practical language will have escape hatches, but it's the equivalent of doing a cast (indeed it sometimes literally is doing a cast) - it sticks out in code review, compilers have options to warn or ban them altogether.
- adrusi 11y agoMaybe the author, presumably well versed in type systems, assumed that the use of effect typing was implied. If you're not familiar go check it out, but I'll give a synopsis. If you're familiar with `unsafe' in Rust, it's a specific case of effect typing. You can annotate a function with an effect (useful examples: unsafe, impure, throws exceptions) and then if another function calls that function, then it needs to also be annotated with that effect, or else the compiler emits and error. Usually there will also be an escape hatch where you can assert to the compiler that you know, in a way that it can't statically prove, that a certain call to, say, an impure function, still doesn't have side effects. Code that appears within these escape hatches needs to be heavily tested in order for effect typing to be useful. For the above examples of effect types, the compiler will also consider certain non-function-call constructs to have an effect annotation. Dereferencing an arbitrary integer as a pointer, for example, would effectively have an "unsafe" annotation. I believe Nim has a generalized implementation of effect typing.
- michaelochurch 11y agoWelcome to Haskell. No, seriously. We allow side effects. We have the STM monad, the ST (single-threaded) monad, and the IO monad if you really want to go wild. And almost no one seriously argues that the IO monad is "dirty" and should be avoided. In fact, the type signature of a top-level executable's main is IO () by mandate. I want to make a proposal, which is that we ban the term "side effect". It doesn't mean what people think it does. For example, the print function puts output on the console but that's not a "side effect"; that's the main effect. I would argue also that non-detectable stateful effects (e.g. building up a large dictionary mutably) shouldn't be called "side effects" (although they are stateful effects) if they're purely implementation details that are hidden behind an interface. "Side effect", I think, is a case of a term that used to resemble either a design flaw or a non-intuitive behavior (i.e. reading your command-line arguments deletes the strings, which is something that I encountered in one system) that is tolerated for performance's sake; but that has now been expanded to include stateful main (i.e. desired) effects as well.
- tome 11y ago"Side effect" means something that happens implicitly as a result of evaluation. There's no need to ban the term; we just need to keep in mind how it differs from the more general notion of "effect".
- deleted 11y ago[deleted]
- pron 11y agoPretending that modern type systems provide anything near an adequate control of effects is misleading at best. Controlled side effects are an excellent goal; the way Haskell (or the pure-FP approach) goes about achieving that goal not only falls short of the target (which is OK) but may be going off in the wrong direction altogether. 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 after you open it and before you close it). While enforcing ordering is possible with PFP type systems (to a certain extent), it gets very complicated very fast. On the other hand, other ways of reasoning about effects and their ordering -- e.g. by debugging and profiling -- are made harder, rather than easier, by the PFP approach. Personally, I believe a better approach is imperative-functional, namely inline (i.e. non-monadic) side-effects, but made explicit with good effect systems. Sadly, we're not yet at a point where this is possible and easy to do. I strongly believe monads are not a good way of representing side effects, and to understand why you only need to look at how hard it is to compose monads and how difficult to follow are the ways of composing them (monad transformers).
- Chris_Newton 11y agoPretending 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/
- ericfrederich 11y agoHe mentions Python, is there a solution to this in Python? Perhaps Python 3.5 with optional type checking annotations?
- patsplat 11y agoWonder how many klocs are underneath that `profile.email.interactions()` method?
- goldbug71 11y agoIt doesn't matter because you don't have to maintain it.
- ak4g 11y ago> I don't like wasting my time on things my computer could do for me. So don't. Let the interpreter check profile.verified_email twice (horror of horrors!) and get back to your day job. Even if there are no in-program side effects, removing a 2nd check for verified_email immediately before preparing and sending the email introduces a race condition: the user's email is verified, your first check runs, then you go talking to the DB and who knows what else for the last_interaction fetch/business logic, meanwhile, user changes their email address, and now profile.verified_email is false. By skipping your 2nd if check, you send your email to an address that is still pending verification. This assumes that .verified_email is a property that talks to the DB, and while it'd be better style in that case to make it an .is_verified_email() method to make that clear, doing so is a plausible and not-incorrect use of properties. It's not worth your (employer's) time to even check if that's the case. Leave the non-broken code as-is and return to your regular programming.
- radicalbyte 11y agoAdd a comment so the next person reading the code can understand why it's built that way.
- cwp 11y agoI was waiting for the paragraph that read "I'm not talking about pure functional languages because X, nor am I talking about Rust, because Y." So, yeah, uncontrolled side-effect are a PITA. Solutions exist.
- adrusi 11y agoThe D programming language has compiler support for this. Unfortunately due to legacy concerns it's opt-in, which makes it much less useful, and the designers have said that they regret not adding it to the language earlier when it could have been mandated. It has the `pure' keyword, which can be used to annotate functions that don't have any side-effects other than mutating its arguments. If you want to disallow mutating arguments, you can just const them up. In the article's example, you could probably eliminate a lot digging through code by only looking at the functions without a `pure' annotation and where `profile.verified_email' is accessible in a non-const argument. The D designers call the property that the `pure' annotation enforces "weak purity" and I think it's a really useful notion for managing side effects in imperative languages. In C, for example, it just means that you can't mutate global (or local static) variables, or mutate anything through them (no mutating the value pointed at by a global). Shame it didn't make it into Rust. I guess the rationale was that it made lambdas too complicated and didn't reall come with much benefit (there aren't many more ways to violate weak purity in Rust than in C, and Rust actually forbids mutable globals anyway).