Monday, January 12, 2015

Readable, or reason-aboutable?

My coworker Tom finds Ruby unreadable.
What?? I'm thinking. Ruby can be quite expressive, even beautiful.
But Tom can't be sure what Ruby is going to do. Some imported code could be modifying methods on built-in classes. You can never be sure exactly what will happen when this Ruby code executes.

He's right about that. "Readable" isn't the word I'd use though: Ruby isn't "reason-aboutable." You can't be completely sure what it's going to do without running it. (No wonder Rubyists are such good testers.)

Tom agreed that Ruby could be good at expressing the intent of the programmer. This is a different goal from knowing exactly how it will execute.

Stricter languages are easier to reason about. In Java I can read the specification and make inferences about what will happen when I use the built-in libraries. In Java, I hate the idea of bytecode modification because it interferes with that reasoning.

With imperative code in Java or Python, where what you see it what you get, you can try to reason about these by playing compiler. Step through what the computer is supposed to do at each instruction. This is easier when data is immutable, because then you can trace back to the one place it could possibly be set.

Beyond immutability, the best languages and libraries offer more shortcuts to reasoning. Shortcuts let you be sure about some things without playing compiler through every possible scenario. Strong typing helps with this: I can be sure about the structure of what the function returns, because the compiler enforces it for me.

Shortcuts are like, I can tell 810 is divisible by 3 because its digits add to a number divisible by 3. I don't have to do the division. This is not cheating, because this is not coincidence; someone has proven this property mathematically.

Haskell is the most reason-aboutable language, because you can be sure that the environment won't affect execution of the code, and vice-versa, outside of the IO monad. Mathematical types like monoids and monads help too, because they come with properties that have been proven mathematically. Better than promises in the documentation. More scalable than playing compiler all day.[1]

"Readability" means a lot of things to different people. For Tom, it's predictability: can he be sure what this code will do? For many, it's familiarity: can they tell at a blink what this code will do? For me, it's mostly intent: can I tell what we want this code to do?

Haskellytes find Haskell the most expressive language, because it speaks to them. Most people find it cryptic, with its terse symbols. Ruby is well-regarded for expressiveness, especially in rich DSLs like RSpec.

Is expressiveness (of the intent of the programmer) in conflict with reasoning (about program execution)?

[1] "How do you really feel, Jess?"

We want to keep our programs simple, and avoid unnecessary complexity. The definition of a complex system is: the fastest way to find out what will happen is to run it. This means Ruby is inviting complexity, compared to Haskell. Functional programmers aim for reason-aboutable code, using all the shortcuts (proven properties) to scale up our thinking, to fit more in our head. Ruby programmers trust inferences made from example tests. This is easier on the brain, both to write and read, for most people. It is not objectively simpler.


  1. > Haskell is the most reason-aboutable language

    I would venture that that honor corresponds to machine code. No compiler or interpreter at all; what you see is what will finally be executed on the machine.

    Unfortunately it is not very good at expressing the programmer's intent.

  2. Quite a meditative thought, and one that makes me wish I was a fly on the wall where the inspiring conversation took place. You've given me the seed of a mental framework that allows me to hold my love of Ruby and Clojure in my head simultaneously.

    1. Consider joining us for our inspiring conversation :-)

  3. > Is expressiveness (of the intent of the programmer) in conflict with reasoning (about program execution)?

    First, it seems apt to recall the idea that all type systems by design exclude certain programs from being run, and that we hope they are only the 'bad' programs; the weaker a type system is, the more 'good' programs will be excluded; the more powerful the type system, the

    I think there is a lot of ambiguity on what exactly "power" in a type system is, and it could mean different things to different people. If power is context-dependent we might have a hard time sorting this out.

    However, your question did not directly reference type systems, but 'reasoning'. Users of dynamic languages clearly reason about their programs too. I wonder what the relationships are. What framework/level of 'reasoning' are we operating in; operational, semantic, predictive?

  4. an excellent point, thank you. Now I understand why I find Ruby so unpleasant - it brings the complexity and ambiguity of natural language into code.

    There was a line in The Imitation Game, Christopher and the young Turing talking about cryptography,
    Christopher: "it lets you say things that no-one else can understand"
    young Turing: "so how is that different from language ? no-one ever says what they mean"
    I laughed, alone in the theater.. ha.

  5. Wow!. Great post. That is exactly why I don't like working with Ruby. I've always felt like I'm wading in a swamp.