chriswarbo-net: 4e6c76d388c0f183fc0e1708e89f5b5aab66b166
1: ---
2: title: Paths of Least Resistance
3: ---
4:
5: > There are two types of programming languages: those which people complain
6: > about, and those which nobody uses.
7:
8: My previous boss used to trot out this line whenever I bemoaned some failing of
9: the technologies we were using (which almost always meant PHP doing something
10: braindead). This is an example of a [thought-terminating cliche](
11: https://en.wikipedia.org/wiki/Thought-terminating_clich%C3%A9): a pithy way to
12: end a discussion with a whiff of resolution, without actually addressing any
13: point or preventing the problem arising again. I've seen this same "apologist"
14: attitude come up a few times in online discussions too, so I thought it was time
15: I wrote the sort of rebuttal I think it deserves. I apologise for the verbosity
16: of this response; it is unfortunate that, by design, the densely-packed nonsense
17: implicit in a thought-terminating cliche can take a lot of work to extract,
18: expose and debunk.
19:
20: ---
21:
22: Like many things in life, everything in programming has problems; that *doesn't*
23: mean everything is equally problematic! In particular, I've become wary of
24: approaches which are fraught with:
25:
26: - Gotchas: These are known, acknowledged breakages in functionality or
27: abstraction, which seemingly come out of nowhere for the uninitiated
28: (violating the principle of least surprise). Those with more experience tend
29: to instinctively code defensively to avoid them, obfuscating otherwise-good
30: code for the fear of triggering one of these situations.
31: - Misaligned Incentives: Where following the "correct" practice is directly in
32: conflict with some other objective. For example, if the "correct" solution is
33: more verbose, slower, harder to debug, harder to read, less modular, etc.
34: - Lack of Objectively Checkable Criteria: Where there's no way to agree if
35: something is done "correctly" or not. This makes it hard for learners to
36: guess what the "correct" approach calls for in any given situation; and also
37: allows goalposts to be moved after the fact, to denounce genuine attempts to
38: follow the practice as "not done *properly*" if they turn out to make things
39: worse.
40:
41: This can be summarised as fixing *the path of least resistance*. Here are some
42: examples of practices which are *all* flawed, but where some are less prone to
43: the above than the alternatives.
44:
45: # Static Typing #
46:
47: Most static typing gotchas are conservative, i.e. some code isn't allowed even
48: though it *might* be correct. This is preferable to allowing broken code, as
49: long as it's not too burdensome to pass the type-checker. The widespread use of
50: statically typed languages (e.g. Java and C#) shows that it need not be too
51: burdensome. Examples of gotchas are [Haskell's "monomorphism restriction"](
52: https://wiki.haskell.org/Monomorphism_restriction) and [Java's lack of
53: multiple-inheritance](
54: https://stackoverflow.com/questions/52620936/why-does-java-not-allow-multiple-inheritance-but-does-allow-conforming-to-multip);
55: in both of these cases it's *possible* to cause a problem, so the type checker
56: forbids it (even though it *might* be fine).
57:
58: For 'incentives', type-checkers don't have any of their own, but they're a
59: mechanism to bring the programmer's incentives ("get this code to compile") into
60: alignment with those of library designers ("make sure users call things
61: correctly"). Hence they're the *opposite* of misaligned incentives. An example
62: is string concatenation, which is the easiest way to dynamically create URLs,
63: HTML pages, SQL queries, shell commands, etc. but is vulnerable to injection
64: attacks. If a library/framework makes each of these a different type then this
65: easy-but-vulnerable approach is no longer possible; if escaping
66: functions/methods are the only way to convert a string from one language to
67: another, then the easiest way to combine strings is to escape them
68: appropriately, hence bringing the user's incentive ("do the easiest thing that
69: compiles") into alignment with the designer's ("prevent vulnerabilities").
70: Another example is sequential coupling, where one function/method, like
71: `dbConnect`, must be called before others, like `dbQuery`. The easiest thing is
72: to just call `dbQuery`, which won't work; the designer can forbid this misuse by
73: having that function require a `ConnectedDB`, and have `dbConnect` be the only
74: way to obtain one. Again, the easiest thing for the user to do (in order for the
75: compiler to succeed) is to use the library correctly. There are many other
76: examples of this sort of thing: in general, we *cannot* necessarily increase the
77: safety or correctness of the easiest approach to a problem; but we *can* use
78: types to easily *forbid* such easy "solutions", forcing the use of more
79: "correct" approaches. Overall this can make life more annoying, but it increases
80: the safety of the path of least resistance.
81:
82: Regarding objective checkability, static types are not only objective, but also
83: *automatically* checkable, since that's what type-checkers are all about. It's
84: less trivial to know whether a particular *choice* of types is objectively
85: "good", but there *are* some general criteria, e.g. "invalid states should be
86: unrepresentable". Relatedly, we can objectively (and *sometimes* automatically)
87: check whether all cases have been handled, which gives an indication of whether
88: our types are a good fit for the problem (e.g. if we're passing around JSON data
89: using strings, there will be lots of boilerplate and/or unhandled cases for what
90: to do when given a non-JSON string; using a more precise type would reduce
91: this, indicating that it's a better fit for the problem). Note that these are
92: general rules, not vague sometimes-"proper"-sometimes-not heuristics.
93:
94: # Automated Testing #
95:
96: Testing has both overly-conservative and overly-liberal gotchas.
97: Overly-conservative examples are things like depending too heavily on
98: implementation details (e.g. checking if one list equals another, when their
99: order doesn't actually matter) or testing invalid situations (e.g. generating
100: test data which doesn't satisfy some required invariant). Like static typing,
101: these will forbid some correct code, making some tasks harder than
102: necessary. Overly-liberal examples are things like only testing the happy path,
103: or forgetting some edge case or space of inputs (e.g. negative numbers), or
104: failing to test the right code (e.g. mocking the thing we want to test). These
105: allow easy-but-broken code through, which might otherwise be caught. This is a
106: real problem with automated testing, and one reason why it's not a silver
107: bullet.
108:
109: Automated testing also has misaligned incentives, if the developer of some
110: feature is the one deciding which tests to write. This is because the path of
111: least resistance is to have no tests at all. Some measures try to prevent this,
112: e.g. code coverage and mutation testing, but they have their own
113: issues. Incentives can be aligned more if the some of the tests come from a
114: separate source, e.g. acceptance tests based on some requirements spec.
115:
116: There are objective criteria for automated tests, like code coverage and
117: mutation testing mentioned above. They're not perfect, but seem to work well as
118: long as they're not being gamed (i.e. when they're treated as an indicator, not
119: as a goal).
120:
121: # Purity #
122:
123: This is another example of gotchas being overly-conservative, since we might
124: want to use impure components like an internal cache or in-place mutation, which
125: are impure but might just-so-happen to be safe. Again, I think it's better to
126: make the path of least resistance more correct, even if the general way to do
127: that is to forbid the easy things (which are often wrong).
128:
129: I think the only real incentive misalignment for purity is efficiency; yet as
130: Knuth tells us, this is usually fine to ignore. It's certainly the case that
131: having more pervasive use of pure languages in our stack (from the kernel up to
132: our scripting) would probably make things slower overall; yet I think the safety
133: benefits would outweigh those downsides for many (me included).
134:
135: The interesting thing about purity is how much it can simplify (automated)
136: reasoning: sure it might seem inefficient at first glance, but compilation can
137: perform much more invasive changes on pure code than are possible in the
138: presence of side-effects. Supercompilation and superoptimisation come to mind;
139: although there is certainly a cognitive burden when trying to keep track of how
140: our code will ultimately compile.
141:
142: Purity is trivially (automatically) checkable, since we simply don't bother
143: putting mutable things into our language. Haskell has famously struggled with
144: this, sticking to lazy evaluation (since, after all, anyone wanting strict
145: semantics could already pick any of the MLs, Schemes, etc.) which seems to
146: necessitate purity (due to evaluation being forced "back to front"). This "hair
147: shirt" approach paid off massively with the recognition of the importance of
148: monads; more recent investigations into algebraic effects have also proved
149: useful, again precisely because of the need to deal with purity (arrows were
150: interesting back in the day, but fell out of favour once applicative functors
151: and profunctors were adopted).
Generated by git2html.