跳到主要内容

1 篇博文 含有标签「Ramda」

查看所有标签

· 阅读需 17 分钟
木易(OwenYang)

Agenda

  • What is Functional Programming?
  • How Functional Programming Differs From Other Paradigms
  • Functional Programming in Javascript
  • Advantages and Disadvantages of Functional Style
  • Interaction of Functional and Object-Oriented Styles

What is Functional Programming?

  • Restricted sense, FP means programming without mutable variables, assignments, loops and other imperative control structures
  • Wider sense, FP means focusing on the functions
  • Particularly, functions can be values that are produced, consumed, and composed.

Functional Programming Languages

Functions in a FP languages are first-class citizens. This means:

  • they can be defined anywhere, including inside other function
  • like any other values, they can be passed as parameters to functions and returned as results
  • as for other values, there exist a set of operators to compose functions

Some languages

Restricted sense:

  • Pure Lisp, XSLT, XPath, XQuery, FP

Wider sense:

  • JavaScript
  • Lisp, Scheme, Racket, Clojure
  • SML, F#
  • Haskell (full language)
  • Scala

OOP vs FP

OO focuses on the differences in the data, while FP concentrates on consistent data structures.


Object-Oriented

  • Data and the operations upon it are tightly coupled
  • Objects hide their implementation of operations from other objects via their interfaces
  • The central model for abstraction is the data itself
  • The central activity is composing new objects and extending existing objects by adding new methods to them

Functional

  • Data is only loosely coupled to functions
  • Functions hide their implementation, and the language’s abstractions speak to functions and the way they are combined or expressed
  • The central model for abstraction is the function, not the data structure. The central activity is writing new functions

Declarative vs Imperative

One main distinguishing characteristics of functional programming languages is that they describe what they want done, and not how to do it. OO, inside its methods, still uses mostly imperative techniques.


Imperative programming is about

  • modifying mutable variables
  • using assignment
  • and control structures such as if-then-else, loops, break, continue, return.

The most common informal way to understand imperative programs is as instruction sequences for a Von Neumann computer

Processor <---- bus ----> Memory


Declarative programming is about

  • concentrate on functions
  • avoid mutations (mutation can bestroy useful laws)
  • have powerful ways to abstract and compose functions

Imperative style

var sumOfSquares = function(list) {
var result = 0;
for (var i = 0; i < list.length; i++) {
result += square(list[i]);
}
return result;
};

console.log(sumOfSquares([2, 3, 5]));

Functional style

var sumOfSquares = pipe(map(square), reduce(add, 0));

console.log(sumOfSquares([2, 3, 5]));

Functional Features in JavaScript

Easily available in Javascript

  • First-class functions
  • Lambdas/Anonymous Functions with closures
  • Compact, even terse, functions

Possible to accomplish in JavaScript

  • Mostly stateless processing
  • Side-effect-free functions calls

Not available in JavaScript

  • Performant recursion through tail call optimization
  • Pattern matching (Haskell, Erlang)
  • Lazy Evaluation (Miranda, Haskell)
  • Homoiconicity (mostly LISP-like languages?)

Shared examples

This calculates the odds of choosing the correct n numbers out of the p possibilities.

iterative version

function odds(n, p) {
var acc = 1;
for(var i = 0; i < n; i++) {
acc *= (n - i) / (p - i);
}
return acc;
}

console.log(odds(3, 10)); //=> (3/10) * (2/9) * (1/8) => (1/120) => 0.008333...

Recursive version

// Recursive version

function odds(n, p) {
return (n == 0) ? 1 : (n / p) * odds(n - 1, p - 1);
}

console.log(odds(3, 10)); //=> (3/10) * (2/9) * (1/8) => (1/120) => 0.008333...
  • recursive functions are often much more elegant than their iterative cousins.
  • Unfortunately, they often don't perform as well. All the overhead of creating stack contexts for function calls tends to add up. But certain kinds of recursive calls can be easily optimized.

