Hacker News new | ask | show | jobs
by gracenotes 4155 days ago
I would agree with GP that emergent code organization principles that come from eliminating side effects are the crux of FP, as far as being the bottom line of what makes it effective and worth internalizing.

Higher-order functions are a simple and powerful form of abstraction, but not much else. I could imagine something which has the software engineering benefits of an FP language without HOF, although maybe it wouldn't culturally be one.

I think getting enthusiastic about higher-order functions and trying to use them in OO languages can range from good to neutral to bad. On the plus side, code is usually shorter. On the negative side, it's not necessarily more readable, and types still usually expose the same amount of information about their inputs/outputs, which is "call me, anything can happen" (plus plenty of opportunity for lazy side effects, the most confusing kind).

2 comments

This kind of becomes a name game, what is truly "FP". For many, FP is meaningfully defined as a focus on functions and it's silly to try to define that away.

There is something, "ML-alike FP" or, even, "Haskell-alike FP" which I might call "pure FP" which is what you say. This is less difficult to see as being centralized on purity more than "FP" at large. Presumably, you could have "pure" OO, classes and all, by eschewing effects just the same.

I have at least one bet in the pot that you cannot, though. I think the true beating heart of "pure FP"/"Haskell-alike FP" is the centricity of composition as a guiding design of language. This comes directly from some of the category theoretic heritage of the core languages here. Once you're here, the primality of functions themselves is hard to escape from. You can go a ways without HOFs, but the lack of richness of composition which you're stuck with is very painful. BiCCCs are a very sweet spot.

But a function isn't really a function unless it is referentially transparent. Having HoF, composability, etc.. is all a direct result of referential transparency.

Sure we can simulate these concepts in imperative languages, but the broken abstractions break down rather quickly since the laws are ignored.

It's just a function in a different, less observable category. That's not the worst thing. And, of course, I'd like to argue for "pure FP" too.

Really, I just want to sidestep the argument that is appearing all over this post: that "FP" is also this, and this, and this.

Sure. FP is terrifically poorly defined. The definition used in the article is rather more specific than what most people would say. But it has merits and should be discussed. Let's just not stumble over naming difficulties to do so.

So, I see what you're saying, but I maintain that without first-order functions, you can't do anything useful with code organization.

If you wanted to simulate that, imagine writing in C or C++ or Javascript using only functions, only returning copies of things changed by the function, and never referencing global state.

There's nothing particularly magic about that, other than better testability--it's just really slow and obnoxious imperative code. When you start passing around functions and currying values, though, then you're actually starting to see cool things happening.

I think you're making this too abstract to be understood without having already seen the light, perhaps an example would help?
Okay, sure, let's look at it in Javascript.

The task? Take a list of numbers, and produce a list containing only the squares of those numbers--and only those squares falling between 10 and 50.

  var inList = [1,2,3,4,5,6,7,8,9,10];
  
  var non_fp = function ( myListOfNumbers ) {
  	var out = [];
  	var temp = [];
  	var i = 0;
  	var j = 0;
  
  	// square the numbers
  	for (i = 0; i < myListOfNumbers.length; i++) {
  		temp.push( myListOfNumbers[i] * myListOfNumbers[i] );
  	}
  
  	for (j = 0; j < temp.length; j++) {
  		if ( temp[j] > 10 && temp[j] < 100 ) {
  			out.push( temp[j] );
  		}
  	}
  
  	return out;
  }
  
  var fp = function ( myListOfNumbers ) {
  	return myListOfNumbers.map( function (x) { return x*x; } )
  			      .filter( function (x) { return (x > 10)  && (x< 100); });
  }
The first has no side effects.

The second version has no side effects, and wouldn't be possible without first-class functions, and to me is clearly the more functional answer.

Suggested minor change to the non_fp one:

  var inList = [1,2,3,4,5,6,7,8,9,10];
  
  var non_fp = function ( myListOfNumbers ) {
  	var out = [];
  
  	// square the numbers
  	for (var i = 0; i < myListOfNumbers.length; i++) {
  		var x = myListOfNumbers[i] * myListOfNumbers[i];
                if (x > 10 && x < 100) {
                        out.push( x );
                }
  	}
  
  	return out;
  }
That's a correct change--I chose the implementation that I did so that it was very obvious how the things remap to a "functional" approach. :)

You see the point I'm driving at, though?

(and thanks for prodding me into writing code to illustrate things better)

> You see the point I'm driving at, though?

Yes, absolutely. I thought you were right on the money with the words but I figured a bit of code would do wonders to hammer it down.

Erlang does an even better job I think:

  [X * X || X <- lists:seq(1,10), X * X > 10, X * X < 100].
Or:

  [Y || Y <- [X * X || X <- lists:seq(1,10)], Y > 10, Y < 100].
Slightly longer, but without that X * X repetition.