Hacker News new | past | comments | ask | show | jobs | submit login
Pissed off about functional programming (2005) (perlmonks.org)
102 points by yiransheng on Aug 6, 2014 | hide | past | favorite | 61 comments



Maybe I'm missing something, but points 3 and 4 don't make much sense to me. Could someone tell me why I'm wrong here?

In myth 3 he seems to mix words of description and the words describing logical equivalence. "even though equals('three',3) is true, length('three') does not equal length(3)" - while this is correct, it only seems to say anything about the myth because of the function name. Try this instead "even though foobar('three',3) is true, length('three') does not equal length(3)" - does this really prove anything? You could never substitute "three" with 3 at any point in the first place.

Specifically he hid a "$lut{ $str }" in equals(). It's not that 3 can be substituted with "three" - it can be substituted with "$lut{"three"}".

In myth 4 he seems to play a similar trick of talking about the variable names. Sure, perl allows you to create a new block with a new variable of the same name. I'm not sure what does that have to do with functional programming as a whole. It's just the language implementation that allowed you to play this trick - referring to a new thing by a name you used before.


>> I just had an long and very frustrating conversation with a young programmer who recently discovered functional programming, and thinks it can solve every problem in the world.

I'm struck by A) the arrogance of such a statement, and B) how completely unsurprised I am by it. The fact that the rest of the article is nitpicky stuff of no real consequence, with a heaping helping of strawmen and goal-post moving, only supports this. This is a person who fears being wrong and is defending their turf.

For the proverbial "you", because this is an endemic problem in our industry:

It's not necessary to defend turf, both if you are right and if you are wrong. Obviously, if you are wrong, it saves you time to have someone else figure it out for you and then you can adjust accordingly (you're going to adjust once proven wrong, right?).

But if, in some weird happenstance that has yet to be demonstrated in history, you're right, you have expended no effort to defend your position. Sometimes, you have to let people learn their lessons the hard way. Let the youngsters run in their enthusiasm and trip and fall on their face sometimes. It's the only way to build a healthy fear of novelty-for-the-sake-of-novelty, and instill some critical thinking as a matter of course.

It's not necessary to be "right" all the time. Just because there are people in the world who are "Doing it Right" versus "Doing it Wrong", doesn't mean you have an ordained duty to inform them.

Because I'm really getting sick and tired of being asked "why didn't you use <whatever I like> for that <whatever you wrote>?"


I assume this post is here to show us that the stupid argumentation about FP vs OOP vs DWIM have been going in circles forever and we should just stop and get back to work.


I was looking for some technical discussion to half read through and procrastinate, but I found wisdom instead.


Yep, there is no need to choose. I use both.


I think Odersky said something along the lines of "mutate with caution". Sometimes it's just the best way to do things, but with caution.


While knowledgable sounding... I kind of feel like he was missing out on what pure FP can and does achieve. There were many statements to the effect of "don't pretend X is Y because while Y is obviously great, it's also true that any lang which does Y could not operate (here's an example in Perl)".

I reject that last bit, that these things Y lead to a useless language though I do think the proof was far from the pudding 9 years ago.


"I kind of feel like he was missing out on what pure FP can and does achieve."

So what exactly was he missing? When I was programming in FP languages, the process didn't seem all that different from other languages, except generally more cumbersome.


"When I was programming in FP languages, the process didn't seem all that different from other languages"

Isn't there a rather famous phrase, 'You can write Fortran in any language'?

I kinda skimmed over the OP, admittedly, and the lambda the ultimate responses; I find myself far more on the side of the lambda the ultimate responses (that this guy is largely strawmanning/misunderstanding FP), but that's still immaterial to me, as I'm not really interested in academic arguments.

I've been programming in Erlang (which though not lazy nor pure still embraces an FP approach), and I've found that after embracing the languages idioms, structuring my code and my thinking to take advantage of the language, I'm far more productive than I was in any imperative language (and became so in less time than I had spent in those imperative languages). And things like concurrency, while still complex, have become comparatively trivial than the crap I was having to deal with in those imperative languages.

