Hacker News new | ask | show | jobs
by grabcocque 3499 days ago
Ask five programmers to define functional programming, get fifteen different answers.
10 comments

If they are truly functional programmers, asking them will always return the same referentially transparent answer. Only side-effecting functions would return different answers at different times :-)
The term "functional programming" is overloaded, but I think there's a sensible way to split the term into two halves.

"Purely functional programming" is writing programs to resemble mathematical functions, with referential transparency and absence of side-effects and so forth. Also know as "what Haskell does."

"Function-oriented programming" is writing programs using functions as your main tool for abstraction, encapsulation, defining interfaces, unit of code division, etc. This part of functional programming is more typified by the Lisp family.

Most languages that are considered functional generally encourage both of these aspects, partly because they work well together. The confusion over definition comes from these two halves getting tangled, and some languages or programmers emphasizing one half over the other.

Non-functional languages that are becoming "more functional" are generally importing features from the "function-oriented" side of things, and adopting the "purely functional" aspect as a best practice convention, if at all. It's probably more accurate to say that they enable a functional style of programming, rather than that the are functional.

Actually, both of those families come from lambda calculus, except in different way. Lisp comes from untyped lambda calculus (and adds things like car, cdr and eq on top of it), while Haskell (and ML) comes primarily from typed lambda calculus.

I offer a definition of "functional programming" as "based on semantics of lambda calculus".

Lisp does not come from lambda calculus. Anonymous functions in Lisp get their LAMBDA name from lambda calculus, that's all. MacCarthy admitted that he didn't even understand lambda calculus properly, which is why early Lisp was dynamically scoped: lambdas didn't capture lexical variables. Whereas lambda calculus is lexical. Lexical scoping was adopted in later Lisp dialects and into Common Lisp, making those dialects retroactively related to lambda calculus. (Emacs Lisp shows the traditional behavior of dynamic scoping; therefore it couldn't be said to be related to lambda calculus.)

Lambda calculus is a formalization of number theory which builds up numbers from functions. An important result is that lambda calculus is Turing complete. It shows that we can boostrap numbers and number theory from almost out of nothing, using Church numerals.

Lisp has never built numbers out of Church numerals; it had ordinary integers.

Also, lambda calculus, typed or not, does not have its own syntax as a data type. It doesn't have symbols. You don't quote a symbol and pass it to a function and so on.

So that's basically it; there is a link between Lisp and lambda calculus due to the use of the word lambda in a related way, and a somewhat stronger link between lexically scoped Lisps (which came later) and lambda calculus.

I guess you're right, it doesn't come so much from lambda calculus as I claimed, it was more inspired by it. Although to be fair, in the time the Lisp appeared, it was the closest thing (by a wide margin) to lambda calculus. I think it was a valiant effort to bridge the gap in that direction (and the design choices were influenced by the trade off that he also wanted a practical programming language).

Also, even languages like Haskell are not based only on theoretical lambda calculus, but they also have primitives for data types, which could be, in theory, represented by lambda expressions.

Here are some features that the lambda calculus doesn't have: n-ary functions for n other than 1, macros (or any other means to analyze its own syntax), dynamically scoped variables, physical object identities, etc. In the untyped lambda calculus, any two alpha-equivalent terms are internally indistinguishable - in fact, you can even make them externally indistinguishable using a nameless representation of syntax like “de Bruijn indices”.

So much for “Lisp comes from [the] untyped lambda calculus”.

Your "function-oriented programming" has an older and more suitable term: https://en.wikipedia.org/wiki/Procedural_programming

You see, if a function isn't pure, then it isn't a (mathematical) function. But we tend to overload terms because of their marketability.

In the same way that some companies wanted to overload "open source". The general rule of thumb is that if something is desirable by the market, then it is going to get overloaded either by people not knowing what they are talking about or by sales people.

Procedural programming usually does not include the use of higher-order functions, and "function-oriented" here seems proper for Lisp and some styles of Python, with their heavy use of filter/map/apply/parallel-map-reduce etc.
In mathematics a function is a mapping having the property that each input is related to exactly one output. This brings with it certain properties you can rely on. In particular functions themselves are also values that can be passed around as parameters or returned by other functions, hence higher order functions.

