Microsoft engineer who worked on the Midori operating system and writes long posts on language design, concurrency, and error models.
Commenters praise the long, excellent posts on OS and language design, especially the Midori series, error model, and transactional memory retrospective.
> you end up needing to propagate a bunch of extra stuff everywhere.
Declaring a function as non-IO is a contract to your callers that you don't do IO. You don't need to do it. You can write everything in IO if you choose. You can also call into the wonderful ecosystem of libraries, because IO functions can call other IO functions, as well as non-IO functions.
There is only ever friction if you declare your function to be IO-free. If you declare your function to be IO-free, but call IO from inside it, it's a compile error because of course it is.
So why bother declaring anything IO-free? If you do arbitrary IO in a Parser, you can't back-track. If you do arbitrary IO in Parallel code, you invite race conditions. Transactions is my favourite example, though:
.NET [1]
> Disillusionment Part I: the I/O Problem
> It wasn’t long before we realized another sizeable, and more fundamental, challenge with unbounded transactions [...] What do we do with atomic blocks that do not simply consist of pure memory reads and writes? (In other words, the majority of blocks of code written today.) This was not just a pesky question of how to compile a piece of code, but rather struck right at the heart of the TM model.
Scala: [2]
> ScalaSTM does not have the goal of running arbitrary existing code, which is where most of their problems arose.
Java/Akka: [3]
> STM is considered as a failed experiment
Clojure: [4]
> Very simply the side-effects will happen again.…
It's good to see so much focus on errors. They are essential when trying to build resilient systems. But our approaches are still very immature.
First, to make it clear, this article appropriately points out that exceptions are still necessary and relevant. I disagree with some of the use-cases given, but it's important to recognize that exceptions should still exist in programming languages. Joe Duffy's article about Midori's error model [0] is in my opinion the best reference to actually understand the difference between exceptions and errors and why it's so important to get right. It's a very good article, and it has been posted here before; if you are interested in error handling it's a must read.
Now, about errors as values. Treating errors as values is practical, and in modern programming languages, relatively ergonomic. That said, we already have some other comments in the thread pointing out how sticking to just "errors as values" is often not enough (btw, the article uses Rust, not Go, but anyway...). And it's also important to clarify this: errors are such an integral part of our programs, and have so much to do with flow control, that I don't believe thinking about errors just as values is enough. Sure, they might be "just values" under the hood, but in all programming languages we see either optional results or syntax sugar to be able to handle errors more gracefully. And in most cases, we still feel it's not enough (or it's enough to be practical, but not to…
The brilliant thing about capability-based security is that it turns classic object-oriented programming patterns into security patterns.
Access control is implemented by encapsulating private members behind a restricted public interface.
Polymorphism allows you to add wrappers around an object implementing arbitrary security policies. E.g. want to revoke access to this object after some time? Add a wrapper where the methods forward to the real object until revoked, and then throw exceptions instead.
What's interesting is that these are patterns we are doing anyway, in real code, not for the purpose of security but for the purpose of correctness and maintainability. Because we were doing these things anyway, our programming languages provide really good tools for making them work naturally, and we already know how to think about them.
On the other hand, the competing model -- access control lists (ACLs), and other types of externally-specified policy -- are not things we were doing anyway. They're external concepts bolted into the system awkwardly. Because of this, they tend to be painful to maintain, which in turn means people often don't bother.
One of the clearest examples of how ACLs go against the grain: An ACL is a list of entities who are allowed to access a resource. These are effectively pointers pointing in the opposite direction of all other pointers in the system: ACLs point from the callee to the caller. This creates all kinds of problem. Just try to…
I haven't heard anyone anywhere say that "typed exceptions are an antipattern" just "typed exceptions _as implemented by Java_ are an antipattern". The general problem is well set forth by Anders Hejlsberg, Bill Venners and Bruce Eckel in _The Trouble With Checked Exceptions_ [1]. Briefly, typed exceptions in Java are not genericizable. Everything must be known at compile time. That is to say, you cannot write:
interface ThrowingRunnable<E extends Throwable> { void run() throws E; } Instead, the best you could do is manual multiplication of interfaces:
interface AThrowingRunnable { void run() throws ExceptionA; } interface BThrowingRunnable { void run() throws ExceptionB; } // ... snip thousands more ... Or give up and erase all the type information by saying `throws Exception` or `throws Throwable`.
Genericizing throws in particular was tried in Midori [2] and worked out really well (by report). In addition, several less-than-completely-obscure languages are starting to experiment with the algebra of effects in general (as opposed to error handling in particular). Pony [3], OCaml [4], and others are experimenting with bringing what Koka [5] (among others) to the masses.
[1]: https://www.artima.com/articles/the-trouble-with-checked-exceptions
[2]: https://joeduffyblog.com/2016/02/07/the-error-model/
[3]: https://www.ponylang.io/
[4]: https://ocaml.org/manual/5.2/effects.html
[5]:…
What we do with the spin lock classes that we use in our products is a SwitchToThread call:
https://msdn.microsoft.com/en-us/library/windows/desktop/ms686352(v=vs.85).aspx
If the SwitchToThread call returns False, then the code reverts to a Sleep(0) call instead.
Credit to StackOverflow and Joe Duffy's excellent article on this:
https://stackoverflow.com/questions/1383943/switchtothread-vs-sleep1
http://joeduffyblog.com/2006/08/22/priorityinduced-starvation-why-sleep1-is-better-than-sleep0-and-the-windows-balance-set-manager/
However, as mentioned in one of the SO replies, you still need to make sure that you don't use locks that use loops on calls like this when those locks will have to wait on anything long-running - you're just going to burn CPU/battery needlessly. So, only use such locks when you know that the protected code doesn't involve any long wait states, or modify the code to back out even further into a proper kernel wait after looping X times. Personally, I agree with second SO answer and don't like such constructs, and would personally say just go with a straight-up kernel wait if there's any question about whether such conditions could exist now or in the future.
Once I got used to writing in Rust, I found it really hard to mess up. I sort of like it for applications, although some idioms can be a bit verbose and hard to parse visually and mentally.
First, thanks to default immutability of variables and the borrow checker, I trust that I'm far less likely to accidentally mutate something and cause an action-at-a-distance bug.
Second, I absolutely love idiomatic Rust error handling of returning a Result (especially combined with the question-mark operator). It forces me to deal with error conditions. Unlike C# or Python or Java or C++, I don't have to worry that any code could throw an exception at any time. (Rust code can panic, but panics generally represent unrecoverable errors — see http://joeduffyblog.com/2016/02/07/the-error-model/#bugs-arent-recoverable-errors ) And unlike Go or C, I don't have to remember to check the error every single time.
I still have to write functionality tests for Rust code, but once I've done that I feel like the code is mostly locked down. It's harder to get to that sense of security in other languages.