C. Alexander in _Notes on the Synthesis of Form_, rather explicitly puts forth the essential architectural problem in a semiformal form - a constraint satisfaction problem.
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.
Whether or not something is "clever" as opposed to annoying or obtuse depends heavily on context. In the context of a production programming environment, it's all about cost/benefit. Does it reduce the team's cognitive load? At what cost?
This is a fairly well known thing in all programming communities that matter, but it was good to see it pointed out once again.
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.
You can code yourself into a corner no matter what language you use. I've not seen much of this beautiful many-seamed code that could suddenly accommodate unanticipated feature requirements without rearchitecture, but I have enjoyed a certain amount of seamful boilerplate requiring much burnishment as changes are made. I don't think I'm anywhere near as experienced as the author but I still think this article is setting up strawmen and knocking them down.
I run a small dev company. This is the first lessons we teach our programmers. If your code cant be understood by someone else with minimal efforts it is not good enough.
Do you maintain a high-enough level within the team though? Most "clever code" I've seen is called clever by people who are not yet proficient enough in a language to understand it. So with unskilled programmers pretty much anything that isn't straight combination of classes, loops and conditionals will be considered clever. There's a tradeoff here - I get the business reasons why one may want to have the codebase at the "lowest common denominator" level for the team. But then again, programming is a craft, so that person who says lambdas in Java are "clever code" (real example from my work) should IMO suck it up and spend few hours learning; then the code will stop being "clever" to them.
Not every company wants highly skilled programmers. Depending on the complexity of the project, and cost of defects unskilled programmers may be the most cost effective.
I know. Took me a while to realize it and understand why companies think that way, but it boils down to the fact that as programmers, we're not paid to do a good job, we're paid to do a good enough job. Those two goals are very often at odds. It seems to be a typical clash of craftsmanship with market economy.
Another articulation is - we aren't paid to write code, we are paid to build stuff.
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.
That's a good point. There is also a fine line between craftsmanship and intellectual self-gratification at a customer's expense. Doing a good enough job that solves a customer's problems without gold-plating or unneeded complexity is what professionals in most fields are ethically required to do.
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.
> Most "clever code" I've seen is called clever by people who are not yet proficient enough in a language to understand it.
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."
This needs to be seen in context though, because it critically depends on who "someone else" is. A category theorist will have a much more abstract view of a Haskell program than your usual developer for instance and something that seems tedious and unnecessarily technical to one can actually be a useful abstraction to someone else if one knows how to use it (e.g. see all the monad submissions).
I agree to a huge degree. If your code is obscure and overly complicated then it's probably not a good idea.
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.
Hell, there were public, senior, MS hires that fundamentally didn't understand "var". Let alone how LINQ worked or anything like that. On HN last month, someone was critising the use of flatmap as being too complicated. Despite the type signature making it clear what it does.
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.)
There's a cost to that too, unfortunately. Longer code may be easier to unserstand line by line, but the sheer volume of lines can make it harder to comprehend. Terseness has its advantages.
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")
This is a very delicate subject. If the program is using language constructs and can't be understood because the reading programmer doesn't know it, then who's fault is it?
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.
Bertrand Meyer and a few other notables talk about separating decisions (policy) from execution, which happens to address some of those issues, because the decide and act become sibling function calls instead of a deeply nested chain of responsibility arrangement.
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...
Cycled lists and the modulo operator are two different abstractions for thinking about the same problem, in the same vein of "blind men grasping at elephant." They are different perspectives on the similar problem.
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.
The issue with the first fizzbuzz isn't just cylcling lists, it's also the use of zipWith. At least, that's what causes some of the brittleness in his examples.
---
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).
> problem's phrasing is much closer to the modulo implementation
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 :)
Modular arithmetic (a.k.a. clock arithmetic) is equivalent to cycles in a very real, mathematical way. There's a reason that clocks are round -- in order to count the naturals modulo 12 (and again modulo 60), you simply need to cycle 1..12 (or 1..60). I'd argue that for anyone who understands modular arithmetic, the `cycle` solution doesn't seem particularly clever. Anyone with enough knowledge of number theory to understand that "x is a multiple of y" -> "y divides x" -> "x (mod y) = 0" ought to have an understanding of the relationship between modular arithmetic and cycles. This is introductory-level discrete mathematics.
Some like to say that you should never write clever code. But clever to who? Some people lambdas, map, reduce, and even polymorphism are clever. So in a vacuum the statement is meaningless.
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.
My rule of thumb is that I attempt to write code that a junior programmer whose experience is limited to a little more than having taken a course in the language and has done a couple tutorials on the framework(s) can maintain. I see so many developers try to squeeze every new language feature or loads of esoteric library use cases into their code because they learned something and feel like it MUST belong in the production code. Sometimes you have to write complicated code, but 99% of the time it should be possible to write very simple code using only the most commonly used language features.
For all of Java's pitfalls, this is one of the things it really has going for it. The language itself is extremely simple. I think adding properties instead of the weirdness of getters and setters would take it to peak simplicity and straight-forwardness. (Also, maybe eliminate inheritance and force interfaces and composition instead.)
I think default methods take care of most of the places where boilerplate would be required. The abstract collections are a great example of this; most of the methods required of a `Collection` can be implemented from a small subset. Sometimes you'll re-implement other operations anyway for efficiency gains, but that's supported with default methods also.
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.
Composition increases boilerplate code.
With simple inheritance hierarchies there is an inherent advantage in this regard.
If you use inheritance you don't need to encapsulate the "base" object and implement all the methods of the interfaces doing most of the time just a routing to the base object, you can just extend from the abstract base object and implement only the methods that you need.
Obviously if your hierarchy is complex then it is much better to use composition to have a better decoupling, but it has nothing to do with decreasing the boilerplate code.
Composition and inheritance are just two tools for different jobs.
If someone says Composition > Inheritance it is like saying Screwdriver > Hammer, a complete non-sense.
There is no tool that is inherently good or bad, it all depends how the craftsman uses it (or don't) in a good or bad way for the job.
> If someone says Composition > Inheritance it is like saying Screwdriver > Hammer, a complete non-sense.
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.
The purpose is completely different. You use interfaces and composition to decouple the implementation of some objects, you use inheritance to define some object hierarchy. Inheritance specifies the is a relationship, Composition the has a. As you can see these two are totally different concepts.
What is an is a relationship? It is the promise that a type implements a contract. That sounds like an interface to me; implementing interfaces also forms is a relationships. Allow interfaces to be externally defined for a type, and now you have Haskell classes or Rust traits. This isn't a new idea I'm proposing here; it's quite workable and very straight-forward.
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.)
you were not speaking about interfaces, you were speaking about composition.
As I said composition and inheritance are two completely different concepts.
And I'm not making up the definitions, is a and has a are very well known concepts in computer science.
You can argue ad libitum, but this doesn't change the definition.
https://en.m.wikipedia.org/wiki/Has-ahttps://en.m.wikipedia.org/wiki/Is-a
I'm starting to feel like you're not actually reading anything I write. I specifically include interfaces. To quote myself:
> 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.
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:
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.
You wrote this: "Inheritance is nothing but composition with some default implicit virtual method routing."
In response to my post where I said that inheritance and composition are two different tools and doesn't make sense to say that one is always better than the other.
So it seems to me that you think that inheritance and composition are the same concepts given your words that I quoted and given that you were arguing with my definition of different tools.
Edit:
I don't see any composition in your example, just an interface implementation.
OK, I should have stated that inheritance is a bundling of multiple tools, composition and default implicit virtual method routing being two of them. It also creates an is a relationship, which in my version of an inheritance-less world would be handled explicitly via interfaces, which I included in my original "toolbox".
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);
}
}
Adding properties would just make the language MORE complex. Then you would have fields, getters/setters AND properties. That said, a "property" keyword that auto-generated default getters/setters would be nice.
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?
You don't need getters and setters in c#. If you don't have any side effect in a read-write property you can just use an automatic property. The same for read only properties that don't need to be recalculated on the fly. And I hope that I don't need to explain why using a write only property is almost always a code smell.
In the other cases you can use a normal property with a backing field.
If your code is well written and you use the appropriate tools, only rarely you need normal properties (that are as annoying as getters/setters).
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.
Java is a quite awful language honestly. And I use it everyday at work. Generics implementation with type erasure is a pain and complicates things a lot. There is no automatic static type inference and before Java 7 you had to specify twice the generic types in the same statement. This adds a lot of "complexity" or noise when reading some code. Checked exceptions..do we want to speak about all the complexity that they add?
Before Java 8 there was absolutely no first class support to closures, and if you wanted to have something equivalent you had to use an anonymous inner class... Very simple, eh?
All the streams (as in input/output streams) are heavily used and while sometime they are useful, they complicate needlessly the code.
The streams implementation (as in the Linq-like feature) is more complex than Linq.
Honestly I can't say that Java is a simple language.
It may be true if you just use loops and basic statements, but you can say the same for pretty much every other high level language.
Finally I'm quite perplexed for the reasons why you want to kill inheritance.
While it is true that when it is abused is bad, ditching it completely is wrong because used in the right way can make you code simpler.
I think there's two definitions of simple being used between you and I. Let's use different words instead: obviousness and terseness. Obviousness is how clear the goal of the code is; terseness is how much typing it takes to get there. I'm using simple to mean obviousness; this seems to be in line with the article, as being "clever" typically means exchanging obviousness for terseness. You seem to be taking terseness as your definition of simple.
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.
I don't think Java is obvious at all apart the aforementioned simple statements.
With generics you have actually no idea of what type you receive at runtime, you receive just an object. If the goal of the code is to return some sensible default for a specific type in Java you cannot use generics. And it's not obvious at all for anyone that doesn't know the generics implementation details.
About closures, do you think that it is more obvious to define an entire anonymous inner class when you really just need to define a method delegate or an anonymous method? For me it's completely counterintuitive other than it makes my eyes bleed. If I need a local variable I obviously define it, I don't create a class which exposes an encapsulated field and use it. And it's exactly the same thing when I need an anonymous method.
When the goal is reading a file why should be more obvious to create a buffered reader that encapsulates a file reader that wraps a file and use it to read when I can just do File.ReadAllLines() in C#?
What is the added value of calling stream() on everything before using any enumerable? In C# obviously you just call Extension Methods on an IEnumerable. You even get IntelliSense. Why should be obvious to call stream() on an object that is already a collection?
> With generics you have actually no idea of what type you receive at runtime, you receive just an object.
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.)
The point of generics is to pass generic arguments to a method/class, as the name says.
In Java you are losing every type information and at runtime your method will accept objects, not arguments with a generic type.
There are several situation in which preserving the type information is extremely useful.
For this reason in Java you still find a lot of libraries that receive a type as argument, while in C# you can just specify the generic type.
For me it is far more obvious in the c# way given that I cannot see why I should pass the type of an argument in a generic method.
For the streams it is honestly ridiculous.
I want to apply a filter, a map, a reduce or wathever to an Iterable.
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.
For the other streams I still can't see how it is obvious that I should do manually all that wrapping that is completely counterintuitive and makes you lose track of the code purpose.
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.
I've got plenty of dislike for type erasure, and I do agree that it makes the language more complicated. An instance where the Java language was sacrificed to ease virtual machine implementation and backwards compatibility.
> 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:
> 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.
Your API should strive to be clever (it should allow end users to do countless things straightforwardly without error). Your implementation should not.
i've often complained about "clever" code. simple code is easier to maintain, and clever tricks are difficult to read...
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.
When "too clever" is "I don't understand", yeah, it's bad. I'd snap back with "let's use clojurescript then". Or I'll clarify whether they think 'var i' in the loop is too clever or the scope fixing code that's pretty common to do in JS is too clever.
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 specify either 3, or 1023 for the number needed to print "fizz."
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..]
So long as the "cleverness" is well-documented it's not a problem. Authors should explain why it is written the way it is. Problem solved.
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..
OTOH comments easily become outdated, and it's painfully easy to make a change that subtly invalidates a comment in another file. They're like lines of code that is never tested or run.
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.
I do not see this as an issue if the comment immediately Precedes the "Clever" section of code.
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.
True--sometimes when translating from a mathematical context to programming causes some readability pains! Especially if it's something that takes advantage of multiprocessing to solve the problem.
It's really a balancing act. Gotta have the right amount of comments. In general I prefer code that is written such that comments are not necessary, but in the context here the "clever" tricks should always be commented. And to that end, the code should be peer-reviewed and determined by other individuals whether it's confusing or not.
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.
As a company we use C# and Visual Studio as an IDE.
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.
Definitely agree - my pet peeve (although I haven't seen it for a while) is introducting circular references by adding compiled dlls from /bin/debug as references instead of adding a project reference.
I use to see it a lot a few years ago, maybe I just got a little better at my job?
I understand what they're getting at, but I think the final listing is even more "clever" than the first. The first one I could read and understand pretty easily, and I've never written Haskell. The few things it does, and how, are easy to spot. The final one is harder to understand at a glance, even though it's more generalized. Some people take it even further than that, to where the implementation is totally abstracted from it's only uses.
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....
I think this is a very insightful article, but I agree with you. I thought the first listing was very clear and immediately comprehensible. Granted, I do have a small amount of experience with Haskell so I didn't have to think about the syntax at all or about `cycle`, etc.
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.
The FizzBuzz example was just to underline how a "considered clever" solution for an academic problem behave when moved into "real life" situations. If we are taught to solve and think problems as if we were in an idealized world we are going to suffer once we enter the battlefield.
Code can be clever to allow (significantly more) other code to be simple.
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.
I wonder how common the use of "ain't" is today.
I grew up in the rural US in the 1980s, and it was common. And today I don't hear it, but I live in a more... well, educted area. It throws me off when I see it in writing now.
I think the real problem with the fizzbuzz example is that the "clever" solution fails to properly break the problem down. Fizz buzz essentially consists of two parts:
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.
This article hinges on a rather silly fallacy. Luckily the author does us the favor of making his mistake very clear in the curve-fitting analogy; but first look at the code.
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 honestly didn't see the same thing as you, my understanding is that using the Lagrange Interpolation is the "clever" choice, but when you add more points, you'll end up with a higher order polynomial and widely different curve which is probably not what you wanted in the beginning.
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.
As a physics grad that curve fitting section was hard to read. Only very low degree polynomial fits can ever be used, through some coincidence with the underlying trend, for extrapolation. A polynomial fit is a simple and easy method to fit a curve which you can use for interpolation if you want. Unless you have some clearly identifiable trend, then extrapolation doesn't mean anything no matter what curve fitting method you use.
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.
So I think what the article asks for (i.e. to bring some more objectivity into the "alchemy" of code preference) is a great ask. However I agree with you however that his examples feel cherry-picked (and I sense an annoyance at FP). I suppose the most valid case would be to both real world problems and real-world change-requests to those problems.
But with mathematics, nitpicking is ~kind of~ the point. For example when dealing with real numbers, you can get away with defining Compact sets as bounded and closed sets; however when you enter a different Topological space, that definition will surely fall apart. So in retrospect, defining compact as bounded and closed was working in your limited test cases (real numbers) but in the real world you really have to embrace that it means there exists an open cover that a finite subcover covers the set. It is not pretty or clever as the limited case, but it is what you need to do if you want applicable flexibility.
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.
Software design is often about finding the correct balance between the available implementation methods. Trading memory for time (low-mem/slow recalculation vs higher-mem/fast precomputed-table) is the usual example.
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.
I remember the days of seeing inline assembly in code (delphi). We griped about that code quite a bit, but this was in the days before "you can't outsmart the compiler". Was it clever: yes. Was it maintainable: no. Was it necessary: YES! This was drawing code, doing it via the normal language routines was just not a performant solution.
There is an interesting genre of complexity that is justified (by its creators) on the grounds of simplicity. One sub-genre makes reducing the line count the primary measure of simplicity. Another sub-genre aims for the most generic and flexible code in order to simplify reuse and modification, regardless of the chances those capabilities will ever be used, or whether several different specific programs would be the simpler, faster and cheaper solution.
Hmm, it may not be very helpful, but there's a replicate function[1] right in the Prelude. I always thought that being clever in Haskell is to be able to find all the strange but powerful packages and functions and weave them in as tight a program as possible. Having to implement replicate by hand is definitely not clever from the start.
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...
103 comments
[ 5.1 ms ] story [ 185 ms ] threadhttps://gist.github.com/anonymous/7a1829f61cf9302c188f80a736...
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.
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.
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.
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 :)
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:
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.
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.
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'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. :(
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.
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.
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?
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.
For example, we are using Higher Order Messaging[1] to turn this:
into this: 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.
[1] https://en.wikipedia.org/wiki/Higher_order_message
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.
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
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.
[1] http://thedailywtf.com/articles/Soft_Coding
[2] http://c2.com/cgi/wiki?ExpressionProblem
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...