It's regrettable that we overloaded the word "function", given we could have used procedures or subroutines to denote, you know, jumps in code that trigger effects and push/pop the call stack.

And as proof, applying filter/map and other similar operations with side-effecting procedures is a really, really bad idea, because such operators are usually implemented with lazy behavior (in order for "fusion" to happen) and laziness doesn't blend well with side effects, with the result becoming non-deterministic. E.g. at least people that worked with Map-Reduce frameworks should know what I'm talking about.

Or in other words, there is no such thing as "function-oriented", there's just local application of FP where it seems to be making sense, but with all the caveats that entails.

Seems to me that's because functional programming continues to evolve (which is great!) and newfangled properties and semantics get folded in.

To me the core is "no side effects" though (for pure FP). It's interesting to see what others consider to be the most salient feature(s).

Same thing can be said for object-oriented programming, unfortunately.
There's only one definition that matters: functional programming is programming with mathematical (pure) functions.

As a consequence you get referential transparency and thus equational reasoning. But change this definition and the term becomes meaningless.

Though by this definition, Lisp, the granddaddy of functional programming languages, is not a functional programming language.
The original LISP was influenced by Alonzo Church's lambda calculus, see: https://dl.acm.org/citation.cfm?id=367199 - however if you'll study its history the first Lisps were only experiments and for example they didn't have lexical scoping, but dynamic scoping. The Lisp descendant that made FP doable was Scheme, bringing lexical scoping and call/cc.

And today's Common Lisp is definitely not Scheme, or an FP language. You can do FP in CLisp of course, since it's quite capable, but CLisp is a general purpose language and is used as such. And in my small experience from playing with it, there isn't much FP in CLisp, but YMMV.

Lisp in general is a big family. Emacs Lisp for example has absolutely nothing to do with FP.

Of course you can romanticize about Lisp and it definitely has some cool descendants like Clojure, but you know, don't do it too much :-)

It indeed isn't. Here's my litmus test. Is 2^100 always equal to 2^100? Let's ask SBCL:

    * (eq (expt 2 100) (expt 2 100))
    
    NIL
Damn object identities, ruining muh equalities.

(Disclaimer: I'm not saying functional programming is the right approach for writing every program, but if a language can't even get arithmetic and relational operators right...)

I don't see how support for multiple equalities makes something not a functional language.

Functional doesn't mean "the semantics of everything is tied to its printed syntax" so that if two things look the same in the syntax, they denote the same thing. (Right?)

Suppose we take Haskell and give it an equal operator which can tell that two bignums are different instances and not equal. Does it cease being functional? What if we don't use that operator anywhere in a program and don't even know it exists; how do we know the language is not functional?

> Functional doesn't mean "the semantics of everything is tied to its printed syntax" so that if two things look the same in the syntax, they denote the same thing. (Right?)

I don't care about syntax. I want to manipulate the values I care about, not object identities. I want to operate on numbers, strings, lists, trees, what have you. Not memory cells that allegedly contain representations of numbers, strings, lists, trees, what have you. This is strictly a semantic issue.

> I don't see how support for multiple equalities makes something not a functional language.

FWIW, what most languages call “physical” or “reference equality” is a special case of structural equality (which is always the prior notion). Structural equality of mutable cells (which have dedicated types in Haskell and ML) happens to be physical equality.

> Suppose we take Haskell and give it an equal operator which can tell that two bignums are different instances and not equal. Does it cease being functional?

Yes.

> What if we don't use that operator anywhere in a program and don't even know it exists; how do we know the language is not functional?

Then you're doing functional programming in a non-functional language.

> What if we don't use that operator anywhere in a program and don't even know it exists; how do we know the language is not functional?

;-)

If a tree falls in a forest and no one is around to hear it, does it make a sound?

If a program satisfies a property but the compiler cannot prove it, can we still rely on it?

