了解递归的几种姿势 - 函数式编程

栏目: 编程语言 · 发布时间: 4年前

内容简介:递归和迭代是一枚硬币的两面,在不可变的条件下,递归提供了一种更具表现力、强大且优秀的迭代替代方法递归函数由以下两个主要部分组成:递归主要的核心思想是将问题分解为较小的问题,逐个解决后再组合,构建出整个问题的答案。

递归和迭代是一枚硬币的两面,在不可变的条件下,递归提供了一种更具表现力、强大且优秀的迭代替代方法

递归函数由以下两个主要部分组成:

  • 基准
  • 递归条件

递归主要的核心思想是将问题分解为较小的问题,逐个解决后再组合,构建出整个问题的答案。

具体概念不详述,可谷歌百度自行搜索。递归适合解决类似XML解析、语法树构建,深度遍历等问题。

而在Haskell这种纯函数编程语言里,原本是没有循环结构的,递归是天然代替循环的,比如求和函数(当然,Haskell有原生的sum方法支持)实现,如下所示:

_sum []  = 0
_sum (x:xs) = x + _sum xs
复制代码

你会发现两行基本表达了上述所说的递归两个主要部分。很优雅诶~

递归适当时候可以优雅的解决迭代不适合处理的问题。掌握递归思考的方式是一个长期训练的过程。

下文将带大家学习几个递归的姿势,由于篇幅有限,不详述原理。

求和的几种姿势

考虑给一个数组求和:

const nums = [1, 2, 3, 4, 5];
复制代码

命令式

命令式的开发思维,会很自然写出以下代码:

let total = 0;
for(let i = 0; i < nums.length; i++) {
  total += nums[i];
}

console.log(total); // 15
复制代码

声明式

更进一步,学了点函数式编程的同学会写出以下代码:

const add = (x, y) => x + y;
const sum = (...nums) => nums.reduce(add, 0);

console.log(sum(...nums)); // 15
复制代码

递归

了解递归的同学,写出来以下代码:

function getTotal(sum, num, ...nums) {
  if (nums.length === 0) {
    return sum + num;
  } else {
    return sum + getTotal(num, ...nums);
  }
}

console.log(getTotal(...nums)); // 15
复制代码

但是,目之所及,递归还是很少用的,不仅仅常见的缺乏递归思维问题,也是有性能问题的考虑,大家会发现写递归存在栈溢出的问题:

了解递归的几种姿势 - 函数式编程

于是我写了个函数,测试一下Chrome浏览器支持递归的深度是多少?

function getMaximumCallStack(getTotal) {
  const f = n => getTotal(...'1'.repeat(n).split('').map(Number));
  let i = 1;

  while(true) {
    try {
      const res = f(i);
      console.log(`Stack size: ${i}, f(${i})=${res}`);
      i++;
    } catch(e) {
      console.info(`Maximum call stack size: ${i}`);
      break;
    }
  }
}

getMaximumCallStack(getTotal);
复制代码

测试了上述写的getTotal递归,

了解递归的几种姿势 - 函数式编程

Chrome宝宝竟然只是到了484层栈就跪了,实在不敢相信!

------------浏览器三八分割线------------

Safari宝宝表现如何呢?

了解递归的几种姿势 - 函数式编程

貌似比Chrome好一丢丢,不过也没什么很大的区别...

那这样让我们如何愉快的使用递归呀?

递归的几种优化方式

如上文所述,递归虽然优雅,但是常常会遇到栈溢出的情况,那么这种问题怎么优化呢?以下三种优化方式:

PTC(Proper Tail Calls)

function getTotal_PTC(sum, num, ...nums) {
  sum += num;
  if (nums.length === 0) {
    return sum;
  } else {
    return getTotal_PTC(sum, ...nums);
  }
}

console.log(getTotal_PTC(...nums)); // 15
复制代码

PTC版的递归其实和上文写的递归只有些微写法上的区别:

// 正常递归
return sum + getTotal(num, ...nums);
// PTC版的递归
return getTotal_PTC(sum, ...nums);
复制代码

改成PTC写法之后,支持支持PTC优化的浏览器,可以不断重复利用原有的栈,从而避免了栈溢出的问题。(原理大致上是由于浏览器不用保留记住每一次递归中的值,在这个函数里特指 sum + getTotal(num, ...nums) 中的sum变量值,从而新栈替换旧栈。

支持PTC优化的浏览器不多,目前可能只有Safari支持,仍然为了眼见为实,在Chrome和Safari两个浏览器进行了测试。

运行上述 工具 方法测试: getMaximumCallStack(getTotal_PTC)

Chrome宝宝很可惜的偷懒了,木有支持~(残念),见下图:

了解递归的几种姿势 - 函数式编程

Safari宝宝果然优秀,对其有所支持!跑了一段时间,未见溢出,见下图:

了解递归的几种姿势 - 函数式编程

CPS(Continuation Passing Style)

const getTotal_CPS = (function() {
  return function(...nums) {
    return recur(nums, v => v);
  };

  function recur([sum, ...nums], identity) {
    if (nums.length === 0) {
      return identity(sum);
    } else {
      return recur(nums, v => identity(sum + v));
    }
  }
})();

console.log(getTotal_CPS(...nums)); // 15
复制代码

这种优化技巧通过创建额外的包裹函数:

  1. 将值的计算延迟
  2. 避免调用栈的堆积

但是不可避免的消耗了更多的内存用来存放这些多余的包裹函数。 (关于具体原理比较复杂,有空单独写篇文章论述)

Chrome浏览器测试如下图:

了解递归的几种姿势 - 函数式编程

木有栈溢出...只是运算越来越慢。

Trampoline

function getTotal_f(sum, num, ...nums) {
  sum += num;
  if (nums.length === 0) {
    return sum;
  } else {
    return () => getTotal_f(sum, ...nums);
  }
}

function trampoline(f) {
  return function trampolined(...args) {
    let result = f(...args);
    while (typeof result == "function") {
      result = result();
    }
    return result;
  };
}

const getTotal_trampoline = trampoline(getTotal_f);

console.log(getTotal_trampoline(...nums)); // 15
复制代码

这种思维技巧将递归巧妙的转换为了迭代! 写法保持了递归的思维,但是经过trampoline工具函数的处理,实际上交给浏览器执行的时候变成了迭代。

Chrome测试如下:

了解递归的几种姿势 - 函数式编程

速度飞快!丝滑流畅~

考虑到内存堆栈问题,trampoline还是蛮适合作为折中的方案的。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Beginning Google Maps API 3

Beginning Google Maps API 3

Gabriel Svennerberg / Apress / 2010-07-27 / $39.99

This book is about the next generation of the Google Maps API. It will provide the reader with the skills and knowledge necessary to incorporate Google Maps v3 on web pages in both desktop and mobile ......一起来看看 《Beginning Google Maps API 3》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具