I think this two fold distinction is not sufficient. You write code. That’s fine. But when reading code, there are actually two different kinds of reading. Lets start with an example:
val string = "Hello World" string should startWith ("Hello")
Even if you didn’t follow my articles about ScalaTest you can understand what this line of code is doing. In this sense reading this line of code is extremely easy. I call this ‘skimming’
But what if this line of code is not doing what you expected? Then you need to really understand the code. Then you want to understand what is going on. In the Scala example above you might consider the following options: Is startWith(“Hello”) a parameter to should? Or is “Hello” an argument passed to the result you get after passing startWith to should? What is should anyway? A method? An object? A function? An instance with apply method?
As you can see what was realy easy to skim now gets quite challenging to ‘understand’.
The same can be the case for libraries. Consider Hibernate. If you have setup Hibernate, CRUD operations on an entity are really easy to write and understand. But once you get one of the dreaded Hibernate exceptions (LazyInitializationException, NonUniqueObjectException,…) you want to really understand what is going on. And at that point many people realize they have no idea what is actually happening when they call session.update or just the getter of an Hibernate Proxy.
Obviously you want all three kinds of handling code to be as easy as possible, and writing might be the least important, because we read more code then we write. But how important is the ability to skim versus understanding?
With anything you use in an application you will do a lot of skimming. That’s why DSLs like the ScalaTest example above are such a great tool.
I suspect if you do complex things with a library or a language, understanding will always be hard. But in some cases it gets extra hard because non standard tricks are used. In Java a typical case of trick is Reflection and Byte Code Manipulation. Many of the more powerful libraries use it to work around the limitations of ‘normal’ Java. The result is code that is easy to skim but for many people next to impossible to understand. Examples are Mockito or Hibernate.
So when evaluating a library or writing one you might consider how much magic it is using.
The other way to limit the problem with ‘understanding’ code is to make it unnecessary. I think Hibernate is an example where this failed. You have tons of places where you have to understand pretty exactly what Hibernate is doing in order to use it correctly.
Java itself is an example where this works pretty good. In many, many projects nobody has to actually understand how the JVM works. It just works. Only in rare cases you have to dive into the details of the garbage collector or the memory model.
The key here is a clean non leaky abstraction. If you have that it doesn’t matter what kind of magic you use to make it happen. But if your abstraction is leaky, parts of the magic you use inside will leak out with the risk of turning your project into something that acts like it is bewitched.