by lucifer@duiba
在线讲稿
不稳定,可能需翻墙
关于我
个人主页: https://azl397985856.github.io/
querySelector = document.querySelector;
querySelector('body') // Uncaught TypeError: Illegal invocation
const sample = {
print() {
console.log(this.name)
}
name: 'name of sample'
}
sample.print() // name of sample'
const print = sample.print
print() // undefined
上下文丢失
最近三年很多大型应用和框架都开始使用FP。比如我们熟知的Elm,redux,ramda和react等。
JavaScript 借鉴了 Scheme 和 Awk 的思想, 将函数当作一等公民.
高阶函数和闭包使得它更合适FP。
因此 JavaScript 很适合我们学习FP。
// redundant
ajax(data => cb(data))
// equivalent to
ajax(cb)
// redundant
const fetchUser = callback => ajaxCall(json => callback(json));
// or
const fetchUser = callback => ajaxCall((data, err)=> callback(data, err));
// or more params if you like
// equivalent to
const fetchUser = callback => ajaxCall(callback)
// equivalent to
const fetchUser = ajaxCall
const BlogController = {
index(posts) { return Views.index(posts); },
show(post, additional) { return Views.show(post, additional); },
create(attrs) { return Db.create(attrs); },
update(post, attrs) { return Db.update(post, attrs); },
destroy(post) { return Db.destroy(post); },
};
// equivalent to
const BlogController = {
index: Views.index,
show: Views.show,
create: Db.create,
update: Db.update,
destroy: Db.destroy,
};
函数式编程是一种编程范式。
函数式编程世界观
FP 是一种通过组合 纯函数,避免状态共享和可变数据,并且限制副作用的一种构建软件的方式。
FP 专注于过程(运算),OOP专注于数据本身。
但是函数式编程有一点难以理解和掌握,尤其对于新手来说。 相对于命令式,FP学习曲线更佳陡峭。
函数式是一等公民
函数式编程中的函数指的是数学中的函数,而不是JavaScript中的函数。
function tween(x) {
// TODO: 用能够完全模拟的函数代替
return Math.log2(x) * 10 ** 1;
}
// pure function
const add10 = (a) => a + 10
// impure function due to external non-constants
let x = 10
const addx = (a) => a + x
// also impure due to side-effect
const setx = (v) => x = v
// pure
const slice = (arr, ...rest) => arr.slice(...rest)
// impure
const splice = (arr, ...rest) => arr.splice(...rest)
Cacheable
Portable / Self-documenting
Testable
Parallel
referential transparency
函数组合是一种将两个或多个函数组合以产生一个新的函数的过程。
这有什么用 ? - 充分重用你的代码
const add1 = (a) => a + 1
const times2 = (a) => a * 2
const compose = (a, b) => (c) => a(b(c))
const add1OfTimes2 = compose(add1, times2)
add1OfTimes2(5) // => 11
const add1 = (a) => a + 1
const times2 = (a) => a * 2
const pipe = (a, b) => (c) => b(a(c))
const times2OfAdd1 = pipe(add1, times2)
times2OfAdd1(5) // => 12
compose
pipe
const isLastInStock = (cars) => {
const lastCar = last(cars);
return prop('in_stock', lastCar);
};
// equivalent to
const isLastInStock = compose(prop('in_stock'), last);
const a = [1,2,3,4,5];
a.filter(isOdd).map(multiTwo); // [2,6,10];
// not strictly equals with
compose(multiTwo, isOdd)(a); // [2,6,10];
我们或许正在使用组合
curry让你可以通过给函数更少参数去调用它,不立即执行,而是返回一个新的接受剩余的参数的函数。
你可以选择一次性全部给全参数调用,也可以给定一部分参数,返回一个新的函数在将来某个时候执行。
const add = x => y => x + y;
const increment = add(1);
const addTen = add(10);
increment(2); // 3
addTen(2); // 12
var obj = ={a:1};
var obj2 = obj;
obj2.a = 2;
console.log(obj.a); // 2
console.log(obj2.a); // 2
uh...
const immutableState = Object.freeze({ minimum: 21 });
怎么让其不可变呢?
创建一个新的对象,而不是直接在原对象修改
那么,性能会有影响吗? - 好问题!
const data = {
to: 7,
tea: 3,
ted: 4,
ten: 12,
A: 15,
i: 11,
in: 5,
inn: 9
}
immutablejs(tree + sharing)
const data = {
to: 7,
tea: 3,
ted: 4,
ten: 12,
A: 15,
i: 11,
in: 5,
inn: 9
}
tea from 3 to 14.
一个🌰
简单起见,我以原生JS实现它。不使用任何框架,但是我会用到两个库(ramda,jquery)
html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Demo</title>
</head>
<body>
<div id="main" class="main"></div>
<script src="main.js"></script>
</body>
</html>
main.js
const $ = require('jquery');
const { curry, compose } = require('ramda');
// auto curried in FP language, and in JS we should curry manually
// (cb, url) => $.ajax(url, cb)
const getJSON = curry((cb, url) => $.ajax(url, cb));
// (selector, html) => $(selector).html(html)
const setHtml = curry((selector, html) => $(selector).html(html));
// for debugging
// (tag, x) => { console.log(tag, x); return x; }
const trace = curry((tag, x) => {
console.log(tag, x);
return x;
});
const getNewsByTitle = title => `http://news.test.com/api/v2/news?title=${title}`;
const app = compose(getJSON(trace('response')), getNewsByTitle);
// search all the news and query the news which has a title of 'hello world'
app('hello world');
data
// assume we have data structure like this:
{
items: [{
title: 'hello world',
author: 'lucifer',
cover: '',
content: '',
publishDate: ''
}, {
...
},
...
],
}
知识点
通过map我们可以将一个接受单个元素的函数,转变成一个接受数组的函数。
达到逻辑复用的目的。
eg:
const map = curry((func, functor) => functor.map(func));
const getItem = text => <li>{text}</li>;
const getItems = map(getItem);
getItems(['text1', 'text2'])
// 我们会得到
// [<li>text1</li>, <li>text2</li>]
main.js
const prop = curry((property, object) => object[property]);
// ['url1', 'url2', 'url3']
const coverUrls = compose(map(prop('cover')), prop('items'));
const img = src => $('<img />', { src });
// [<img src="url1" />, <img src="url2" />, <img src="url3" />]
const images = compose(map(img), coverUrls);
// <div id="main"> <img src="url1"/> <img src="url1"/> <img src="url1"/> </div>
const render = compose(setHtml('#main'), images);
const app = compose(getJSON(render), getNewsByTitle);
app('hello world')
const $ = require("jquery");
const { curry, compose, map } = require("ramda");
const { getUrlParam } = require("./utils");
import news from "../db/news.json";
const queryNews = curry((url, news) => {
const title = getUrlParam("title", url.slice(url.indexOf("?")));
return {
items: news.filter(item => item.title.indexOf(title) !== -1)
};
});
const ajax = (url, cb) =>
setTimeout(() => cb(queryNews(url, news.items)), Math.random() * 1000);
const getJSON = curry((cb, url) => ajax(url, cb));
const setHtml = curry((selector, html) => $(selector).html(html));
const trace = curry((tag, x) => {
console.log(tag, x);
return x;
});
const getNewsByTitle = title =>
`http://news.test.com/api/v2/news?title=${title}`;
const prop = curry((property, object) => object[property]);
const coverUrls = compose(
map(prop("cover")),
trace("items: "),
prop("items")
);
const img = src => $("<img />", { src });
const images = compose(
map(img),
trace("coverUrls: "),
coverUrls
);
const render = compose(
setHtml("#main"),
trace("iamges: "),
images
);
const app = compose(
getJSON(render),
trace("news url: "),
getNewsByTitle
);
app("hello world");
代码地址: https://github.com/azl397985856/functional-programming/tree/master/examples/news
finally
trace
没关系,慢慢来
新闻列表 是怎么工作的?
const coverUrls = compose(map(prop('cover')), prop('items'));
const img = src => $('<img />', { src });
const images = compose(map(img), coverUrls);
const render = compose(setHtml('#main'), images);
const app = compose(getJSON(render), getNewsByTitle);
代码地址:
https://github.com/azl397985856/functional-programming/tree/master/examples/todoList
const getItem = item => <li>{item}</li>;
const getWrapper = className => items => <ul className={className}>{items}</ul>;
const render = compose(getWrapper('demo-wrapper'), map(getItem))
ReactDOM.render(
render(['todo1', 'todo2', 'todo3', 'todo4']),
document.getElementById('main'));
最简版本
const store = {
todos: ["todo1", "todo2", "todo3", "todo4"]
};
function forceRender() {
ReactDOM.render(render(store.todos), document.getElementById("main"));
}
function deleteTodoByItem(item) {
store.todos = store.todos.filter(todo => todo !== item);
forceRender();
}
const getItem = onDelete => classname => item => (
<li className={classname} key={item}>
{item}
<span className="delete" onClick={onDelete.bind(null, item)}>
X
</span>
</li>
);
const getWrapper = className => items => <ul className={className}>{items}</ul>;
const render = compose(
getWrapper("demo-wrapper"),
map(getItem(deleteTodoByItem)("demo-todo-item"))
);
ReactDOM.render(render(store.todos), document.getElementById("main"));
稍微复杂一点
声明式通过高度抽象屏蔽了底层细节,比如map屏蔽了如何循环的具体细节。
从可变数据和副作用的地狱中解脱出来
运行更快(看情况)
懒执行
square(3 + 4)
(3 + 4) * (3 + 4) // evaluated the outermost expression
7 * 7 // both reduced at the same time due to reference sharing
49
square(3 + 4)
(3 + 4) * (3 + 4) // evaluated the outermost expression
7 * (3 + 4)
7 * 7
49
普通情况
Haskell( 一门函数式编程语言)
const results = _.chain(people)
.pluck('lastName')
.filter((name) => name.startsWith('Smith'))
.take(5)
.value()
再来一个例子
如果是树,那么FP和命令式查询的花销是一样的,但是修改的话,FP需要修改若干个节点,而不是像命令式修改一个节点。
如果是一个无序的hashmap,那么声明式必须使用树来模拟,那么花销就不是O(1)。
具体取决于树的具体结构。 据测试,大概会有10-20倍的性能差距。
FP 是没有循环的。
递归调用的时候,call stack会更长(不考虑尾调用优化),因此内存上会有一点损耗。这在某些场景下是致命的。
// 循环两次
const addOne = x => x + 1;
const time2 = x => x * 2;
const numbers = [1,2,3,4,5];
numbers.map(addOne).map(time2)
// 只需要循环一次
const numbers = [1,2,3,4,5];
const ret = [];
for(let i=0;i < numbers.length;i++) {
let item = addOne(numbers[i]);
item = time2(item);
ret.push(item)
}
return ret
compose(map(f), map(g)) <==> map(compose(f, g))
javascript引擎没有优化
那么FP语言本身呢?
// 循环一次
const addOne = x => x + 1;
const time2 = x => x * 2;
const numbers = [1,2,3,4,5];
map(compose(time2, addOne))(numbers)