走近函数式编程

走近函数式编程

活动简介

活动主要是从书香墨剑学习函数式编程的心得体会出发,来谈谈他所理解的函数式编程,并以一个罗马数字转阿拉伯数的例子和大家一起探讨函数式编程的使用以及对我们日常编码的影响,最后大家互相分享对函数式的理解。

活动信息

  • 时间:2019.03.30 14:00 - 17:00
  • 地点:成都市高新区世纪城路 1029 号天华社区乡愁故事馆

活动流程

时刻 内容
14:00 Who are you?
14:10 函数式编程之我知
14:40 函数式编程之我用
16:00 你谈?我谈?共交流

我可以参与?

欢迎有一定 JavaScript 基础、对函数式编程有所理解 或 有不同看法的小伙伴~


活动总结

一来到活动现场,乡愁故事馆的文艺气息似乎可以冲淡些技术宅的刻板,为沙龙参会者带来一丝别样的感受。本次沙龙的主讲书香墨剑不但网名文艺,平时也是个话剧爱好者,活动场地也是他亲自选的,果然符合本人气质。

What ?

业内人士深知源自数学思想的“函数式编程”抽象晦涩,书香便以川菜经典“回锅肉”的做法来讲解 ——

function () {
  return++;
}

function (...食材) {
  return++ 食材 +;
}

let 回锅肉 = (((())), (蒜苗), 豆豉);

数学函数 层层传递输入输出不引用外部变量不改变外部变量 等主要原则一目了然。

但往往概念、原则讲多了,初学者要么云里雾里、要么颠覆三观,不能对新学的思想方法正确认识、合理运用。于是书香便一针见血地来了个“敲黑板”三连 ——

函数式编程只是一个编程范式

区别在于对程序的抽象看法

一段程序里可以存在多个编程范式。

那…… 书香你怎么看?一图胜千言 ——

Why & How ?

既然函数式编程有这么多好处 ——

  1. 方便单元测试
  2. 减少外部状态干扰
  3. 通过高阶抽象方便阅读、灵活组合

那该怎么用呢?很多人连 JavaScript 数组自带的 map()filter()reduce() 都还用不好呢。无妨,我们动手演练一个例子 —— 罗马数字与阿拉伯数字的转换。

规则如下:

//  罗马数字与阿拉伯数字的对应
const roman_arab = {
  I: 1,
  V: 5,
  X: 10,
  L: 50,
  C: 100,
  D: 500,
  M: 1000
};

罗马数字从大到小排列,并加起来得到最后的结果,只有下列情况除外:

  • I 可在 V、X 前,表减 1(如 IV 表示 4)
  • X 可在 L、C 前,表减 10
  • C 可在 D、M 前,表减 100

最后我们实现了罗马数字转阿拉伯数字的代码:

const romanLetterToInt = letter => {
  const table = {
    I: 1,
    V: 5,
    X: 10,
    L: 50,
    C: 100,
    D: 500,
    M: 1000
  };

  if (table[letter]) return table[letter];

  throw Error("capacity should be positive integer");
};

const strSplit = str => str.split("").map(romanLetterToInt);

const subtractItem = arr =>
  arr.map((element, index, arr) =>
    index < arr.length - 1 && element < arr[index + 1] ? -element : element
  );

const getInt = str =>
  subtractItem(strSplit(str)).reduce((acc, curr) => (acc += curr), 0);

以及阿拉伯数字转罗马数字的代码:

const table = {
  5: ["I", "V", "X"],
  50: ["X", "L", "C"],
  500: ["C", "D", "M"]
};

const intToIString = number => new Array(number).fill("I").join("");

const mergeLetter = (str, numIndex) => {
  const length = str.length;

  const headLen = Math.floor(length / 10),
    tailLen = length % 10;

  const headerArray = new Array(headLen).fill(table[numIndex][2]);

  switch (tailLen) {
    case 9:
      return headerArray
        .concat([`${table[numIndex][0]}${table[numIndex][2]}`])
        .join("");
    case 4:
      return headerArray
        .concat([`${table[numIndex][0]}${table[numIndex][1]}`])
        .join("");
    default:
      const tailArray = new Array(Math.floor(tailLen / 5))
        .fill(table[numIndex][1])
        .concat(str.substring(str.length - (tailLen % 5)).split(""));

      return headerArray.concat(tailArray).join("");
  }
};