> If a program satisfies a property but the compiler cannot prove it, can we still rely on it?

It's very simple: You can rely on what you can prove. This applies both to compiler authors and compiler users. That sometimes one can prove things the other can't shouldn't come off as a surprise. Neither party has all the information: The compiler author doesn't know the intended meaning of the program submitted to the compiler by the user. The user doesn't (need to) know the internal details of how the compiler works.

Structural equivalence is given by EQUALP and has of course its own limitations (but works with trees, user-defined structs and hash-tables, for example). Of course, if you use the identity comparison, you get different results. I agree CL does not fit your definition of functional programming.

An implementation is permitted to make "copies" of characters and numbers at any time. The effect is that Common Lisp makes no guarantee that eq is true even when both its arguments are "the same thing" if that thing is a character or number.

http://www.lispworks.com/documentation/lw51/CLHS/Body/f_eq.h...

I'm aware of EQUALP. But the problem remains that it's possible to distinguish between supposedly “equal” values. Lisp and Scala are first and foremost object-oriented languages - whatever values you want to manipulate are always subordinate to objects whose physical identity in memory matters in the language's semantics, no matter how irrelevant they might be for your problem domain.

On the other hand, in Haskell and ML, due to their superior value-oriented design, there's no way to distinguish between “this 123456789” and “that 123456789”, or between “this list [1,2,3]” and “that list [1,2,3]”. There is only one number 123456789 and one one list [1,2,3] in the language's semantics, no matter how many copies of their representation might exist in memory.

I am also aware of that. I am not really trying to convince you of anything, mostly trying to prevent casual readers from being taught Common Lisp from you. Look at the example you just gave, this is beyond ridiculous. I would prefer if you were focusing on what you want to fight "for", not what you want to fight "against".

You like having a strong separation between the language and its implementation. I get it. Note however that if you are the Haskell compiler, you can know things the programmer cannot, or you can inject code that can perform manipulations the programmer cannot express. There probably is an identity equality operator down there, that people cannot access. If you like that, I can understand; I can understand the appeal for a strict separation of concerns and the desire to express only the high-level intent. But really, I think this is not much different in CL and that you are focusing on implementation details. In CL the separation is not enforced, and that is your main problem with it.

The CL compiler is defined at the language specification level. You have access to internals, by choice, just as if you were writing Haskell ASTs using an Haskell compiler's internal API. As such, you can cross abstraction barriers whenever you need. We already discussed about this once, you can use purely functional data-structures (see FSET) and EQUALP and code to that functional interface and pretend there is no computer down there. Then, if you want, you can play with packages and symbols to make MAP & co. refer to the parallel version (see LPARALLEL), re-compile and have parallel code (this works best with unqualified symbols and different package definitions for the same file).

In all PL discussions, there is eventually mention of an hypothetical sufficiently smart compiler. The CL point of view is (among other things) that such a compiler is one where a programmer can easily add its own extensions. That allows you to express the intent of the program clearly in one place and have the implementation details elsewhere.

In Lisp, symbols are very important. And symbols are identity. An identity with other dressing, such as having a character string name which is somewhat of a semantic footnote.

"Functional programming" doesn't restrict the kinds of obejcts you can work with. An identity value, whose virtue is that it is different from other identities, is a legitimate concept which can be treated under functional programming.

I guess your problem is that you don't want non-symbolic objects from behaving like identities; only symbolic ones.

That is to say, two symbols are equal iff they are actually the same symbol, otherwise not---but other kinds of entities are treated differently.

There is something to be said for having just one kind of equality, which is appropriately defined for every kind of object.

Pragmatic issues get in the way though.

Let's consider "this list [1,2,3]" versus "that list [1,2,3]". What if we have lazy lists (as those things tend to crop up in functional languages, particularly non-strictly evaluated ones). Is "this infinite list [1,2,3, ...]" equal to "that infinite list [1,2,3,...]" even if they are generated by completely separate lazy procedures?

Your equality then has to basically test that two Turing computations are equivalent: that the underlying lazy generation procedures themselves are computing the same thing, even if in different ways.

