JavaScript函数式编程

by lucifer@duiba

在线讲稿

不稳定,可能需翻墙

关于我

个人主页: https://azl397985856.github.io/

目录

  • 什么是函数式编程 ?
  • 专业术语
  • 一个例子
  • 又一个例子
  • 函数式编程的优缺点
  • 下一步

一点闲话

why FP

and why JavaScript

FP 解决了传统OOP的一些令人诟病的问题

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

上下文丢失

mutable state 造成代码难以维护和调试

callback hell

CPS

副作用和纯操作混在一起

近几年 FP 变得越来越流行

最近三年很多大型应用和框架都开始使用FP。比如我们熟知的Elmreduxramdareact等。

JavaScript 借鉴了 SchemeAwk 的思想, 将函数当作一等公民.

 

高阶函数闭包使得它更合适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

curry让你可以通过给函数更少参数去调用它,不立即执行,而是返回一个新的接受剩余的参数的函数。

 

你可以选择一次性全部给全参数调用,也可以给定一部分参数,返回一个新的函数在将来某个时候执行。

const add = x => y => x + y;
const increment = add(1);
const addTen = add(10);

increment(2); // 3
addTen(2); // 12

Avoid mutating state


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 });

怎么让其不可变呢?

FP is immutable

创建一个新的对象,而不是直接在原对象修改

那么,性能会有影响吗? - 好问题!

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);

又一个🌰

todo list with react

代码地址: 

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中是反模式

  • Data.Map.Map 内部实现是平衡二叉树, 因此查询的复杂度是 O(log n). 这是一种持久化的数据结构。 这里的持久化不是将数据存储起来。如何理解呢? 它的意思是直接生成一个新的copy,并且重用可以重用的部分(前面介绍过)
  • Data.HashTable.HashTable 是一个真正的hashTable, 它的查询复杂度是O(1). 然而它是一种可变的数据结构 -- 直接修改原有数据 -- 因此你需要 IO monad 如果你确实要使用的话.

FP 是没有循环的。

递归调用的时候,call stack会更长(不考虑尾调用优化),因此内存上会有一点损耗。这在某些场景下是致命的。

javaScript解释器并没有对FP进行优化


// 循环两次
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)

参考

  • https://github.com/azl397985856/functional-programming
  • https://github.com/stoeffel/awesome-fp-js
  • https://github.com/MostlyAdequate/mostly-adequate-guide
  • https://ramdajs.com/
  • https://github.com/knowthen/elm
  • https://www.haskel.com/
  • https://www.codementor.io/haskell/tutorial/monoids-fingertrees-implement-abstract-data

下一步

  • container
  • error handling
  • async handling
  • monad

Thanks

FP in JavaScript(CN)

By lucifer

FP in JavaScript(CN)

  • 1,570