This is an impromptu response to Never edit a method, always rewrite it, by Dave Cheney.
At a recent RubyConf, Chad Fowler presented his ideas for writing software systems
that mirror the process of continual replacement observed in biological systems.
Reminds me of Ch. 6, of Applications of Automata Theory and Algebra on Complexity of Evolved Organisms.
This, in my opinion, is a statement about perspective. It's a cognitive bias: when you make one thing immutable, everything around it simply looks more mutable.
We don't want our calling and RPC interfaces to be too complex, we want to use them easily and efficiently all over the place.
They should be simple and easy so that you can use and reuse them.
The first principal of this approach is, unsurprisingly,
to keep the components of the software system small–just as complex organisms
like human beings are constituted from billions of tiny cells
which are constantly undergoing a process of renewal.
- What's small in Flock?
- Interpreted as what's free, given that an expression is valid in Flock: inhabitance typing, parsing complexity, functional typing
- Interpreted as what's cheap: constant upper-bounded shannon complexity is equivalent to having a constant upper-bounded inhabitance count. The problem is effectively reduced to finite combinatorics.
From the talk:
- Get into a mindset where the tools are not monolithic
- One way is to distribute over languages, e.g. use 12-13 backend languages
- Focus on recovering from failures, how do we do this, how can we start recovering before we've even failed?
Notes:
- What can be easily interchanged?
- Equivalent functions
- Type-equivalent functions
- Free implementations of systems of constraints
- Inhabitance-type-equivalent functions
"When the code is so simple that it just can't break, adding tests may serve better to prevent modification of the code than to ensure that it's doing what you want it to." -- Fowler, RC17
Notes:
"You're screwed when you have stasis" -- ibid.
-> We need to be really careful about our metrics.
When we assign a metric, we may become afraid of that metric changing from a long-held constant.
-> "Back to homeostasis"
What does "function-renewal" look like in Flock?
- Exchange of an implementation with a functionally-equivalent implementation
- Elimination of superfluous constraints
- Replacement of an implicit solution with an explicit solution
One approach is to script common issues, and then add some randomness, which I'd consider to be a kind of fuzzing.
Following from that Fowler proposed this idea:
What would happen if you had a rule on your team that said you never edit a method after it was written,
you only rewrote it again from scratch?
-- https://dave.cheney.net/2017/11/30/never-edit-a-method-always-rewrite-it
This sounds like an AST where snippets are immutable.
They may be locked in with types, e.g. _ : Int -> Bool
.
In Flock, this could be a literal code snippet: (code) : Int -> Bool
.
This could work with some types of distributed version control systems,
which could require method-atomic commits.
Well, what are the benefits of distributed method-atomic commits?
- Reduce load required for (re)testing, profiling, organizing code
- Easier parallizability: similar commits are used in Haskell's
STM
module
Fowler quickly walked back this suggestion as possibly not a good idea,
nevertheless the idea has stuck in my head all day.
What would happen if we developed software this way? What benefits could it bring?
I like how distanced it is from the specifics of the talk.
"All of the problems we cause have the same solution...Make smaller classes, make smaller methods, and let them know as little about each other as possible." -- From RailsConf 2014 - All the Little Things by Sandi Metz
What in the world is "small"?
- We might try a lines of code metric, but then the lines could be 100s of characters long
- We might try a character metric, but then we get Code-Golf-esque results
- We might like to use a "perfect" metric, like Kolmograv Complexity, but then we're no longer working with Turing Complete programs.
- We might like to use a highly algebraic metric, such as Krohn–Rhodes complexity, but then we have to deal with translating everything to algebra.
- We might like to use a more efficient metric, like Cyclomatic complexity, but then we have to deal with the issue that few people have any idea what it is.
- We might switch tracks to a CTO-friendly metric, like test coverage, but that's just "moving the dirt around" and we still don't know what "small" means.
Finally, we have this slide from Chad's talk: "Kill and replace cells regularly -- forces you to work with small components."
What about rephrasing this sentiment: "It's small if you can kill and replace it regularly."
But if the ecosystem changes, the metasystems around that "small" code can completly change the meaning, usability, and validity of that literal "code snippet."
Long deploy cycle -> meta systems are not checking up on the code.
I think it depends on how your codebase is set up with your code-review/release-cycle practices: if people are used to moving around individual commits, merging them on such a granular level could seem natural.
Opening up a method to add another branch condition or switch clause would become more
expensive, and having rewritten the same function over and over again, the author might
be tempted to make it more generalisable over a class of problems.
Would it have an impact on function complexity?
If you knew that changing a long, complex, function required writing it again from scratch,
would it encourage you to make is smaller?
I think it depends on the field: On one hand, I'd expect modifications to a high-performance trig-function in C to be small in the first place. On the other hand, I'd expect modifications to prototyping code to be "huge", according to simple commit metrics like line-diff.
Perhaps you would pull non critical setup or checking logic into other functions
to limit the amount you had to rewrite.
Would it have an impact on the tests you write?
I'd expect it to affect the organization of the tests: with tests up from unit (matching the methods), pair (two methods), .., integration, etc. "It's as easy as 1, 2, 3."
[Some functions are] truely complex, they contain a core algorithm that can’t be reduced any further.
If you had to rewrite them, how would you know you got it right?
-
Type-checking
- In Ruby, this could be a test or even a
NoMethodError
- In Haskell, this is part of compilation.
- In Ruby, this could be a test or even a
-
Property-based testing
-
Static test cases
- Sure
2 + 2 = 4
, but is it likely to fail in your system? - You might also want to use a premade list of corner cases
- Sure
Are there tests?
Do they cover the edge cases?
Are there benchmarks so you could ensure your version ran comparably to the previous?
Would it have an impact on the name you chose?
Is the name of the current function sufficient to describe how to re-implement it?
Would a comment help?
Does the current comment give you sufficient guidance?
Great questions. Why not implement your answers as command-line tools, Rake tasks, RSpec tests?
While we're at it, why not implement the questions too: Are the edge-cases covered? What about the tests?
I agree with Fowler that the idea of immutable source code is likely unworkable.
But even if you never actually followed this rule in practice, what would be the impact on
the quality, reliability, and usability of your programs if you always wrote your
functions with the mindset of it being immutable?
"Encapsulation: services need to encapsulate their requirements." -- Fowler, RC17
"This can lead to effects such as small databases all over the place, simply because people would rather work with them." -- ibid.
Ever play code golf? The game where you try to fit a program/function into as few bytes (or whatnot) as possible?
One can reduce the space of possible solutions by providing one or more reference solutions then successively attempting to optimize one of them.
This is not the best strategy, it takes no account of the many nuances of Code Golf, but it's a place where people program within highly constrained bounds.
I'd expect similar impacts on the quality, reliability, and usability of programs as I've seen in Code Golf:
- There are some really high quality programs in Code Golf, of course they're often unreadable, unmaintainable, and undocumented, but there's some good ones!
- Some of the most reliable code I've ever seen has been on Code Golf sites: many of the larger ones end up looking like Rosetta Code.
- Usability, sure! Code Golf solutions can be highly usable. They're usually only usable for one thing, and that thing might not be good for much, but they're usually ready to go out of box!
This sounds like a lot to juggle during evaluation/quoting. How does Flock manage this complexity? Through push and pull-through functor semantics: Haskell prototype
Note: Fowler talks about methods, because in Ruby, everything is a method.
I prefer to talk about functions, because in Go, methods are a syntactic sugar over functions.
For the purpose of this article, please treat functions and methods as interchangable.