tail recursive

var odds = (function(){
var odds1 = function(n, p, acc) {
return (n == 0) ? acc : odds1(n - 1, p - 1, (n / p) * acc);
}

return function(n, p) {
return odds1(n, p, 1)
}
})();

Note that the recursive call in odds1 is the last statement in its branch of the function. If this is true for all recursive calls, then the function is tail-recursive, and the compiler can replace the entire set of nested calls with simple JUMP operations.


Functional Programming in Javascript

With first-class function, closures, and anonymous functions, Javascript allows us to do a great deal of functional programming, even if we don't have things like pattern matching and homoiconicity.

There are some tools built in to modern Javascript environments, and it's straightforward to roll your own.

  • Ramda
  • Underscore, Lo-Dash

Using Functional Techniques in JavaScript

Example will be a Task List application, fetching something like the following data from the server:

var data = {
result: "SUCCESS",
interfaceVersion: "1.0.3",
requested: "10/17/2013 15:31:20".
lastUpdated: "10/16/2013 10:52:39",
tasks: [
{id: 104, complete: false, priority: "high",
dueDate: "11/29/2013", member: "Scott",
title: "Do something", created: "9/22/2013"},
{id: 105, complete: false, priority: "medium",
dueDate: "11/22/2013", member: "Lena",
title: "Do something else", created: "9/22/2013"},
{id: 107, complete: true, priority: "high",
dueDate: "11/22/2013", member: "Mike",
title: "Fix the foo", created: "9/22/2013"},
{id: 108, complete: false, priority: "low",
dueDate: "11/15/2013", member: "Punam",
title: "Adjust the bar", created: "9/25/2013"},
{id: 110, complete: false, priority: "medium",
dueDate: "11/15/2013", member: "Scott",
title: "Rename everything", created: "10/2/2013"},
{id: 112, complete: true, priority: "high",
dueDate: "11/27/2013", member: "Lena",
title: "Alter all quuxes", created: "10/5/2013"}
// , ...
]
};

Goal

tasks: [
{id: 104, complete: false, priority: "high",
dueDate: "11/29/2013", member: "Scott",
title: "Do something", created: "9/22/2013"},
{id: 105, complete: false, priority: "medium",
dueDate: "11/22/2013", member: "Lena",
title: "Do something else", created: "9/22/2013"},
// , ...
]

The goal will be a function that accepts a member parameter, then fetches the data from the server (or from some application cache), chooses the tasks for that member that are not complete, returns their ids, priorities, titles, and dues dates, sorted by due date.

  • asynchronous, we'll hook everything together with promises.
  • ignore all error-checking

Imperative Approach

var getIncompleteTaskSummariesForMember_imperative = function(memberName) {
return fetchData()
.then(function(data) {
return data.tasks;
})
.then(function(tasks) {
var results = [];
for (var i = 0, len = tasks.length; i < len; i++) {
if (tasks[i].member == memberName) {
results.push(tasks[i]);
}
}
return results;
})
.then(function(tasks) {
var results = [];
for (var i = 0, len = tasks.length; i < len; i++) {
if (!tasks[i].complete) {
results.push(tasks[i]);
}
}
return results;
})
.then(function(tasks) {
var results = [], task;
for (var i = 0, len = tasks.length; i < len; i++) {
task = tasks[i];
results.push({
id: task.id,
dueDate: task.dueDate,
title: task.title,
priority: task.priority
})
}
return results;
})
.then(function(tasks) {
tasks.sort(function(first, second) {
return first.dueDate - second.dueDate;
});
return tasks;
});
};


Object-Oriented Approach

// main method
var getIncompleteTaskSummariesForMember_objectOriented = function(memberName) {
return fetchData()
.then(function(data) {
var taskList = new TaskList(data.tasks);
taskList.chooseByMember(memberName);
taskList.chooseByCompletion(false);
var newTaskList = taskList.getSummaries();
newTaskList.sort(new TaskListSorter("dueDate"));
return newTaskList.tasks;
});
};