In that vein, what do you do about structures with cycles and shared structure? Is the list #1=(1 . #1#) equal to another one that is also #1=(1 . #1#). (Lisp's equal functions don't have to handle this; but we could specify an equal function which does).

Equality is not so simple that we can just cover all of its nuances with a blanket rule based on some handwaving principle.

Well, I would just use EQL.

If you want to use Haskell, use Haskell. Common Lisp works differently.

I don't even like Haskell, so, no, thanks. I was just arguing that Common Lisp isn't a functional language.
Well, that's not news.
and if it was about OOP, you'd get a hierarchy of answers that no one can follow and is unsound ;)
The author of this article did reference a classic treatment, though: John Backus's lecture.

A compact definition of functional programming is using only expressions, never statements. This leads to the idea that the effect or meaning of every computation must be encoded in its return value.

It is the programming style I learned to program in Lisp and Caml Light, and when Haskell was still known as Mirada.
Most people would agree that in functional programming we can pass functions around as first class objects. What we disagree on is the definition of function.
That's just one aspect of functional programming. By that definition, almost any modern language is functional.
Take it from the other side: why would a language that allows to create higher-order functions/closures not be called functional?
Most languages only have higher-order procedures, not higher-order functions.

As for closures, well, closures are an implementation technique. Not distinguishing between language features and implementation techniques is a part of an established tradition that comes from Lisp, but that doesn't make it any less wrong.

> Most languages only have higher-order procedures, not higher-order functions.

What do you mean by that?

Functions have the following properties:

(0) A function maps every element of its domain to a unique element of its codomain.

(1) Functions don't exist in time, let alone change over time.

(2) Two functions with the same domain and codomain are equal if they map the same domain elements to the same codomain elements.

Since when do so-called “first-class functions” in most programming languages behave like this?

Because that makes the term meaningless. You may as well ask "why don't we call any language that lets you do two actions in sequence 'procedural'?"
A term does not necessarily become meaningless when it applies to a lot of things. "Functional" might be a broad category after all, not the exclusive name of a subset of functional languages. And if your language allows different paradigms, it will be called "functional and imperative and object-oriented... ". At best, if a property is so common that most language have it, it can be assumed to be satisfied by default. As for "procedural", the definition on wikipedia is a little more precise and does not apply to all languages: https://en.wikipedia.org/wiki/Procedural_programming.
That doesn't make the term meaningless, it just means that "functional programming" is a victim of its own success.
If you can apply the term to every programming language in widespread use today[1], then yes, it is pretty useless. There is real value in having the term "functional programming" be meaningful and denote a certain class of languages; defining that as "any language with first-class function values" is too broad as to render the term meaningless.

[1] Even C has this with Apple's Blocks extension (http://clang.llvm.org/docs/Block-ABI-Apple.html).

Every modern language is functional. :P But more accurately, every modern language is generally multi-paradigm.
No, wrong. You can pass pointers to functions in C.
It is a bit snarky, but also has a ground truth: pointers to functions aren't functions.

More importantly, you cannot make functions in C. For example, one cannot write a function that, given pointers to functions that compute 1/x and sin(x), returns a pointer to a function that returns 1/sin(x)

That's not a function, that's a closure. And you can simulate that in C by creating a struct that contains the function pointer and the set of captured data (and then when you invoke the function you pass the struct to it as a parameter).
Yes, hence the distinction between first-class features and others, which you have to implement yourself.
The parent comment said C function pointers aren't functions. They are. They just aren't closures.
I always thought FP == pure functions. I guess not?
I would call a language like Haskell "purely functional" and a language like OCaml "impurely functional". A functional programming language to me is a programming language where functions are in charge of data. In a language like Java, data is in charge of functions (broadly speaking). In a language like Prolog, relations are in charge of data. It's all about what perspective the programmer has between the abstraction and the data.
You have it backwards. In Java, collections of procedures (aka objects) are in charge of data. In Haskell and OCaml, data exists independently of the procedures that will operate on it.