[英] 准备充分了嘛就想学函数式编程?(Part 3)

1,052 阅读4分钟
原文链接: medium.com

Taking that first step to understanding Functional Programming concepts is the most important and sometimes the most difficult step. But it doesn’t have to be. Not with the right perspective.

Previous parts: Part 1, Part 2

Function Composition

As programmers, we are lazy. We don’t want to build, test and deploy code that we’ve written over and over and over again.

We’re always trying to figure out ways of doing the work once and how we can reuse it to do something else.

Code reuse sounds great but is difficult to achieve. Make the code too specific and you can’t reuse it. Make it too general and it can be too difficult to use in the first place.

So what we need is a balance between the two, a way to make smaller, reusable pieces that we can use as building blocks to construct more complex functionality.

In Functional Programming, functions are our building blocks. We write them to do very specific tasks and then we put them together like Lego™ blocks.

This is called Function Composition.

So how does it work? Let’s start with two Javascript functions:

var add10 = function(value) {
    return value + 10;
};
var mult5 = function(value) {
    return value * 5;
};

This is too verbose so let’s rewrite it using fat arrow notation:

var add10 = value => value + 10;
var mult5 = value => value * 5;

That’s better. Now let’s imagine that we also want to have a function that takes a value and adds 10 to it and then multiplies the result by 5. We couldwrite:

var mult5AfterAdd10 = value => 5 * (value + 10)

Even though this is a very simple example, we still don’t want to have to write this function from scratch. First, we could make a mistake like forget the parentheses.

Second, we already have a function that adds 10 and another that multiplies by 5. We’re writing code that we’ve already written.

So instead, let’s use add10 and mult5 to build our new function:

var mult5AfterAdd10 = value => mult5(add10(value));

We just used existing functions to create mult5AfterAdd10, but there’s a better way.

In math, f ∘ g is functional composition and is read “f composed with g” or, more commonly, “f after g”. So (f ∘ g)(x) is equivalent to calling f after calling g with x or simply, f(g(x)).

In our example, we have mult5 ∘ add10 or “mult5 after add10”, hence the name of our function, mult5AfterAdd10.

And that’s exactly what we did. We called mult5 after we called add10 with value or simply, mult5(add10(value)).

Since Javascript doesn’t do Function Composition natively, let’s look at Elm:

add10 value =
    value + 10
mult5 value =
    value * 5
mult5AfterAdd10 value =
    (mult5 << add10) value

The << infixed operator is how you compose functions in Elm. It gives us a visual sense of how the data is flowing. First, value is passed to add10 then its results are passed to mult5.

Note the parentheses in mult5AfterAdd10, i.e. (mult5 << add10). They are there to make sure that the functions are composed first before applying value.

You can compose as many functions as you like this way:

f x =
   (g << h << s << r << t) x

Here x is passed to function t whose result is passed to r whose result is passed to s and so on. If you did something similar in Javascript it would look like g(h(s(r(t(x))))), a parenthetical nightmare

Point-Free Notation

There is a style of writing functions without having to specify the parameters called Point-Free Notation. At first, this style will seem odd but as you continue, you’ll grow to appreciate the brevity.

In mult5AfterAdd10, you’ll notice that value is specified twice. Once in the parameter list and once when it’s used.

-- This is a function that expects 1 parameter
mult5AfterAdd10 value =
    (mult5 << add10) value

But this parameter is unnecessary since add10, the rightmost function in the composition, expects the same parameter. The following point-free version is equivalent:

-- This is also a function that expects 1 parameter
mult5AfterAdd10 =
    (mult5 << add10)

There are many benefits from using the point-free version.

First, we don’t have to specify redundant parameters. And since we don’t have to specify them, we don’t have to think up names for all of them.

Second, it’s easier to read and reason about since it’s less verbose. This example is simple, but imagine a function that took more parameters.

Trouble in Paradise

So far we’ve seen how Function Composition works and how we should specify our functions in Point-Free Notation for brevity, clarity and flexibility.

Now, let’s try to use these ideas in a slightly different scenario and see how they fare. Imagine we replace add10 with add:

add x y =
    x + y
mult5 value =
    value * 5

How do we write mult5After10 with just these 2 functions?

Think about it for a bit before reading on. No seriously. Think about. Try and do it.

Okay, so if you actually spent time thinking about it, you may have come up with a solution like:

-- This is wrong !!!!
mult5AfterAdd10 =
    (mult5 << add) 10

But this wouldn’t work. Why? Because add takes 2 parameters.

If this isn’t obvious in Elm, try to write this in Javascript:

var mult5AfterAdd10 = mult5(add(10)); // this doesn't work

This code is wrong but why?

Because the add function is only getting 1 of its 2 parameters here then its incorrect results are passed to mult5. This will produce the wrong results.

In fact, in Elm, the compiler won’t even let you write such mis-formed code (which is one of the great things about Elm).

Let’s try again:

var mult5AfterAdd10 = y => mult5(add(10, y)); // not point-free

This isn’t point-free but I could probably live with this. But now I’m no longer just combining functions. I’m writing a new function. Also, if this gets more complicated, e.g. if I want to compose mult5AfterAdd10 with something else, I’m going to get into real trouble.

So it would appear that Function Composition has limited usefulness since we cannot marry these two functions. That’s too bad since it’s so powerful.

How could we solve this? What would we need to make this problem go away?

Well, what would be really great is if we had some way of giving our addfunction only one of its parameters ahead of time and then it would get its second parameter later when mult5AfterAdd10 is called.

Turns out there is way and it’s called Currying.

My Brain!!!!

Enough for now.

In subsequent parts of this article, I’ll talk about Currying, common functional functions (e.g map, filter, fold etc.), Referential Transparency, and more.

Up next: Part 4