const splitStr = (str, numIndex) => {
  const index = str.split("").findIndex(ele => ele !== table[numIndex][0]);

  return index >= 0 ? [str.slice(0, index), str.slice(index)] : [str, ""];
};

const mergeStr = (arr, numIndex) => mergeLetter(arr[0], numIndex) + arr[1];

const getRoman = num =>
  mergeStr(
    splitStr(
      mergeStr(splitStr(mergeStr(splitStr(intToIString(num), 5), 5), 50), 50),
      500
    ),
    500
  );

再举个栗子

本次活动主题较为抽象,主讲者虽尽力借鉴生活中的例子来讲解“函数式编程”的理念,但大家还是比较困惑;加上参会者自带电脑的又比较少,原定的现场动手实践较早结束。于是水歌便上台即兴分享了自己做过的 TDD 习题 —— 基于函数式编程的保龄球算法,让大家更清晰地认识函数式编程的应用范式。

  1. 每局比赛每人有十轮投球
  2. 每轮共有两次机会来打倒全部十个瓶子
  3. 一次打完为“全中”,本轮得分为 10 + 后两球分数
  4. 两次打完为“补中”,本轮得分为 10 + 后一球分数
  5. 第十轮全中、补中加 2、1 次击球

规则看完,想必全中、补中的事后加分最让人头疼…… 但函数式编程范式却像我们上学解数理化题时“套公式”一样,让计算程序一目了然 ——

每局分数 = 本局第一次击球分数 + 本局第二次击球分数 +
  下局第一次击球分数 x 补中系数 +
  (下局第一次击球分数 + 下局第二次击球分数) x 全中系数

用代码描述如下:

//  两数相加
function sum(first, second) {
  return first + second;
}

//  补中系数
function isSpare(first, second) {
  return first !== 10 && first + second === 10 ? 1 : 0;
}

//  全中系数
function isStrike(first) {
  return first === 10 ? 1 : 0;
}

//  每轮分数
function round(this_first, this_second, next_one, next_two) {
  return (
    this_first +
    this_second +
    isSpare(this_first, this_second) * next_one +
    isStrike(this_first, this_second) * (next_one + next_two)
  );
}

上述代码虽清晰明了,但还有个关键没有解决 —— 未来的分数怎么办?这就需要学习函数式编程的第一难关柯里化来解决:

function curry(origin) {
  //  原函数声明了几个参数
  const count = origin.length;
  //  包装函数
  return function wrapper() {
    //  当前是否已有足够的参数
    return count > arguments.length
      ? //  还差几个,返回一个记住已传入参数的新函数
        wrapper.bind(this, ...arguments)
      : //  够了,执行原函数
        origin.apply(this, arguments);
  };
}

这样我们就可以把击球数一个个填进去,最终合计即可算出总分 ——

const curry_round = curry(round),
  score = [];

score[0] = curry_round(3)(7)(3)(4); // 13

score[1] = curry_round(3)(4)(0)(0); // 7

score[2] = curry_round(10)(0)(3)(7); // 20

//  以此类推……

讲到这儿,台下部分同学频频点头,露出了“原来如此”的笑容~


活动反馈

以下是会后部分同学的建议 ——

  1. 编辑器居然没有设置自动保存
  2. 不写注释一时爽,一直不写一直爽
  3. 其实讲一讲 Redux,对大家理解函数式编程帮助比较大
  4. 其实讲一些实用的例子会更好,也更容易理解!
  5. 听了对柯里化理解还不够
  6. 罗马数字转阿拉伯数字的例子不错。阿拉伯数字转罗马数字的例子可以不用讲了,比较冗余了。可以加点例子: 如果其中某个函数抛出异常了怎么处理。

参考资料


评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×