Now, that may be a combination of other factors (a REPL, more concise language, fewer teammates writing better code), and it's anecdotal, same as your response, but ultimately I think personal experience is going to trump academic argument every time.

No matter how much someone extols the benefits of FP, if you don't experience them you won't be swayed, and similarly no matter how academic an argument against FP someone tries to make, having experienced benefit from using a functional language, academic arguments are of little interest to me, and I'm curious to try more functional languages.

All I'd say is, to your experience, examine whether you were simply trying to bend the language to fit your existing models and approaches, or if you were willing and able to replace your models and approaches with those the language required (i.e., in Haskell, not merely "Okay, I need to do a side effect here, so time to write 'do'", but "Hmm, I have a side effect here...is this the right place for it? What other side effects do I have? Can I minimize where they're being called, so I have comparatively few functions that require the io monad?" etc). If it was the latter, fair enough. My experience was different; both are valid.


"except generally more cumbersome".

So true. Especially if the domain is not "naturally" functional - say an HTML5 game. I am sure brilliant programmers will come up with a functional model even in such cases, but I don't think that is how most people would think. I am more fond of languages like JavaScript, which has features associated with functional programming (higher-order functions, closures) but feels as natural to me as C.


You should look into http://elm-lang.org/. A lot of the examples are specifically for web games. I haven't used it myself, but from research I did into it months ago, I remain very intrigued.


He simply made a lot of assertions that certain styles of programming are impossible. That assertion feels great if you (a) don't actually know the techniques of achieving some of the benefits outlined and (b) try to program in a language that doesn't support them as well in a style which doesn't work well with them.

In some sense, despite the author's assertion that FP is mind-expanding and a great learning experience, I believe he still had a lot to learn. Mostly that most of these techniques and benefits are workable, but they take a genuine change in perspective (such as outlawing ambient state entirely).


Well, at least he gave some concrete examples. I was hoping for some concrete examples of what he was missing from you, because I am curious.

My own exposure didn't show me that much that was new/different in ways that were practically useful (and no, it wasn't writing FORTRAN or BASIC or Pascal in a different language).

Mind you, I have, for example used (mostly) immutable data structures and generally write in what many would probably consider a somewhat functional style. I also like HO mechanisms, but all of these are not at all exclusive to FP languages.

So as I wrote, I am looking for concrete examples.


Myth 1: if you restrict variables to be references alone and no longer mutable slots then this argument vanishes. Additionally, these kinds of computations do not need to take forever. Case in point:

    fix f = f (fix f)  -- ought to take forever, right?
    fact' rec n = if n == 0 then 1 else n * rec (n-1)
Here `fix` appears to set up an infinite computation, but applying it as `fix fact'` produces a terminating function, the factorial.

Finally, simultaneity in LC doesn't much hold you back from using concurrency because you have two avenues: (1) model your concurrency abstractly within LC and then execute it specially and (2) have concurrency applied implicitly with optional hinting.

Both of these are a little non-obvious, but extremely workable.

Myth 2: Summarized as: Turing Completeness holds, perhaps? It seems to be attacking a certain strawman, but in doing so depends a lot upon some definition of functional as being a totally different form of computation. It's hard to even concretely respond to this abstract notion, but I'll try with two points

1. LC and TMs are not equivalent on higher order computation. While any function Int -> Int can be equally represented in each, functions like (In -> Int) -> Int are different. In LC you cannot examine the input function (with tradeoffs and benefits) and in TM you can (with tradeoffs and benefits). That's totally theoretical, though since you can model higher order computation as a function Int -> Int for most practical purposes.

2. Turing Completeness is not an end goal. Some languages today even challenge whether you want TC to hold all of the time (or only sometimes) and push right up the border of TC from below. I think they provide example that taking TC as your ultimate arbiter of language comparison is shortsighted.

Myth 3: Referentially transparent is not a property of a language or of a (poorly defined) style of language. It's a property of an analysis of a language, the language's definition/semantics/statics. I've discussed this on this page in another comment concerning C.

