Why most “clever” code ain’t so clever after all
drive.google.com
drive.google.com
He gives several examples of poorly-written FP code (which we're supposed to believe is "clever" by virtue of being FP), then introduces some new problem constraint which is carefully selected to make the FP solution break but some equally ridiculous imperative-style code not break. In fact, what he really wants to say is, "FP is stupid", but he won't come out and say it.
This is illustrated perfectly by the curve-fitting analogy: a perfectly-fit 5th degree polynomial is the "clever" solution, but oh wouldn't it be so much simpler to use an best-fit 3rd degree function? And oh look, the 3rd-degree polynomial has a positive slope at the end whereas the 5th degree polynomial has a negative slope, ergo if we just happen to pick a point greater than the previous one, ta-da! The 3rd-degree polynomial fits the new set better than the 5th-degree. QED.
When put that way, the argument is completely idiotic. You could just as easily have picked a new point lying, say, on the 5th-degree polynomial and thus "prove" that it was a better fit for some yet-to-be-defined data than was the "non-clever" 3rd-degree polynomial. The analogy to the code is that it would be trivial to come up with new requirements which could be easily solved by the "clever" FP code and very difficult by the imperative-style code; and in fact the author admits as much in the second case.
By the end I can't help but wonder if this was satire, designed to make anyone agreeing with the article look silly when the author later comes out pointing out all the above...
I read the piece more as a reminder that most specs are usually incomplete/too specific/not conveying what is actually needed, and a warning against cornering oneself with a too clever solution. I also didn't see any big issues with their stance on FP, except maybe that they dismissed the Monoid solution that someone posted down in the comments that looks both robust and readable.
It's like criticizing a Taylor series for diverging from the target function as you get far away from its centre. No shit that'll happen. It's the name of the game.
I do have to agree that that section read a lot like satire.
I tried to make a programming challenge based around this premise: http://alexrohde.com/zennish/index.html#/challenges but it never seemed to interest many people.
Same with fitting polynomials to data. Statistics and Machine Learning specifically warn you of overfitting. You can get 0% training error if you try to fit MNIST with a 60,000th order polynomial, but can you imagine the test error rate?
In what sense could that even conceivably be a true statement? I could define compact sets as sets which make me think of the color purple and it would be a completely valid definition. Nothing will fall apart. I think you are vaguely referring to the fact that proofs concerning compact sets in general topological spaces are not valid if you replace compact sets with bounded and closed sets, but that's just confusing the term "compact set" with what it refers to; the proofs aren't about the term but about the thing, which doesn't change. If "compact set" meant "bounded and closed" and, say, "cover-compact set" referred to sets where (ahem) every open cover has a finite subcover, then the concept of "cover-compact" will have certain general proofs associated to it and the term "compact" will have a different collection of general proofs associated to it.
So in some ways you're making the same mistake the author did: carefully selecting a certain way to generalize a concept (real -> topological) and a certain ill-conceived claim about the original concept (compact=bounded and closed), and then acting all surprised when, oops!, the claim doesn't generalize.
i think we need to merge and supplant imperative and functional
Whenever you're writing code you need to know your audience. Is the Haskell solution really that clever to someone who uses Haskell on a daily basis?
That said, I always liked the saying: it's easier to write code than read it. Which means, that if you're writing code at "maximum cleverness" you're writing code you won't be able to read.
Why would you want that? Wouldn't eliminating inheritance add a lot of boilerplate code?
However, I may just be weird. My Java coding style is probably closer to Rust or Haskell. Lots of interfaces defining my type requirements, and static methods to implement compound operations on those interfaces. I like this because it's easily testable and traceable.
You're stretching a bit too far. Inheritance is nothing but composition with some default implicit virtual method routing. You admit as much in your comment. If you want tools, it's more like a hammer vs a mallet and a prybar. Same tools, same purpose, just one is bundled.
EDIT: To be clear, in inheritance, the contract being implemented is the implicit contract specified by the parent type. (Assuming you're following the Liskov substitution principle, which you should be.)
> Also, maybe eliminate inheritance and force interfaces and composition instead.
Implementing an interface meets all the conditions of an is a relationship; no redefinition is necessary. [0] After all, interfaces are nothing more than abstract base classes with no method bodies to inherit. I haven't argued anywhere that composition defines an is a relationship, so I have no idea where you're pulling that from.
[0] https://en.m.wikipedia.org/wiki/Is-a#Java
EDIT: I really like this article you linked. It has all sorts of things that help me prove my point. For instance, the Rectangle example from the LSP section. If Rectangle were an interface:
interface IRectangle {
double getWidth();
double getHeight();
}
It's trivial to implement concrete Rectangle and Square classes that don't violate any principles. Inheritance, on the other hand, is pretty much guaranteed to be broken as long as Rectangle is mutable, because Square cannot add additional constraints to Rectangle and still pretend to be a Rectangle in all use-cases.Edit: I don't see any composition in your example, just an interface implementation.
EDIT: Notice how I can enforce the invariants of `Square`, while composing a `Rectangle` and implementing `IRectangle`. This works because composition allows me to encapsulate the problematic setter methods of `Rectangle`, which is not something I can do if I inherit it.
class Square implements IRectangle {
private final Rectangle rectangle;
public Square(final double size) {
this.rectangle = new Rectangle(size, size);
}
public double getWidth() {
return this.rectangle.getWidth();
}
public double getHeight() {
return this.rectangle.getHeight();
}
public double getSize() {
return this.rectangle.getWidth();
}
public double setSize(final double size) {
this.rectangle.setWidth(size);
this.rectangle.setHeight(size);
}
}This was one of things that always bothered me about C#, What is the point of properties. What do the offer that public fields don't?
What do getters and setters offer that public fields don't? Properties offer exactly the same advantage as getters and setters, fields encapsulation https://en.m.wikipedia.org/wiki/Field_encapsulation No wonder that you don't understand properties if you don't know why getters and setters are useful.
Now Java is the king of obviousness; it's very clear exactly what is being done on every line of code. It pays for it in being very wordy. I don't like inheritance because it breaks obviousness; it's no longer obvious where a virtual call is actually going to be served. With composition, the immediate object has to serve the call, even if it's just to delegate it. The only virtual calls would be dispatching calls to interface methods. And this style isn't expensive after JIT; inlining these kinds of delegations is its primary optimization. (Think getter-method chains that return a field value, a common pattern in Java.)
Lambdas were a step towards terseness, but they sacrifice obviousness to get there. It hides things... What SAM type is a lambda implementing? You have to check to call site; the lambda itself won't tell you. The anonymous class syntax tells you exactly what is being implemented, but that means you have to type more to get there.
Now, I like properties because IMO they optimize both obviousness and terseness. You are obviously making a single property with a getter and a setter, as opposed to defining each separately. And you are more terse by removing the need to define each separately.
That's kind of the point of generics? I don't understand this argument at all. If you need a specific kind of type, then bound the generic using `extend` and you can use the bounding type's methods and fields. [0]
You're still using a different definition of obvious, despite my having defined it for this use case. For instance:
> Why should be obvious to call stream() on an object that is already a collection?
That's the very definition of obvious. You are making it quite clear that you intend to use this collection as a stream. It's extremely obvious. Same with the buffered reader; Readers are unbuffered, and it's very obvious that you are buffering the reader when you wrap it. (Also, Java has `java.nio.Files.readAllLines` [1], so I'm not sure what you're going on about.)
Extention methods are very non-obvious; where is an extension method defined? The answer is anywhere; it can live in any type you imported. It's not obvious looking at the line of code where execution is going to flow. (And yes, an IDE will track it down for you. Just because an IDE can do the work for you, doesn't mean that there's not work to be done.)
[0] http://docs.oracle.com/javase/tutorial/java/generics/bounded...
[1] http://docs.oracle.com/javase/7/docs/api/java/nio/file/Files...
> The stream is a very well known and completely different concept and it is widely used in reactive programming, they just stole that concept to use it in a completely non obvious way.
Java streams seem to meet the ideas of stream processing to me. Given a sequence, apply transformations in order, possibly in a transparently parallel fashion. Compare the Wiki definition with the `BaseStream` interface documentation:
https://en.wikipedia.org/wiki/Stream_processing
http://docs.oracle.com/javase/8/docs/api/java/util/stream/Ba...
> Finally extension methods are perfectly obvious. They are just syntactic sugar for static methods, I hope that at least you agree that static method are the gist of obviousness.
Yes, I of course agree. My problem is that extension methods hide where they live, by pretending to live on the type. And to be fair, I have the same issue with static imports in Java. When I look at a function or method call, it should be immediately apparent where control flow is going to go. That's not the case with either static imports or extension functions; I have to resolve the method name first.
The first axis is how far away you are from the naive solution(s). The further away you are, the longer it takes someone else to understand what you're doing. In some cases you have to take a non-obvious approach, usually for performance reasons in my experience. Fortunately, commenting is a good way to explain when you are using a non-obvious solution to a problem.
The other axis is how many edge cases your solution has relative to the general case. The negative connotation of "clever" is that the clever approach works but is over-specialized for the specific set of cases where it's used. It might not solve the general problem, but it solves enough specific cases of the general problem for your current needs. Then when for some reason the larger program changes (new feature?) and the solution is expected to handle a new case, it fails, and now you've got a clever non-working solution.
To me, code that is clever in the sense of "non-obvious algorithm" is fine when it is needed. Just comment and explain it well. The problem is when your clever code comes with a side effect of brittleness. Either document the edge cases thoroughly, or use an approach with fewer edge cases.
If you removed the "fizzbuzz" related names from the code, could you tell that listing 4 is being used for a FizzBuzz? Sometimes a 3&5 FizzBuzz is just a 3&5 FizzBuzz and nothing more, and doing anything further is akin to premature optimization. Planning is hard, haha.
I realize FizzBuzz is a toy solution for the sake of example, and the solution with fewer assumptions is more often the better choice when dealing with larger problems, overall this was a very good article. But we also need more programmers to understand that if you're writing an internal backend function which is passed a date-as-integer, you probably don't need to double check that it's actually string representation of the integer, or an ISO formatted date, or YYYY-MM-DD formatted string, or YY-MM-DD formatted string, or....
Listing 4 was harder for me to understand. In particular, I had to actually look up `foldl1`. Once I understood it, though, I could see the beauty of its generality. I'd say it took me twice as long to understand listing 4 fully compared to listing 1.
Authors get paid to write prose people can read - the readability of our work only 'matters' insofar as good craftsmanship practices help the team maintain velocity over the long haul.
As tired as I am of seeing sloppy code, I also intensely dislike the trend at the other end, of creating a half-dozen RESTful microservices and throwing away the relational DB just because it seems cool, maintainability be damned.
I'm hopeful that software development (especially business software development) will someday escapes it perpetual adolescence, and will develop stronger ethical standards.
My interpretation is the opposite. I call code "clever" after I've understood it. I agree its not universal, but in all the cases I've seen it used so far, its been another way to say "This could have been written in a simpler, more readable manner. And it should have been."
To play the devils advocate though and show that sometimes we have to challenge our rule set I'll give an example.
I worked in a C# shop where they were using Webforms for a massive application in 2012. Their developers were old hands at winforms and didn't like to learn new things in the framework. So myself and another developer started introducing things such as Linq, javascript, and other novel concepts.
Because some of the old hands there didn't understand it we weren't supposed to use it...
I also attempted to get people to write more testable code so that when we do make a change it doesn't break something in a different section of the codebase. I got push back on everything.
So I'd amend, "If your code cant be understood by someone else with minimal efforts and/or explanation it is not good enough." Because sometimes people don't understand the code because they don't have an inquisitive nature.
There's probably lots of apps that need real boring code, so you can add yet another case to the data entry app to handle the extra 2% sales tax for a certain county in Wyoming.
I can't imagine that there's any apps anyone wants to work on that have a low bar for "cleverness". And it's mostly a one-off thing. Learning language features should not be the bottleneck on getting someone up to speed on a project. On any system of any fun or size, the domain-specific stuff probably dominates.
(Anecdote: I've had several people come onto projects with F#, fresh, no FP/F# experience. It's been very easy and after a while they wonder how they ever put up with C# in the first place. And the people that aren't able to do this, I wouldn't want to hire to work on C# either.)
Usually, making good developers write code for less experienced developers makes the code quality worse. OTOH, it can then be maintained by entry-level developers.
I'd argue there's a difference between being concise and being terse. I think that's what's at the heart of beautiful code: It expresses its ideas in a very simple, concise manner, without sacrificing readability. ("As simple as possible, but not simpler")
why have multiline if/else when a tenary operator can do?
I rather read a = b ? c : d; than possibly 4 lines of code, p/s ignore the names of the variable.
There are plenty of code out there that are so easy to understand if you read them one line at a time, but very difficult to see the big picture, say there is a method being called as follows foo->dostuff(bar).
The issue is there is so many layers between calling dostuff() and something interesting happening. because dostuff calls nextstuff() and nextstuff() calls stuffafternextstuff(). The code is easy to read but terrible and tough to unravel.
There is only so much anyone can hold in their head at once, a bit complex but 2 layers deep is better than so simple to understand one line at a time but 10 layers deep.
It also happens to make testing a lot easier.
It's probably just a crystalization of the old idea of keeping business logic out of your code, but somehow misses the open invitation to create rules engines...
The article doesn't use "clever" as an antonym for "readable", though. The point is that clever code can be excessively brittle.
For example, we are using Higher Order Messaging[1] to turn this:
if ( [delegate respondsToSelector:@selector(doSomethingElse:)]) {
[delegate doSomething:self];
}
into this: [[delegate ifResponds] doSomething:self];
Yes, the implementation of -ifResponds is slightly clever, though I've gotten it simpler over time, but it certainly is worth it in the simplification of client code.Engineers manage tradeoffs. That's what makes them engineers.
Software design involves a similar trade-off where simple (but repetitive) code can be traded for a smaller (and maybe more complex abstraction). It's not about "cleverness" (though that can be a strong warning sign), but instead if the abstraction involved is correct, useful, and appropriate.
> My code in Listing 4 is not based on those assumptions.
Yes. It's based on the new assumption that it might be necessary to "add that pair to the list of cases" in the future.
> I choose not to read into requirements more than needed.
Except for the new abstraction that attempts to allow for other word replacements not mentioned in the original specification.
While I agree that hard-coding assumptions is usually a bad idea, soft-coding[1] can also be a problem.
> FP
> OO
That's the Expression Problem[2], which always involves yet another trade-off. Some types of modifications are easier under FP, others are easier under OO.
fizzbuzz' :: Int -> String
fizzbuzz' n = (test 3 "fizz" . test 5 "buzz") id (show n)
where
test d s x | n `mod` d == 0 = const (s ++ x "")
| otherwise = x
fizzbuzz :: [String]
fizzbuzz = map fizzbuzz' [1..]
The article is very interesting and worth a read. In particular, this implementation doesn't come out of "thin air": it's been golfed from an implementation that defines a simple domain specific language to solve FizzBuzz. And if you understand your problem well enough, DSLs are an extreme force multiplier.Feature request: Client should be able to optionally request "Lizard" as another string whenever the number is a multiple of 13.
That's what the article is referring to. Your function may be more clever, but it's more specific, works in only exactly the requested cases and is harder to change.
fizzbuzz' :: Bool -> Bool -> Int -> String
fizzbuzz' big doLiz n = (fizz . test 5 "buzz" . lizz) id (show n)
where
test d s x | n `mod` d == 0 = const (s ++ x "")
| otherwise = x
fizz = if big then (test 1023 "fizz") else (test 3 "fizz")
lizz = if doLiz then (test 13 "Lizard") else id
fizzbuzz :: Bool -> Bool -> [String]
fizzbuzz big doLiz = map (fizzbuzz' big doLiz) [1..] fizzbuzz' n = (test 1023 "fizz" . test 5 "buzz" . test 13 "Lizard") id (show n)
easy as pie...The reason we think that cycling a list is "clever" but using modulo is "boring" is because most of us use modulo often, and cycling rarely. There is nothing inherently "clever" about either of the two approaches.
In an alternate universe where we all used Haskell, perhaps the modulo solution would come off as "clever". And perhaps this article would be pointing out the flaws in the modulo approach instead: the fact that numbers divisible by both 3 and 5 have to be treated by a special case, the fact that it doesn't easily generalize to fizz buzz bazz, etc.
---
Also, I think you're slightly wrong in that cycled lists and modulo are "just different abstractions" - they may be to a machine, but they are not to people. And in this case, critically, the original problem's phrasing is much closer to the modulo implementation than the cycled list equivalent: "...for multiples of three print Fizz...".
By choosing to encode that constant 3, rather than represent it, you're being clever; by failing to use a straightforward analog of "is multiple of", you're being clever.
This "encoding" has real risks - for instance, because you're human, you'd immediately notice that 5 is not 6. You don't need to think to do that; don't need to process equivalence or do any kind of algebraic reasoning; a kid can and will notice the identical shapes before they can talk or walk. It's built in. By contrast, even a trained professional might overlook a list with 5 spaces followed by Buzz as opposed to a list of 5 elements consisting of spaces followed by a single Buzz. Sure, it's a really stupid mistake, and one I hope you make very rarely - but people make stupid mistakes. No need to encourage that habit by making them think.
So no, it's not that cycled lists are bad and module is good, it's that the problem description is closer to the definition of modulo than it is to list cycling. (This is my interpretation, the article's definition of clever is more about seems and changes and doesn't really touch this).
But I take the problem description as only describing a specification of what output is wanted, I don't think it's intended as a guidelines of how it should be implemented internally. If teams I've been on ever implemented things as some of our clients imagined they would work (which we had to sit through while trying to coerce some coherent requirements from them) we'd have been in a world of hurt :)
The closest application of CSP-like problems in places close to statistics-land is in neural networks, where Rumelhart published in multiple places that the objective of neural networks, including the backprop net that he reinvented, was soft CSP. So actually, there is probably a path towards formal equivalence.
Of course, there's a disgustingly large amount of work done on the analogies between CSP and Ising model at criticality, because you only need to look at the damned things (compare to Hopfield model). The other path is to look at R. Sole's work on munging for power law-ish things in software. So physical methods for software are actually quite old hat (from the 90's). It's just that it's all really fucking hard condensed matter stuff.
This is quite close to Huberman's work at PARC about networks and stuff, which led to the different Adamic and Huberman munging for power laws in networks and Internet things.
I've seen some weird-ass calculations in code with no comments. If the author has gone through so much effort to come up with the clever solution why not add some commentary so the next guy can read it quickly.
That's why every language has support for comments..
For calculations, I'm not really sure I agree either. I used to work in games, specifically on physics and collision, and there's a point at which you have to assume that the person maintaining it has a reasonable level of familiarity with the subject matter, or else you'd need to write a comment that contains all of a linear algebra course.
Calculations are the most important part of the code to comment I'm an Engineer most of the guys I work with have never been formally taught any programming often I see a bunch of code written in Fortran with notoriously short variable names if it wasn't for comment very easy to get lost.
So long as there is a comment saying something like
"Ergun's Equation - Calculates Pressure Drop"
You don't need to follow all the fluid dynamics to know what the code does - at minimum someone reviewing it can google "Ergun's Equation"
You don't need to comment linear Alegbra but at least comment what the code does "//Solve using LU factorization" or similar is sufficient and it beats seeing segments of code like this with no explanation:
r[i] = 1/(pb[i]+t[i]-fr1[i-1]-gr2[i-2]); u[i] = a[i]-r1[i-1]u[i-1]-r2[i-2]u[i-2]; f = pc[i]+t1[i]-h*r1[i-1];
Edit: It stripped chunks of the above code but you get the idea.
While comments can certainly become stale, we're talking about portions of code that will most likely not change frequently. If there's a clever solution it better need to be clever otherwise it's just bad style.
for(var i in list) {
(function(element) {
someAsyncCallThatNeedsTheRightIndexedElement(element);
})(list[i]);
}
I've seen people grumble that this is "too clever"I've been known to snap back "Bootcamps don't have a monopoly on what technical solutions are valid"
Pointers are probably too clever for most people.
Admittedly, goto is too clever for me. :(
for(var i in list) {
someAsyncCallThatNeedsTheRightIndexedElement(list[i])
}
though I usually try to avoid for loops with `.map` or `.forEach`: list.forEach(someAsyncCallThatNeedsTheRightIndexedElement)
The IIFE in your code is unnecessary and I would thus probably side with the too clever camp in this case. a = []
for (let i = 0; i <3; i++)
a.push(() => console.log('hi ' + i))
for (let e of a)
e()
Logs: hi 0
hi 1
hi 2
Actually, it's almost surprising in the opposite direction now.Hmm, it's also what drove me away from Haskell and kept me on and off with the language, as most of the powerful stuff are too powerful and so hard to tame...
[1] http://hackage.haskell.org/package/base-4.9.0.0/docs/Prelude...
1. We have an increasing sequence of numbers.
2. Every number divisible by 3 is replaced with "fizz", every number divisible by 5 is replaced with "buzz", every number divisible by both is replaced with "fizzbuzz".
If you set out to solve these two parts individually, you would never come up with the clever solution.
While the desired result of fizzbuzz is a specific sequence of numbers, that fact is almost incidental. The clever solution doesn't really look at the problem definition but instead merely generates the desired sequence.
However, I would argue that the classic guarded function using modulo would actually be clearer. It takes a moment to follow the logic of cycling up an infinite list, and modulo statments are far clearer and easier to understand to most people, I would say. In addition, it's easier to change than either of the above, and equally declarative: modulo is a declarative operation as is a guard: when x % 3 is zero, it IS fizz.
Everyone has Resharper, which is undoubtedly an invaluable tool. However, Resharper had a feature that will convert for/for each loops with conditional logic into LINQ lambdas.
For simple cases I find it's conversion useful and readable, but for more complex cases it becomes an unreadable nightmare for anyone bug fixing later on. I find that to be the case even when I personally wrote the original code, so I avoid these conversions whenever they become overly complicated.
I use to see it a lot a few years ago, maybe I just got a little better at my job?
https://gist.github.com/anonymous/7a1829f61cf9302c188f80a736...
speaking of difficult to read. please don't set examples with terrible single letter variable names. and terrible two letter variable names. these are even less forgiveable than clever tricks in my book. don't encourage abbreviating things.
programming is about thinking about, reading and writing code a hell of a lot more than it is about typing.
I like that he made an attempt to make this language-agnostic, the final example, but now he's introducing the idea of "elegance," (missing a definition) and kind of saying that "cleverness == elegance == overfitting?" I think the problem here is that he's _too_ generic (might I say, he's trying to be too clever : P) and pulls in a third field to make his abstract "cleverness" case. But, from my understanding, I'm under the assumption that cross-validate is a given for models: overfitting is for chumps and students. I'd give that a "to be improved" for draft 1.1.
---
Overall, from the recap:
> “Clever” code tends to exploit explicit and implicit symmetries in the problem structure, building highly-symmetrical solutions by exploiting highly-symmetrical library functions / types.
and from the abstract:
> Unfortunately, people tend to assume that the style of such (supposedly) clever code is representative of “good” functional programming.
The main issue here is that clearly the first part is correct and makes for fragile code (given context), but he’s completely opinion based on the latter. What he needs to do is find “production-level code" or “education-level practices” that are handed down to new FP developers and taken as “truths." After reviewing the citations, I think that his points on [6] and [7] are weak: he needs to show more than just comments and one-offs, and needs to hammer in the "why this is bad" more. Instead, it feels like he's just trying to show his contempt for haskell. I'm also thinking that, if he wasn't just trying to run haskell through a ringer, you'd think he'd use other functional languages / language-features (frege is designed as "a Haskell for the JVM").
His opening line of 'there is a deluge of “clever” code constantly pushed down from gurus to hipsters to innocent readers' also primes me to think that this is more of a cathartic article than a useful one.
I'd love to agree with him, say that "stupid code, everywhere" is a problem in educating new devs for pure-functional languages, then spider the internet, comment/fix all these code examples, and finally make all my friends haskell developers : P (disclaimer: I'm just an enthusiast). However, I remain unconvinced — I think the problems in haskell education is elsewhere. Perhaps I have simply been lucky in finding sane, good resources, and all the production code I study is uncommonly blessed. Looking forward a 2.0 so that I can convert all my friends.
(I redacted some more thoughts since this is my first real post to HN; be nice!).