The Soul of Functional Programming: Separation and Recognition
函数式编程的灵魂:分离和识别
The structure of the course
The Slience
The Voyage
The Demo
In the beginning…
Anything goes
Everything changes
Weird names
There was primordial soup
1 2 3 4 5 6 7
10 i = 0 20 i = i + 1 30 PRINT i; " squared = "; i * i 40 IF i >= 10 THEN GOTO 60 50 GOTO 20 60 PRINT "Program Completed." 70 END
Discipline Wins
Exercising restraint while coding feels weird at first, but it’s worth it. “The goto statement as it stands is just too primitive; it is too much an invitation to make a mess of one’s program.”
The Symptoms
Custom names
Looping patterns
Glue code
Side effects
Omit Needless Names
Omit needless names with separations and recongnitions.
separate inputs from environment
Secret input: time
1 2 3 4 5 6 7 8
functiondaysThisMonth() { var date = newDate() , y = date.getFullYear() , m = date.getMonth() , start = newDate(y, m, 1) , end = newDate(y, m + 1, 1); return (end - start) / (1000 * 60 * 60 * 24); }
Always works the same
1 2 3 4 5
functiondaysInMonth(y, m) { var start = newDate(y, m - 1, 1) , end = newDate(y, m, 1); return (end - start) / (1000 * 60 * 60 * 24); }
var teaser = slice(0); map(compose(setText, teaser(50), text), all('p'));
Recognize Pure Function
Function that don’t change anything are called “pure”.
Their purity makes them:
testable
memoizable
portable
parallelizable
Let’s play pure or impure
1 2 3 4 5 6 7 8 9 10
functiongetQueryVariable(variable) { // impure 因为window.loaction var query = window.location.search.substring(1); var vars = query.split('&'); for (var i = 0; i < vars.length; i++) { var pair = vars[i].split('='); if (decodeURIComponent(pair[0]) == variable) { returndecodeURIComponent(pair[1]); } } }
/***************************************** C U R R Y I N G E X A M P L E ******************************************/
// We've got a nice multiply function. // It takes two arguments.
console.log( _.multiply(3, 4) );
// But it has been secretly curried already // so we can feed it fewer arguments and it // will return a new function. // // How about making a function to double a // value? Done. var double = _.multiply(2);
console.log( double(13) );
/***************************************** Y O U R T U R N ******************************************/
// _.split pulls a string apart around a // given value console.log( _.split('i', 'mississippi') );
// -- Challenge 1 ------------------------ // Make a function called "words" which // returns a list of words in a string. // Use only the split function and // currying.
console.log("Testing challenge 1...");
var words = _.split(' '); // change this assertEqualArrays( ['one', 'two', 'three'], words('one two three') ); console.log("passed");
// -- Challenge 2 ------------------------ // Create a function to triple every // number in a list using only // _.multiply and _.map.
console.log("Testing challenge 2...");
var tripleList = _.map(_.multiply(3)); assertEqualArrays([3,6,9], tripleList([1,2,3]));
console.log("passed");
// -- Challenge 3 ------------------------ // Create a function to find the largest // number in a list. You can use the // greater(a,b) function which returns the // greater of its two inputs. You can do // this with currying and one of the list // functions _.map, _.filter, or _.reduce.
console.log("Testing challenge 3...");
var greater = function(a,b) { return a > b ? a : b; };
var max = _.reduce(greater, undefined); assertEqual(9, max([1,-3483,9,7,2])); assertEqual(-1, max([-21,-3483,-2,-1]));
console.log("passed");
console.log("All tests pass."); /****************************************** B A C K G R O U N D C O D E *******************************************/
functionassertEqualArrays(x,y) { if(x.length !== y.length) throw("expected "+x+" to equal "+y); for(var i in x) { if(x[i] !== y[i]) { throw("expected "+x+" to equal "+y); } } } functionassertEqual(x,y){ if(x !== y) throw("expected "+x+" to equal "+y); }
var _ = R; var get = _.curry(function(x, obj) { return obj[x]; });
/****************************************** C O M P O S I T I O N E X A M P L E ******************************************/
// Curried functions are easy to compose. // Using _.map, _.size, and _.split we can // make a function that returns the lengths // of the words in a string.
var lengths = _.compose( _.map(_.size), _.split(' ') ); console.log(lengths('once upon a time'));
/******************************************* Y O U R T U R N ********************************************/
// -- Challenge 1 ------------------------- // Return a list of the author names in // articles using only get, _.compose, and // _.map.
var names = _.map( _.compose(get('name'), get('author')) ); // change this assertEqualArrays( ['Debbie Downer', 'Caspar Milquetoast'], names(articles) );
// -- Challenge 2 ------------------------- // Make a boolean function that says whether // a given person wrote any of the articles. // Use the names function you wrote above // with _.compose and _.contains.
// var isAuthor = (name, list) => _.contains(name, names(list)) // change this varisAuthor = (name, articles) => _.compose(_.contains(name), names)(articles);
// -- Challenge 3 ------------------------- // There is more to point-free programming // than compose! Let's build ourselves // another function that combines functions // to let us write code without glue variables.
// As you can see, the fork function is a // pipeline like compose, except it duplicates // its value, sends it to two functions, then // sends the results to a combining function. // // Your challenge: implement a function to // compute the average values in a list using // only fork, _.divide, _.sum, and _.size.
var avg = fork(_.divide, _.sum, _.size); // change this assertEqual(3, avg([1,2,3,4,5]));
console.log("All tests pass.");
/****************************************** B A C K G R O U N D C O D E *******************************************/
functionassertEqualArrays(x,y) { if(x.length !== y.length) throw("expected "+x+" to equal "+y); for(var i in x) { if(x[i] !== y[i]) { throw("expected "+x+" to equal "+y); } } } functionassertEqual(x,y){ if(x !== y) throw("expected "+x+" to equal "+y); }
Point Free
Point means argument. You can think of Point Free means argument free.
console.clear(); var _ = R; var P = PointFree; var map = P.fmap; var compose = P.compose; varMaybe = P.Maybe; varIdentity = P.Id;
// Exercise 1 // ========== // Use _.add(x,y) and map(f,x) to make a function that increments a value inside a functor console.log("--------Start exercise 1--------")
// Exercise 2 // ========== // Use _.head to get the first element of the list var xs = Identity(['do', 'ray', 'me', 'fa', 'so', 'la', 'ti', 'do']) console.log("--------Start exercise 2--------")
// Exercise 3 // ========== // Use safeGet and _.head to find the first initial of the user var safeGet = _.curry(function(x,o){ returnMaybe(o[x]) }) var user = {id: 2, name: "Albert"} console.log("--------Start exercise 3--------")
console.clear(); var _ = R; var P = PointFree; var map = P.fmap; var compose = P.compose; varMaybe = P.Maybe; varIdentity = P.Id;
varEither = folktale.data.Either; varLeft = Either.Left; varRight = Either.Right; varIO = P.IO.IO; var runIO = P.IO.runIO; P.IO.extendFn();
// Exercise 1 // ========== // Write a function that uses checkActive() and showWelcome() to grant access or return the error console.log("--------Start exercise 1--------")
var showWelcome = compose(_.add( "Welcome "), _.get('name'))
var checkActive = function(user) { return user.active ? Right(user) : Left('Your account is not active') }
var ex1 = compose(map(showWelcome), checkActive)
assertDeepEqual(Left('Your account is not active'), ex1({active: false, name: 'Gary'})) assertDeepEqual(Right('Welcome Theresa'), ex1({active: true, name: 'Theresa'})) console.log("exercise 1...ok!")
// Exercise 2 // ========== // Write a validation function that checks for a length > 3. It should return Right(x) if it is greater than 3 and Left("You need > 3") otherwise console.log("--------Start exercise 2--------")
// Exercise 5 // ========== // Use getHref() / getProtocal() and runIO() to get the protocal of the page. var getHref = function(){ return location.href; }.toIO(); var getProtocal = compose(_.head, _.split('/')) var ex5 = compose(map(getProtocal), getHref)
// Exercise 6* // ========== // Write a function that returns the Maybe(email) of the User from getCache(). Don't forget to JSON.parse once it's pulled from the cache so you can _.get() the email
console.clear(); var _ = ramda; var P = PointFree; var map = P.fmap; var compose = P.compose; varMaybe = P.Maybe; varIdentity = P.Id; var runIO = P.IO.runIO; P.IO.extendFn();
// Exercise 1 // ========== // Use getPost(id) to return a Future of the title of the post ({id: i, title: 'Love them futures'}) console.log("--------Start exercise 1--------")
var ex1 = compose(map(_get('title')), getPost)
ex1(3).fork(log, function(title){ assertEqual('Love them futures', title) console.log("exercise 1..ok!") })
// Exercise 2 // ========== // Use ex1 to extend the computation and render the title in a div console.log("--------Start exercise 2--------")
var render = function(x){ return"<div>"+x+"</div>"; } var ex2 = compose(map(render), ex1)
ex2(3).fork(log, function(html){ assertEqual('<div>Love them futures</div>', html) console.log("exercise 2...ok!") })
// Exercise 3 // ========== // In JSBin, click the "Output" tab to see a div. Click this div to run the test. // Turn the clicks into a stream of the div's innerHTML console.log("--------Start exercise 3--------")
var clicks = Bacon.fromEventTarget(document.querySelector("#box"), "click")
//Todo: turn clicks into a stream of the e.target.innerHTML var htmlClicks = clicks.map(function(e){ return e.target.innerHTML; })
// Exercise 4 // ========== // Keep the Output tab open. Type into the input to run the test. // Transform the keydowns into a stream of the input's value // Then use pureLog() to log it to the console console.log("--------Start exercise 4--------")
var pureLog = function(x){ console.log(x); return x; }.toIO(); var search_input = document.querySelector("#search") var keydowns = Bacon.fromEventTarget(search_input, "keydown")
//Todo: turn keydowns into a stream of the logged input's value var logs = keydowns;
functiongetPost(i) { returnnewFuture(function(rej, res) { setTimeout(function(){ res({id: i, title: 'Love them futures'}) }, 300) }) }
functiongetComments(i) { returnnewFuture(function(rej, res) { setTimeout(function(){ res(["This class should be illegal", "Monads are like space burritos"]) }, 300) }) }
console.clear(); var _ = R; var P = PointFree; var map = P.fmap; var mjoin = P.mjoin; var chain = P.flatMap; var compose = P.compose; varMaybe = P.Maybe; varIdentity = P.Id; var runIO = P.IO.runIO; P.IO.extendFn();
// Exercise 1 // ========== // Use safeGet and mjoin or chain to safetly get the street name console.log("--------Start exercise 1--------")
var safeGet = _.curry(function(x,o){ returnMaybe(o[x]) }) var user = {id: 2, name: "Albert", address: { street: {number: 22, name: 'Walnut St'} } }
var ex1 = compose(chain(safeGet('name')), chain(safeGet('street')),safeGet('address'))
// Exercise 3 // ========== // Use monads to first get the Post with getPost(), then pass it's id in to getComments(). console.log("--------Start exercise 3--------")
var ex3 = compose(chain(getComments, _.get('id')), getPost)
functiongetPost(i) { returnnewFuture(function(rej, res) { setTimeout(function(){ res({id: i, title: 'Love them futures'}) }, 300) }) }
functiongetComments(i) { returnnewFuture(function(rej, res) { setTimeout(function(){ res(["This class should be illegal", "Monads are like space burritos"]) }, 300) }) }