The short of it is, however, that RT depends upon what you're willing to analyze as a frame of reference and for any language you can pick choices of frames which provide RT or those which break it.

The author's choice of mechanism to demonstrate RT breakage is pretending like variables in Perl are references and then showing that mutability and dynamic scoping breaks them. Then he even tries to pretend like universal use of dictionaries models reference and breaks that.

These are strawman arguments, deliberately pushing models outside of their theoretical value and then showing that they no longer have theoretical value.

Ultimately, this culminates in an unjustified argument that "referential transparency isn't all that desirable" due to "incredibly tight constraints" being untenable.

As a concrete counterexample, I give you pretty much the semantics of Haskell which has a ton of practical code written in it and referentially transparent variable semantics by default. It goes much further than his ideas do and achieves it in such a way where the programmer does not have the ability to break the model.

(For instance, if Haskell let you modify its own running memory and fiddle around with arguments on the stack then you'd certainly be able to use those facilities to break RT... but you can't.)

Myth 4: This is total strawman—he asserts that FP does not allow "assignment", defines assignment as he chooses, and then shows that his own model of assignment can be modeled without assignment. He does this by embedding an imperative language in a "functional" style in Perl and then observing the effects of running that embedded language.

Then he shows... something utterly unrelated to referential transparency but claims that it's the same. Since you asked for concrete, here's his example in Haskell

    data Op = Inc | Dec | Zero

    counter :: Int -> [Op] -> [Int]
    counter n []       = []
    counter n (op:ops) = case op of
      Inc  -> n + 1 : counter (n+1) ops
      Dec  -> n - 1 : counter (n-1) ops
      Zero -> 0     : counter 0     ops

    -- modified to return only the last value
    counter' n ops = last (counter n ops)

    x = [Inc, Inc, Inc]
    y = [Inc, Inc, Inc]

    test1 = counter' x == counter' x
    test2 = counter' x == counter' y
    test3 = counter' x == counter' (x ++ [Inc]) -- what?
---

Ultimately, while he does spend a little time talking about how reference works—and he's not completely wrong in his definitions—his application of this idea to "FP" (whatever he defines it as) is full of logical fallacies, unbacked assertions of impossibility, and... ultimately just totally flawed arguments.

It turns out you can have much of what he's asking for—even in Perl if you like. But to do so you must intentionally restrict yourself from using certain parts of your language. If you move the goal posts on your restriction over and over then, yes, you can discover a lot of weird counterexamples to your own broken models.