var TaskList = (function() {
var TaskList = function(/*Task[]*/ tasks) {
this.tasks = tasks;
};
TaskList.prototype.chooseByMember = function(memberName) {
var results = [];
for (var i = 0, len = this.tasks.length; i < len; i++) {
if (this.tasks[i].member === memberName) {
results.push(this.tasks[i]);
}
}
this.tasks = results;
};
TaskList.prototype.chooseByCompletion = function(completion) {
var results = [];
for (var i = 0, len = this.tasks.length; i < len; i++) {
if (this.tasks[i].complete == completion) {
results.push(this.tasks[i]);
}
}
this.tasks = results;
};
TaskList.prototype.getSummaries = function() {
var results = [], task;
for (var i = 0, len = this.tasks.length; i < len; i++) {
task = this.tasks[i];
results.push({
id: task.id,
dueDate: task.dueDate,
title: task.title,
priority: task.priority
})
}
return new TaskList(results);
};

TaskList.prototype.sort = function(/*TaskListSorter*/ sorter) {
this.tasks.sort(sorter.getSortFunction());
};

return TaskList;
}());

    var TaskListSorter = (function()  {
var TaskListSorter = function(propName) {
this.propName = propName;
};
TaskListSorter.prototype.getSortFunction = function() {
var propName = this.propName;
return function(first, second) {
return first[propName] < second[propName] ? -1 :
first[propName] > second[propName] ? +1 : 0;
}
};

return TaskListSorter;
}());

The contents of the functions are much the same; it's the way they are organized that varies.


Converting to Functional Code

The process for the remainder of this talk will be to convert this code into concise, readable, functional code, one block at a time, explaining some of the basic building blocks of functional programming as we go. First up is this little function:

.then(function(data) {
return data.tasks;
})

Functional version

.then(prop('tasks'))

So the obvious question, then, is, what is the prop function?


The prop function

module.exports = _curry2(function prop(p, obj) { return obj[p]; });

Our then call needs a function, so curry must be doing something interesting with this function, which should return an object propery, to instead returning a new function. So we need to take a detour to discuss curry a bit.


Currying Functions

Currying is the process of converting functions that take multiple arguments into ones that, when supplied fewer arguments, return new functions that accept the remaining ones.

var addFourNumbers = function(a, b, c, d) {
return a + b + c + d;
};

var curriedAddFourNumbers = R.curry(addFourNumbers);
var f = curriedAddFourNumbers(1, 2);
var g = f(3);
g(4); //=> 10

Back to get

Now that we understand curry, we can see that a manually curried version of this function might look like this:

var prop = function(prop) {
return function(obj) {
return obj[prop];
};
};

And that means that our new get('tasks') is equivalent to

function(obj) {
return obj['tasks'];
}

Filtering

.then(function(tasks) {
var results = [];
for (var i = 0, len = tasks.length; i < len; i++) {
if (tasks[i].member == memberName) {
results.push(tasks[i]);
}
}
return results;
})


Functional version

.then(filter(function(task) {
return task.member == memberName;
}))

But we've done something more important too: We've moved the focus from iteration and updating the state of a local collection to the real point of this block: choosing the tasks with the proper member property.

One of the most important features of functional programming is that it makes it easy to shift focus in this manner.


Rejecting elements

The next block is similar, except that instead of using filter, we will use reject, which behaves exactly the same except that it chooses those members of the list that don't match the predicate. We replace this code:

.then(function(tasks) {
var results = [];
for (var i = 0, len = tasks.length; i < len; i++) {
if (!tasks[i].complete) {
results.push(tasks[i]);
}
}
return results;
})

with this:

.then(reject(function(task) {
return task.complete === true;
)))


Refactoring

A reasonable question would be why with didn't do this instead, which would work equally well: