Functional Programming Isn't the Answer (2014)
degoes.net
degoes.net
“Look at my intentionally non-thread-safe, spaghetti imperative implementation. It sucks! So FP sucks.”
The author used that example to demonstrate that just using FP languages is not enough to ensure good code, because it is possible to write bad code in FP languages.
Therefore, FP programming languages are not going to guarantee you good code by themselves. They will only get you good code if you use them to write good code. By the way, the author never said "FP sucks," and that's a very uncharitable and unreasonable interpretation of the argument.
From the article:
> My view is that although FP makes it easier to write good code, just because something’s functional, or even “purely functional”, doesn’t necessarily mean it’s any good. Stated differently, as software engineers trying to improve our craft, we shouldn’t latch onto or defend something just because it’s “functional” or “purely functional”. Although it’s possible to write good code using techniques from functional programming, it’s also possible to write bad code. FP doesn’t protect us. We need a different measure for “good code” than just “functional”.
You may think that "FP guarantees good code" is a strawman argument, but it isn't. A quick search of this site will turn up people who make that very argument. It's useful to have a realistic corrective to the pseudo-religious fervor of some FP proponents, and this article provides that.
For example, you can write a beautiful algorithm that solves the wrong problem. No language will help you with that.
Or you can write a program in language X that ignores every convention or practice -- you can write code that makes an extraordinary effort at subverting the type system, for example -- but that's more of an argument against your understanding of X.
I do agree with the author that FP, good as it is, is not enough. That's not an argument against FP, it's an argument in favor of devising even better tools, practices and the theoretical underpinnings for building correct software.
People do say that, so it's not a strawman.
> I do agree with the author that FP, good as it is, is not enough. That's not an argument against FP, it's an argument in favor of devising even better tools, practices and the theoretical underpinnings for building correct software.
The article was not an argument against FP. Why do you think it is? It is simply an argument against a certain justification for using FP, namely that it guarantees good code.
Who says this? No-one who is a serious practitioner of FP (as opposed to someone who just reads about it in blogs) will tell you that FP automatically guarantees good code. You'll hear that FP goes a long way at reducing some common sources of errors, which is a pretty big deal, but automatically correct code? No-one will claim that.
> The article was not an argument against FP.
I didn't say the article was. Besides the clickbait title, and some dubious assertions (and a poor example with QuickSort), I do agree with the overall assertion that "FP is good, but not enough".
No true scotsman fallacy.
Therefore, in order to have a rational discussion about this, we must apply some sort of filter.
I propose this filter: no-one who is an actual practitioner of FP (someone who uses it to build software, not someone who just talks about it in the internet) will not make the claim you mentioned, i.e. "FP guarantees good code" (I'm quoting you verbatim).
I hope it stands to reason that it doesn't matter what people who've never used FP claim about FP.
> I hope it stands to reason that it doesn't matter what people who've never used FP claim about FP.
That doesn't matter. An argument is not a strawman if it has been made by anyone, anywhere, anytime, for any reason. You can't just filter out the people you think don't count.
An argument is only a strawman if it is a false argument, e.g. one that has been invented by the person who is arguing against it.
If you want to have a discussion about the merits of FP, feel free to filter out whoever you like. But you said the argument mentioned in the article is a strawman, and it isn't.
It seems there's a phase that some FP programmers go through where they have a zealotry that requires convincing the masses of the glorious wonders of FP. For the most part, they seem to either outgrow the "FP is the One True Way" zealotry, or at least learn to keep it to themselves for the most part.
In fairness, I'd say that I've seen it on HN less in the last year or so than I did before that.
Can you provide an example of someone who claims "FP guarantees good code"?
At least implying that? Probably not at the moment, given how many different ways it can be stated, and given that I've forgotten what the search engine for HN is. (There is one, but I failed to bookmark it when it was called to my attention.)
But I tell you that here on HN, I personally have argued against views that in my view are at least similar to that. Similar enough that you'll think they are the same? I can't answer that, partly because of my own forgetfulness of the exact details, partly because I don't know what you'd consider similar enough to count.
I don't think I am. That code deliberately uses Haskell’s low-level features. It’s like saying, “hey, high-level languages don‘t even save you from segfaults! Just look at my purposely crafted low-level code in a high-level (but not dead, so it has low-level interface) language that access memory at 0.“
The author argues that using FP does not guarantee good code.
All that is necessary to prove that is to demonstrate bad FP code, which he has done. Only a single example is required to disprove a universal assertion.
> It’s like saying, “hey, high-level languages don‘t even save you from segfaults! Just look at my purposely crafted low-level code in a high-level (but not dead, so it has low-level interface) language that access memory at 0.“
If someone argued that high level languages save you from segfaults, your statement would be a valid argument against that, just as the article has provided a valid argument against a particular belief about FP.
We should make an unemployable list and put these people on it.
His point was that writing in an FP language doesn't mean that you're not writing trash. An FP language (or even an FP orientation/mindset) isn't enough. It may be helpful, but it's not enough. Haskell won't save you, and neither will using higher-order functions.
His later discussion of Free monad is pretty interesting: http://degoes.net/articles/modern-fp
It's like science research: The point of individual studies or here, articles about something, isn't to finish the topic once and for all in one stroke. The criticism of either (studies or articles) often seems to be based on attempts to find what a study/article does not provide instead of looking at what it does. Obviously, the stuff not in it far exceeds the stuff that is, so I see a lot of such criticism missing the mark.
One interesting note is that the quicksort code in Haskel illustrates a very important point about code evolution. Suppose you wanted to avoid the worst case quadratic behavior. There is not a good evolution strategy that goes through median of 3 pivot, median of 3 pivot with randomization, and introsort. With the equivalent imperative code, the evolution strategy is pretty easy by just adding a few if statements.
There are people who criticize code that isn't concise enough, even if being concise would make it harder to comprehend. Using lots of magic or overly-terse languages are also valued by some people and not by others.
Composability is also something that is totally alien to large sections of the OOP community. If you've ever dealt with a legacy PHP or Java framework, you'll know what I mean. They heavily use inheritance over composability.
That said, I tend to prefer functional patterns over OO... there are times where OO/classes as a syntax expression is easier, I would say in most cases, it isn't. It really depends on what you're trying to create, where and how.
Function Programming is fun (mostly ;))
and i think even in corporate setting that is a valid argument. It may not the the most important, but it is one nonetheless. Happy developers are more productive in my experience.
whileM ((<=) <$> readLeftIdx <*> readRightIdx) $ do
And, from what I've seen in a few Haskell codebases, this isn't all that uncommon. I refuse to believe that there's not a better way to express these kinds of programming patterns. I recognize that applicatives, functors, etc. have their value in creating well-engineered interfaces, but when they manifest themselves in obscure operators, it feels self-defeating.Every competent Haskell programmer will be intimately familiar with those three operators.
The problem arises when new libraries introduce new infix operators. I find this highly annoying and almost always consider it a code-smell. So honestly we're in agreement in general, just not with that particular example. I consider those to be basically the "arithmetic operations" of function application. Considering the entire language is based on function composition and application, I don't think it's a bad thing for an economical notation to be used.
But, I agree with you that there are obscure operators scattered around the code base and I have to keep jumping away from that point to lookup what the particular operator means in that context.
You don't like punctuation in your identifiers. It's a a fair objection, and a common one for people whose native language uses a small alphabet (as opposed to CJK and other languages that are ccustomed to using wide range of glyphs)
cond = do
li <- readLeftIdx
ri <- readRightIdx
return (li <= ri)
-- ...
whileM cond $ do ...
Personally, I don't think it adds much clarity. It just makes the code more verbose.Maybe that's not a reason to avoid the more concise syntax, but a language that is more comprehensible and intuitive to people unfamiliar with it is more likely to capture mind-share.
(0) Type signatures (obviously).
(1) Abstraction boundaries - which implementation details are not exposed outside of a module, class, whatever.
(2) Documented preconditions, postconditions, invariants and algebraic laws.
(3) In impure languages, the scope of mutable variables and effectful procedures.
> My point is that it's easier to evangelize for a language that is intuitive and comprehensible with only limited exposure to the language.
I have to admit I don't care much for technological evangelism. I prefer to let technical merits speak for themselves. To be more precise, I'm okay with others showing me technologies I might like (“good” advertisement), but I'm not okay with others trying to convince me to like whatever they like (“bad” advertisement).
> If you've ever done any programming, you can read go code and know what it does.
That hasn't been my experience, and I've honestly tried. If you show me a heavily concurrent Go program, I can't easily tell on a first read whether the program is correct, or, if it's incorrect, where the problem might be. Just because I can run a program (in the sense of myself acting as the interpreter), it doesn't mean that I actually understand its design. :-p
> I'd say Elixir, a functional programming language, is also good in this regard.
I haven't actually tried it, but the fact it's designed to be similar to Ruby doesn't inspire much confidence on me. I don't mind Ruby's syntax, but the impossibility to make sense of Ruby code other than by trial and error is incredibly frustrating.
I suggest the following plausible points:
1. Nobody is born knowing a programming language. Languages have users to the degree that they convert non-users into users.
2. Sometimes people are forced to use a language for a job (which is, I suspect, somewhat common, but less common than hiring someone who already knows the desired language). Others are forced to learn it to pass a course. Other than those cases, though, languages convert non-users into users in proportion to how much a non-user gets rewarded for the first attempts to write code in that language. This was one of the virtues of BASIC, for example. I picked up BASIC from the TRS-80 manual in an hour, and was writing code that afternoon. I got immediate feedback (we didn't call it a REPL on BASIC, but it had one). You could just read BASIC code.
But if I have to learn a mathematical notation, or a 1000-page book, in order to be able to do anything that I want to do, I have to want to learn that language more before I'm going to bother to learn that language.
Other than university people, Haskell seems to attract people who have been around the block enough times to know why they might want to learn FP, and therefore who care enough to make the effort.
The other version, however, is more readable to anyone who isn't a software developer by profession, but has been exposed to at least some code or even pseudo-code. It's the difference between turning people off of programming versus keeping them interested.
It's not a tautology. Especially in a language like Haskell, where it's very easy to write incomprehensible programs, even to experts, by following “common sense” principles.
> The other version, however, is more readable to anyone who isn't a software developer by profession, but has been exposed to at least some code or even pseudo-code. It's the difference between turning people off of programming versus keeping them interested.
I don't want to turn anyone off, but I also don't want to make an effort to accommodate people who aren't willing to learn. If other people like programming for what it is, that's great! If they don't, well, that's okay too. To each their own.
If you had never seen +,-,%,• before, simple math statements would be inscrutable. But, once you take the (short) time to learn them you can express things much better than if you had to write `add`, `subtract`, etc everywhere.
A point that is particularly poignant to anyone who has ever dealt with code using custom numeric types in a language that doesn't let you define custom operators (or overload operators).
Absolutely, anyone with programming experience can make sense of the longer (and still quite terse) version.
whileM ((<=) <$> readLeftIdx <*> readRightIdx) $ do
is literally symbolic soup without knowing the context and language in which it is written. For example `(<=)` could be less than or equal, but then again, given all of the other operators, who knows, might pipe output into whatever's calling `whileM`.Is that true? I'm skeptical.
Why is the "<-" symbol clearer to you? In particular, why did the commenter above write
li <- readLeftIdx
instead of let li = readLeftIdx
?I don't think anyone with programming experience could correctly answer this question unless they also have some specific Haskell experience.
What exactly does "return" do? I don't think someone without Haskell experience has any chance of answering that correctly, because it does not have the same meaning that it does in most languages. You might be able to make a reasonable guess, but you're unlikely to actually get it right.
I think once you're at the point that you can completely understand and answer these questions, the odds are high that you'll appreciate a more economical syntax for these operations.
As for return, even if the semantics of return are different in Haskell the keyword itself is quite common in programming languages. Therefore, given that it's the last statement one can assume that it returns the result of `(li <= ri)`.
If you know Haskell then the one liner is likely obvious. If not, not at all, there's a lot of domain specific knowledge required to grok what the one liner actually does. The long form version at least provides some guide posts to work off of.
No, that's not what it's doing. It's not imperative assignment. It's not even a `let` binding. Against popular belief, you actually need to learn a programming language to make sense of code written in it.
> If you know Haskell then the one liner is likely obvious. If not, not at all, there's a lot of domain specific knowledge required to grok what the one liner actually does.
s/Haskell/C/, s/one/hundred/g
The point is that everyone can take a guess as to what's happening in the long form version.
What about monadic actions that may return less or more than one value? For instance, parsers for languages with non-deterministic grammars, that return all valid parse trees, rather than just the zeroth one.
> The point is that everyone can take a guess as to what's happening in the long form version.
And that guess would often be wrong.
At the same time, I think it's less valuable than it might seem. A vague, cursory understanding is enough in this case to get a gist of what's happening, but it's not enough to actually work with the code, except via luck. The intuition you describe works okay in this case, but it would fail somewhat spectacularly in other cases, for instance with the non-determinism of the list monad.
"Premature optimization is the root of all evil" and all that. But, a sorting routine is something everyone would expect to be as efficient as possible, right? (Maybe it was just for the sake of the example?)
I heard 1 or 2 things about Haskell being lazy, etc. Does it makes the second version less inefficient than it looks?
System maintainability and comprehensibility owe more to architecture than to local readability. It doesn't matter if local code is completely functional, or if Scala secretly uses imperative algorithms underneath. What matters is if the code module / division / unit is keeping its contract or API with respect to the rest of the system, because if it is, then you can actually hope to comprehend your system at a useful level of abstraction. Local vs system comprehensibility are not in tension, but an unfair amount of focus has been placed on local clarity.
So a question I'd have is what architectural solutions to some problem domain are natural and special to the functional style. To that end, it's more interesting to see a skeleton project tackling a generic problem, as an inspiration for a generic architecture, than to talk about whether it's possible to write locally bad code with an FP language.
More seriously, `>=` is not the correct negation of `<` due to NaNs. The code as written will return a list with all NaNs removed, because NaNs are neither greater than nor less than any other number.
It's interesting that an in-place sort that uses only comparisons and swaps cannot have bugs like duplicated or removed elements, regardless of any bugs in the algorithm or comparison predicate. The "shuffle via sort by random" approach [1] is a nicely insane test case.
Use the tool where it shines. Here is a doc that I have bookmarked for Haskell: https://github.com/Gabriel439/post-rfc/blob/master/sotu.md
> I don't understand this post.
The point is, as the author states quite clearly, that it's not enough to say "I wrote it in FP"."Writing it in a functional language means it's FP" might be a bit more of a strawman, however.
I doubt Degoes would appreciate the phrase "religion" used as a disparagement to defend his work by the way. He's shown he's willing to take a lot of flack to defend religious people's dignity and needs.
> Some people have fallen so much in love with FP (it’s hard not to!) that their views are almost religious. Hence, what I refer to as the religion of FP. The problem with any religion is that dogmatism can be blinding.
I suspect that you didn't read the article, or at least merely skimmed it.
Degoes fought bitterly for Lambdaconf to support people who hold unpopular and racist views, often citing the social ostracism of the religious members of the community as his reasoning.
But of course he casually dumps on them in this article and equates "religion" with a lack of logic. So really it was about supporting his DE chums and not his poor religious friends.
But that's not really the topic of this discussion.
Yes, bad code is possible in any language or paradigm. We don't judge languages by their worst examples. We never have expected FP to magically solve all problems. Heck, this disclaimer exists in a slightly different form even in the Backus paper!
I don't understand the point of this article either.
What distinguishes a credible FP proponent from a non-credible one? This is getting close to the territory of the no true scotsman fallacy.
If someone makes the argument, it's not a strawman. It doesn't matter if you consider them credible or not. The author of the article responded to a real argument made by real people.
Who specifically is he arguing against? You cite "no true Scotsman" but we lack even a false one to point to.
> Find me a credible FP proponent that would disagree with that point?
By including the word "credible," you are pre-emptively excluding some FP proponents as not being "true" FP proponents. So you've already committed yourself to applying the fallacy to evidence that anyone might present to you.
But this pre-emptive strike thing is just a way to avoid providing any sort of example, I suspect.
I think the problem is dysfunctional workplaces in general -- things like open plan office dogma, "full stack" destroying the value held in specialization of labor and compartmentalization, everyone trying to data mine "insights" that are really serving political agendas instead of customers, users, clients, or really even shareholders, and so on.
Writing good software systems inside a bureaucratic system constantly throwing wrenches into the gears is really hard, and just changing the language paradigm, even if the paradigm comes with really great tools for correctness and quality enforcement, generally isn't enough to overcome badly structured, uncreative, un-turn-off-ably oversaturated with "collaboration" working environments.
I love FP and hope I get to use it more in the future. I also love simpler OO or even module-oriented programming with lots of mutation. You can write great software in any of these paradigms, and you can have great teams.
But I think first we all have to unite and utterly refuse the horrible working conditions pervading the industry. Put our debates about language or tool superiority to the side and see if we can improve functionality and basic humanity-affirming healthiness of workplaces and team structures.
Having good tools does not make you a good craftsman, I'm not sure this is contentious.
I don't think that anyone would argue that (good tools => good craftsman) is valid. This isn't an argument against FP, just the statement that FP cannot transmute a bad programmer into a good one.
[1] https://ericlippert.com/2012/10/15/is-c-a-strongly-typed-or-...
(0) Standard ML: Yes.
(1) Java: No, but dynamic checks are used to patch the type soundness holes.
(2) C: No, and if you do anything wrong, may God help you.
If you forbid casts or reflection, then you're throwing away much of the Java ecosystem.
That's not true. There are two fundamental differences:
(0) Options can nest. Nullability doesn't nest.
(1) Options are manipulated by pattern matching (or direct application of their induction principle, that is, Haskell's `maybe` function). In Java, you just assume a nullable variable isn't null, and if it is, well, you get an exception.
This blog post (I'm not the author) explains the issue (item 3) very well: http://www.sebastiansylvan.com/post/language-design-deal-bre...
After thinking about it again, unless you made an error in the specification or implementation, every programming language should actually be sound. You can always weaken your guarantees until they (trivially) hold at runtime. But that actually worries me a bit, soundness suddenly seems not such a useful concept. Maybe because I am wrong with my understanding?
Note that this also possibly contradicts what I wrote previously, I may not have thought carefully enough about some of the things.
I'm using my own interpretation of “soundness = progress + preservation”, originally due to Wright and Felleisen. “Progress” means “not getting stuck” (a term is either in normal form or further reduces). “Preservation” means that, if a term `t` has type `T` and reduces to a term `u`, then `u` also has type `T`.
From a purely technical point of view, raising a NPE is a well-defined execution step, different from getting stuck. But I contend that this is just a technicality, and not really how a programmer thinks about his code. For practical purposes, in most situations, throwing a NPE is reaching a stuck state, and thus a violation of type safety.
That's technically correct. But it's like the joke about the mathematician who can only provide technically correct but otherwise totally useless answers.
> But that actually worries me a bit, soundness suddenly seems not such a useful concept.
Indeed, if you define soundness that way, it's a useless concept. That's why I refuse to accept a semantics that doesn't match how programmers actually think about their programs. I consider it undefined behavior to dereference a null pointer: the language implementor might trap the error to make debugging easier, purely out of goodness in his heart, but the program itself remains meaningless until it doesn't dereference null pointers anymore.
> Maybe because I am wrong with my understanding?
Nope, you're not wrong.