That it's possible to write nontrivial programs using immutable data (see Clojure in particular), which makes debugging highly concurrent applications much much easier. That it's possible to achieve precise control over effects with only a moderate amount of overhead. Look at the perennial debate in Ruby-land over whether you should test at the unit or the integration level. In Haskell that's not a choice, it's clear from the very type of a function whether it's a pure function or a function that makes use of context. And when debugging this lets you rule out whole swathes of your program as areas that couldn't possibly be causing the problem (contrast e.g. in Java I've spent days tearing my hair out only to discover that a problem was caused by a bug in a getter method - by convention getters are short and simple and can't possibly go wrong, but there's nothing enforcing that at the compiler level the way there is in Haskell).


Concurrent access of immutable data is super easy since....no real concurrency is going on. But what about when I need concurrency? One can manage the time with which updates are seen (so it is deterministic), but there are many ways to accomplish this beyond one mutable reference to an immutable world style that clojure seems to promote.


I'm sympathetic to that viewpoint; I prefer the Haskell style where some things are mutating and some things are not and you can see which and control when they happen. But I've seen the Clojure style used effectively, building real-world webapps, and I'd take either over the imperative-language style where your tools give you no help and it's up to you to keep track of when mutations can and can't happen.



Interesting. It sounds like you sometimes only detect incompatible updates at runtime?


Do you have some concrete concurrency use-cases in mind that you could possibly share?


Here is something I worked on recently. Let's say you could re-parse code while executing it. A lock is needed so you don't modify a statement list while executing it (you don't want execution to see the partial list). This is where you want one mutable reference to the statement list and then change it completely...so you could use an immutable list at this point. But you could just as well create a new mutable list at this point and save some cycles.


> except generally more cumbersome

you're doing it wrong.


No true Scotsman?

FP is not actually the One Right Answer for every problem.


"Even a side-effecting function call in C has a well-defined "value" as a state transformer that maps states to pairs of states and values (the so-called "monad" in functional programmers' terminology). The reluctance of functional programmers to call such languages "referentially transparent" merely implies that they are reluctant to admit such complex mathematical/conceptual objects as "values". On the other hand, they seem perfectly willing to call a state transformer a "value" when it is put in their own favourite syntax and dressed up with a buzz word like "monad". I have to say that they are being entirely inconsistent, even if we grant it to them that their idea of "referential transparency" has some coherence."

http://stackoverflow.com/a/11740176


I don't think people who really understand referential transparency would claim that it's impossible to give C a referentially transparent semantics. It's pretty easy. (This essentially feels like looking at the meaning of the language from "the inside" versus "the outside" if that makes sense.)

There's a difference between being able to imagine such a semantics and having that semantics be in common and widespread use or having it be useful to explain functions of the language or common techniques.

The nice thing about, e.g., Haskell is that these referentially transparent semantic models are really just shoved into your face. Purity makes it hard to ignore referential transparency. Monads make it incredibly clear when you have regions of code which aren't referentially transparent.

So, much like the whole contentious "vacuous type system" arguments from a day ago---these analyses exist naturally for practically any language you can think of but their value varies a lot depending on whether or not they are natural.


I think this is somewhat misleading. It's easy to give a semantics for a toy C-like imperative language. As far as I know, a semantics that is mostly faithful to the C standard becomes extremely complicated and requires modeling e.g. code layout in memory. The equational reasoning you get from that kind of semantics is terribly weak: essentially, only terms that have the same byte-for-byte effect on memory are really equivalent. So I wouldn't say that exploiting "referential transparency" in Haskell is somehow more obvious, but that it's useful at all.


I think we're in agreement on content if not tone... and, honestly, I think your tone is probably closer to reasonable while mine was a bit facetious.

I said it that way to emphasize the argument that existence of a referentially transparent semantics isn't enough, though. You need extant and useful.


Very true. In fact, that's true of every language feature. Way too often, people waste time bikeshedding about how language X can do Y. But if it can't usefully do it, who cares?


The value in a monad is that it allows you to work with pure data transformations (functions), which can then be reused, composed, tested, used in isolation and then deal with the side-effect later when you really need it. If you think about the monadic type as a container, it allows you to apply transformations while not pulling the values out of that container (think data-structures). If you think about a monad as a context, it allows you to apply transformations while keeping the context (think Future/Promise).

The call of a side-effecting function of course it has a well-defined value and I don't think there's anybody knowledgeable enough in FP to deny that. Problem is - there's data missing in the representation of side-effecting operations. For example, take a statement like this:

    x = x + y
As a matter of fact, this makes no sense mathematically, because there's data missing from the above representation, as it really means this (where i, j and k are moments in time):

   x(i) = x(i - j) + y(i - k)
So time is actually an implicit parameter here that you've got no control over. The above representation makes even more sense when studying the architecture of CPUs. And as a side-effect, the old value of "x" gets lost and so if anybody else holds a reference to "x", then that reference will point to an entirely different value, an event that can happen at arbitrary points in the future, so you could say that the whole world changes after an operation like that. Since concurrency is often brought into the picture, I think it's fairly easy to see how this can create problems in terms of our capability to reason about the logic we read or write.

Also, if we think about side-effecting components (say, mutable objects), the functional behavior of such a component ends up depending on its history, history that's in no way explicit. Variables are buckets or places. Immutable values are facts that can be compared. E.g. 2 references to mutable things cannot be considered equal, unless they point to exactly the same memory location, otherwise equality (as defined in the programming languages we are using) is broken and a constant source of gotchas.

That link you posted argues that the way we are using the term "referential transparency" today for programming languages is different from its original meaning, but I'm arguing that the original meaning is not relevant for programming languages. There's no point in arguing that a side-effecting function call in C does have a well-defined "value". Yes, the output of a side-effecting function can be considered a value, but you can't treat it as a value.

Of course, we can always argue semantics.


You might as well say that any language takes as input The Universe and returns a new Universe (perhaps also adjusting for time, if the program gives output during the execution). See? Totally referentially transparent. But not interesting.

Like he says, the technique/viewpoint is already known in FP. But in certain languages you have more control over it than others.


I think his argument held more weight in 2005. Even though Haskell at that point was 10 years old I dont think it was quite the poster child of FP it is today.

It's my guess that he's talking about non-pure FPLs like Lisps and MLs, which today don't seem nearly as FP as Haskell, Idris, Agda, Coq - the langs that are now carrying the FP torch.


There it is. I noticed it was 2005 but didn't think of this. I was trying very hard to understand statements like:

>Now.. with those definitions in place, you can see how the relationship between simultaneity and lazy evaluation isn't quite as simple as it appears at first glance. It's theoretically possible that the 'f()' or 'x' might change between the lazy evaluation of step one and the lazy evaluation of step three million. Trying to prevent that is what we programmers call a 'hard' problem.

It only makes sense if you don't consider Haskell to be the most immediate example for lazy, purely functional programming.


Haskell was _absolutely_ the poster child of FP in 2005.


But Haskell_2005 presented more difficulties (and research opportunities) than Haskell_2014. It was always interesting but now it's also proven to be powerful and meaningful in a way nobody had gotten around to at that point.


I disagree; I think, at the time, functional programmers were primarily using OCaml and various lisps. This is purely anecdotal, mind you, and haskell was definitely on the FP radar big time.


I dunno. I remember it being all over LtU and most every programming language discussion was that or scheme, but always treating Haskell like it was the ideal (and it had become default on research papers instead of OCaml)


Also, I remember when this came out the first time and the discussion on proggit was replying using mainly Haskell examples.


Then he's not thinking of Haskell when talking about referential transparency and variable assignment, or the other points of his post.


Relevant discussion on LtU: http://lambda-the-ultimate.org/node/678


As a beginner with knowledge of only F# and Clojure, I feel a lot of his issues are solved with having immutable values.

> It's theoretically possible that the 'f()' or 'x' might change between the lazy evaluation of step one and the lazy evaluation of step three million. Trying to prevent that is what we programmers call a 'hard' problem.

and the examples he gives in Myth 3 & Myth 4 where he reassigns `x` to mean something else. Both of these can be avoided had x been immutable.

I agree with the conclusion in Myth 2. I didn't understand the latter half of Myth 3, so can't comment on that.

Am I missing something wrt my assumption of immutable values solving most of the issues?


Here's another myth: "Functional programming solves the concurrency problem".

http://www.infoq.com/presentations/java-performance

Scroll to 41:05


Has anyone ever seriously believed this? I guess it's just easier to coordinate resource sharing when resource sharing isn't allowed.

My favorite is "Functional programming is becoming more relevant because...multi-core" so your 100x slower functional code would get a 4x speed up and only be 25x slower?


Here's another myth, functional code is 100x slower (than what?). Here's the counter example, Microsoft Research chose the functional first language F# to build arguably the most advanced and useful quantum computer simulator in existence. http://research.microsoft.com/en-us/projects/liquid/ There's no incentive to rewrite it in C++. It already solves real quantum computing problems like molecule simulation https://channel9.msdn.com/Events/Speakers/dave-wecker


There isn't anything in this project that implies immutabilty was extensively used for shared state in this case. F# is a pragmatic language, you can do what you want.


I think it's a good step for exactly the reason you suggest. In a world where resource sharing happens uncontrollably it's maximally difficult to design good concurrency. In a world where sharing is totally eliminated, it's trivial to be concurrent but boring.

Working back toward greater sharing from the share-nothing viewpoint is a great place to throw effort.


> Has anyone ever seriously believed this?

Hickey, Datomic?


Believing you can solve the concurrency problem[1] using functional programming as your tool-shaping method of choice is different from believing functional programming solves the concurrency problem.

[1] Whatever 'the' concurrency problem may be and not implying that I agree that the Clojure people believe they can solve it or intend to solve it


To be fair, I've done a fair bit of Clojure and Ruby in my short career, and while it's technically possible to write Ruby code in a Clojure-esque style, it's very ugly and Ruby does not make it very easy. When you write in Ruby, use classes or you will have a headache on your hands!


After years of trying to force things in various languages and frameworks (like MVC ala Struts on top of WebForms), I've learned this simple thought: don't kick against the bricks; they don't care and you'll get hurt. If you're in Ruby, program idiomatic Ruby. If you're in Python, program idiomatic Python. If you're in PHP, get a new language.


And if you're in Ruby or Python or whatever, and the language is a bad fit for the problem, use a different language for that problem.


Wait, since when did functional programming imply immutability? I think fifty years of lisp would like to have a few words with this fellow.


Functional programming means working with referential transparency by definition and an expression is said to be referentially transparent if it can be changed with its value without changing the behavior of the program (i.e. you receive the same output for the same input). And that's actually the definition of functions in mathematics that children learn in school - functional programming does not refer to the "functions" that we know from regular imperative programming ;-)

This implies immutability, because pieces of code, or functions that mutate variables / objects / data-structures are NOT referentially transparent.

> I think fifty years of lisp would like to have a few words with this fellow

LISP is not really a family of FP programming languages, even though LISP in general does apply concepts from lambda calculus and does have everything needed for FP. LISP in general is more like a multipurpose swiss army knife. For example the style people use to develop in Common Lisp is often very far away from FP. There are exceptions of course, like Clojure, which comes with immutable data-structures by default, strived to make uncontrolled mutation more painful and the culture around it does encourage FP.


Functional Programming is so poorly defined.. It clearly means whatever the op appears to think it means for the context of the current discussion.


Don't all scalable modern functional languages depend on immutability? So they can move function execution to different processes/virtual machines freely. Or something.


Most modern FP languages (and quite a few non-FP languages these days as well) do immutable by default, with carefully controlled explicit mutability where necessary. It's basically the flip of older traditional languages where unless specifically stated otherwise everything was mutable. Doing immutable by default encourages you to use mutability only where you really need it. Immutable references allow all kinds of desirable compiler optimizations to be implemented which leads to better performing code with fewer bugs, but it doesn't need to be an all or nothing, you can have both mutable and immutable references in the same code, it just means the mutable parts don't benefit from the same optimizations the immutable parts do.


Most modern functional languages encourage immutability to a greater or lesser extent, but allow or even encourage workarounds. I don't think they depend on immutability but it's a bigger deal than it is in some other paradigms.


I don't quite get why the 'Substitutability' code snippet fails? Is it because the both vars 'point' to the same RAM address initialized with '3'? If that is the case, I would argue the language is somewhat broken, or at least it`s a major caveat ... Please explain?


Don't think about RAM. Instead, consider something like the mathematical expression

    x = 3 = y
thus `x` and `y` are both just new names for the same identical value `3`. Since both variables reference the same thing then we ought to be able to substitute x for y whenever we like.

Of course, in Perl `x` isn't really a variable but instead a name referencing a "slot" which can be mutated. That mutation breaks the reference and makes `x` hold more meaning than merely a "reference to 3". So then you cannot substitute it for `y` any longer.

I think his example code snippet is a little obtuse, though.


Isn't because after the increment operation, x has the value 4. Thus even if y has the value 3 and equals 3, it doesn't equal x.


The code line which 'fails' is $y == 3 , I think we are both confused ...


>It's theoretically possible that the 'f()' or 'x' might change between the lazy evaluation of step one and the lazy evaluation of step three million.

You can't re-bind values in the lambda calculus, so no, they never will change. This is why languages like haskell are immutable.

His complaint #3 also relies on the false assumption of a mutable language.

Complaint #4 was solved by monads, quite a while ago.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: