6 ms·
When Functional Programming Isn't Functional
- seanmcdirmid 9y agoIs it fair to call APL a functional programming language? Array programming seems to be its own paradigm that inspired and was inspired by what was later called FP but is far from equivalent.
- oddity 9y agoI'm not sure "functional programming language" means the same thing to any two people. Just like "object oriented programming", it once labeled what might have been a very precise idea, but has since been bundled and unbundled with any number of other concepts. Unfortunately the term has become so muddled in the common usage that if at this point, if someone wants to call APL functional, they probably can.
- bad_user 9y agoThe term only has become muddled in the minds of those that don't want to learn, but have an interest in using it for marketing purposes — just like how people tried to redefine what Open Source means. Functional Programming is programming with mathematical functions (aka pure functions). And an FP language is one that has features, but also a culture that encourage FP as the main paradigm for solving problems. Examples of FP languages: Haskell, SML, OCaml, Scala and Clojure. Common Lisp, Emacs Lisp interestingly are not. Python, Java and C# are not. JavaScript is not, although it has a cultural shift in progress with pretty good results. Go at this point is actually anti-FP, if such a thing was possible.
- platinumrad 9y agoBut if you look at the history of the term, the Lisp or APL families of languages have just as much of a right to claim "functional programming" as their own as the ML family. I say this as someone who really likes OCaml and has never gotten along very well with Lisp. Also I don't see how Go can be considered actively "anti-FP" when it supports passing closures as arguments, which many, many languages do not. I don't understand why (some) FP and Go partisans feel such a need to see one another as enemies.
- catnaroek 9y agoClosures are simply an implementation detail of first-class procedures. But functional programming, unlike object-oriented programming, isn't just about making procedures first-class. It's about using, to the greatest extent possible, procedures that evaluate functions.
- platinumrad 9y agoYes, I understand that having closures does not mean that a language supports functional programming. What I am taking issue with is the idea that Go is somehow especially "anti-FP" when an enormous number of languages have even fewer core functional programming features.
- seanmcdirmid 9y agoLisp was based on the lambda calculus, not sure if you can get more functional than that. Common Lisp was obviously more multi paradigm, having one of the most powerful object system ever. Scala is obviously also multiparadigm, though many would rather ignore that side of the language. Most languages are multi paradigm, even clojure is used with an object-like entity component system, while there are papers that push Haskell as the best language for imperative programming. The word is kind of meaningless when applied to languages but more meaningful when applied to code.
- catnaroek 9y ago> Lisp was based on the lambda calculus, not sure if you can get more functional than that. I've tried really hard (okay, for about five minutes), but couldn't find any account of the lambda calculus that includes the following features: (0) variadic argument lists, (1) object identities for absolutely everything, (2) procedures that can be manipulated in ways other than applying them to arguments. Are you really saying that all this time my understanding of the lambda calculus has been a lie?
- jcelerier 9y agoWell, pretty sure you can give a lambda denotation of those. Just start by implementing an interpreter in lamda-calculus ;p
- catnaroek 9y agoSure, but that would be an object language embedded in the lambda calculus, not the lambda calculus itself. btw, variants of the lambda calculus do exist where some values (so-called “reference cells”) are object identities. But a value cannot be a lambda abstraction and a reference cell at the same time.
- bad_user 9y agoIt doesn't matter if the language is multi-paradigm. The most influential programming style in Common Lisp is not FP and if you'll take a look at any CLisp book, you'll be hard pressed to find any FP in it. LISP might have been born of Lambda Calculus, but Common Lisp is no longer based on it. Scala might be multi paradigm, but is the only language on top of the JVM where pure FP programming is made possible by a pretty good and well maintained ecosystem of libraries, see for example: https://typelevel.org/ https://typelevel.org/ That is not true of Common Lisp.
- incadenza 9y agoGenuinely curious, how is Go more anti FP than Java or other more classic imperative languages?
- dragonwriter 9y ago> how is Go more anti FP than Java or other more classic imperative languages? Than Java or any of a number of other popular languages, because it uses a static type system without generics. You can build functional control structures up from common imperative ones as well as from an in-language base of the simpler functional ones, but without either dynamic typing or generics, this is a painful exercise in copy-and-paste coding.
- catnaroek 9y ago> Functional Programming is programming with mathematical functions (aka pure functions). Exactly. To which the following must be added: Functions are mappings from values to values. It makes no sense to try to do functional programming in a language whose semantics does not provide a rich enough supply of values. > And an FP language is one that has features, but also a culture that encourage FP as the main paradigm for solving problems. Culture matters very little. What really matters is what the language's semantics allows.
- willtim 9y ago> Culture matters very little. What really matters is what the language's semantics allows. I disagree. Java's semantics permit defining pure functions, immutable values and persistent data structures, but the "culture" is biased towards idiomatic Java not FP. The languages syntax, defaults and core libraries can make it difficult to program in an FP style. The syntax, defaults and libraries will not change while the culture persists.
- catnaroek 9y ago> Java's semantics permit defining pure functions, immutable values Only by social convention. Not enforced by the language's semantics in any meaningful way. In fact, Java does not even allow the programmer to define custom values. All values are primitives or object identities. Note that so-called “value objects” will not fix anything. A fundamental property of functions is so-called “function extensionality”. Two functions “f, g : Foo -> Bar” are equal if and only if “f(x) = g(x)” for every “x \in Foo”. In particular, this means that there should be no equality testing operator for functions, because function equality is undecidable. --- OCaml at least has bona fide values. It allows you to implement procedures that evaluate interesting functions (i.e. value-to-value mappings), even if it doesn't typefully distinguish a special class of procedures that are syntactically guaranteed to evaluate functions. F# and Clojure are lost cases, though. As for Haskell's IO and IORef (or ML's ref), there is absolutely nothing wrong with the ability to implement procedures that do other things besides evaluating functions. The problem is when you lack the ability to express interesting functions, and the root cause is the lack of a sufficiently rich universe of values. --- Your mention of purity completely misses the point. The ability to do traditional imperative programming is a feature, not a bug! The problem here is the inability to do functional programming. The reasons for this are technical, not cultural.
- chewxy 9y agoHow is Go anti FP? It's anti-generic programming and I think that's a great idea. As a side note, there are a LOT of considerations that are implicit within a language when you do functional programming. Let me explain. Let's consider a `map` function. There are at least 2 ways to write a non-generic map function: func MapSafe(f func(int) int, l []int) []int { retVal := make([]int, len(l) for i := range l { retVal[i] = f(l[i]) } return retVal } func MapClobber(f func(int) int, l []int) []int { for i := range l { l[i] = f(l[i]) } } Any good FP-er will tell you clobbering data is NOT a good idea - that functional programming is all about immutable data. Yeah, the immutable data version however, allocates memory. So you either need: a) impressive compiler building skills to reason that for some application of `map` clobbering can be done, and for others it cannot; b) failing to do that, you would need a very awesome GC that handles memory that work on a very short timescale (that is, within callframes) Rust is nice in the sense that the compiler does a LOT of the heavy lifting, but it's far from userfriendly. Requiring users to manage and be clear about memory ownership is a Hard Sell (though I will concede, a Good Idea). Haskell is another language which relies heavily on the compiler, and GHC is an amazing compiler. However, performance for Haskell isn't that great. It's on the opposite end of the scale: super user friendly, offloads a lot of work to the compiler, but suffers in performance. The problem is I don't think we're there yet. Not with Go, but with functional languages. State wrangling is still a problem. Running away from it by hiding under layers of functional language works for a small number of problems (business rules,etc) but in my view, not the majority of the problems. Programmers don't spend a lot of time on high level problems. A long time ago, there was a compiler for Haskell called JHC. It was nice because you could use it to write C, which would expose the underlying state for the programmer to modify to her heart's content. The project's been dead for 5-6 years now I think. GHC has a weird --from-c option that I've never been able to use mainly because you needed to recompile GHC from scratch, which again is something I struggle with and rapidly give up.
- platinumrad 9y agoThe APL family actually has a fairly strong claim to the term given that APL was cited as a major influence in John Backus's lecture "Can Programming Be Liberated From the von Neumann Style? A Functional Style and its Algebra of Programs".
- seanmcdirmid 9y agoAPL definitely influenced functional programming, but why would they even want to claim that word when they were already happy with array programming? What would Iverson say if he were still alive?
- platinumrad 9y agoI was thinking about FP, FL, and J when I said "APL family" but I guess they've adopted the term "function-level".
- tytytytytytytyt 9y agoHow is it not referentially transparent and how does it use mutable state?
- seanmcdirmid 9y agoAPL supports reassignment and has state based control flow.
- jcelerier 9y agoMost functional languages (ocaml, lisp, etc) support reassignment though.
- seanmcdirmid 9y agoYes, but APL was designed before the rules were written. It doesn’t have the feeling of a FP.
- tytytytytytytyt 9y agoAren't you composing a lot of functions and operators?
- dblotsky 9y agoDoes anyone else 100% disagree with the claim that this "lambda version is more concise and expresses its intent effectively"? public static List generate(int series) { return Stream.iterate(new int[]{0, 1}, s -> new int[]{s[1], s[0] + s[1]}) .limit(series) .map(n -> n[0]) .collect(toList()); } as opposed to this: public int fib(int n) { int x = 0, y = 1, z = 1; for (int i = 0; i < n; i++) { x = y; y = z; z = x + y; } return x; }
- seanmcdirmid 9y agoYa, I noticed that also. Actually, the basic recursive version would be functional and concise, why did they go with the functional origami version instead?
- hardlianotion 9y agoI think the point is with the basic tail recursive copy it is easy to blow the call stack with the number of function calls needed.
- bad_user 9y agoYes, also the author is wrong — in FP the direct equivalent of a loop are tail recursive functions (i.e. you can take any loop and transform it into a tail recursion) and not "list processing". Here's Scala: def fib(n: Int, a: Int, b: Int): Int = { if (n > 0) fib(n - 1, b, a + b) else a } Unfortunately Java cannot express self tail recursive functions and Scala too can't express mutual tail recursive functions, as that would require runtime support. But you know what? That mutable loop is totally fine, because any mutation it has is encapsulated, the function itself being pure.
- epai 9y agoJust to expand a little on the "you can take any loop and transform it into a tail recursion" (because I think it's cool), here's an example: Python iterative solution 1. def fib(n): 2. i, curr, next = 0, 0, 1 3. while i < n: 4. i += 1 5. curr, next = next, curr + next 6. return curr Python tail-recursive solution* 1. def fib(n): 2. def fib_tail(i, curr, next): #1 3. if i >= n: #2 4. return curr #3 5. return fib_tail(i+1, next, curr+next) #4 6. return fib_tail(0, 0, 1) Some explanation of the 1-1 conversion process: #1. We need a helper function, `fib_tail` to keep track of the variables we update in the while loop. (lines 4-5 in iterative) #2-3. Return `curr` when the opposite of the while condition is met. (lines 3,6 in iterative) #4. Recursively call `fib_tail`, using same update logic as while loop body when passing arguments. (line 5 in iterative) #5. Call helper function with initial values for the additional variables (line 1 in iterative) * We're going to pretend that Python supports tail recursion. It doesn't, but I figure more people are familiar with python; think of it more as pseudocode for instruction. You could easily implement the above in something like Scheme
- DanielBMarkham 9y agoWhen I use lambda expressions in an OO language, I find that the smallest logical chunk of code that can be exercised by a micro-example is larger than when I use iteration or recursion for the same solution. That's the thing I keep coming back to. FP simply allows you to do more with less code that has to be tested. I would not consider lambda programming FP, although I sort of understand what is being said. Lambda is just the tiniest beginnings of FP. When I started FP, I chose F#. I figured that way I could still write classes when I needed them. I also figured it would allow me to do things the "wrong" way and then learn by cleaning up. I found over several projects that I eventually stopped using classes altogether. There just wasn't any need for them. So much of what TDD brings to the table can be boiled down to "ways to make sure you're not shooting yourself in the foot in OOP" Sidebar: At one point, Dave (the author here) had the only TDD/Microtest framework for COBOL. It was a very cool idea. Dave's a really smart guy.
- rraghur 9y agoThese articles exhorting the benefit of using FP in the small are great, but I'd been lost forever making the leap from small (<10 line) examples vs typical apps with their CRUD behavior and in general state everywhere like a db or file system or network requests and so on. I've found this talk(https://www.destroyallsoftware.com/talks/boundaries https://www.destroyallsoftware.com/talks/boundaries) by Gary Bernhardt way more useful in terms of how to apply FP principles in the real world. The other grouse I have with posts like these are they try to over-simplify FP - "using lambdas is FP" or something like that. I think this leads to folks thinking 'hmm - I know how lambdas work... so if I use lambdas, then it's good' which is missing the point completely. I think one does need to 'grok' FP concepts completely - composition, higher order functions, tail recursion, immutabililty etc and gain an appreciation of FP vs procedural the hard way before they can hope to utilize it effectively.. I think those things help you think 'functionally' which is much more valuable in the long run rather than 'how do I bolt on FP on OOP'
- giomasce 9y agoIt seems to me that the author is confusing what the actual point of FP is. I do not think it is about writing less, more concise or more understandable code: the examples they bring are of different length mostly because they use languages with different abstraction level, which is largely independent from being FP or not. The first snippet (the assembly one) requires to use a whole line for each single sum; the second one has a built-in concept of lists and iterations on lists; the third one allows higher order functions. This gives different lengths, but the point is not FP. Neither it is true that FP guarantees more readability or understandability, which actually depends a lot on what your are used with. If you are used to imperative, FP will probably be a real pain. It is not even true that shorter functions are in general more readable the longer ones: I can immediately understand pages of code and struggle on two lines, depending on what they do and how they are written. The actual point of FP, as I see it, is that it decouples a function from the context it is executed into. A pure function is not influenced by its context (the global status) and does not influence it (it has no side effect). Actually, for a pure function the context does not exist. So if you want to read or understand it, you can ignore the context; and viceversa you can ignore the function when studying any other. The only interaction between different functions remain the explicit calls, which are easier to track than the implicit coupling given by the global context. So that is the advice from FP that I would give to every programmer: when writing a function, try to use the global context as few as possible, possibly even not at all. All the other stuff with lists, zipping and lambdas is fun and useful, but it is not the real heart of FP.
- yen223 9y ago> The actual point of FP, as I see it, is that it decouples a function from the context it is executed into. I 100% agree with this. To me, a rough measure of code simplicity is the amount of additional context you need to be able to reason about any specific function or object. Pure, statically-typed functions are close to ideal - all the context you need is encapsulated within the function definition itself. Functions in dynamically-typed languages are trickier - in addition to the function definition, you also need to know how the function was called, since there's no guarantees about the arguments that were passed in. Impure functions are the worse - if the function interacts with global state, then you'll need to be aware of every callsite that interacts with global state. Practically speaking, that means you can't reason about anything in isolation - you always need to hold the entire program in your head. Hence the whole "globals are evil" mantra. Thinking in terms of functional purity gives a convenient way to write clean code. I definitely recommend every programmer at least be familiar with the concept.