函子(Functor)

函子是一个特殊的容器,通过一个普通对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系),容器包含值和值变形关系(这个变形关系就是函数)。函数式编程中解决副作用的存在

  • 函数式编程的运算不直接操作值,,而是由函子完成
  • 函子就是一个实现了map契约的对象
  • 我们可以把函子想象成一个盒子,盒子里面封装了一个值
  • 想要处理盒子中的值,我们需要给盒子的map方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理
  • 最终map方法返回一个包含新值所在的盒子(函子)

根据函子的定义我们创建一个函子

  1. // functor 函子
  2. class Container {
  3. constructor (value) {
  4. // 函子内部保存这个值。下划线是不想外部访问
  5. this._value = value
  6. }
  7. // map 方法接收一个处理值的函数
  8. map (fn) {
  9. return new Container(fn(this._value))
  10. }
  11. }

此时就已经创建了一个函子但是这是面向对象的方式来创建的,换成用函数式编程来写一个函子

  1. class Container {
  2. constructor (value) {
  3. this._value = value
  4. }
  5. map (fn) {
  6. return Container.of(fn(this._value))
  7. }
  8. static of (value) {
  9. return new Container(value)
  10. }
  11. }
  12. let x = Container.of(5).map(x => x + 1).map(x => x - 1)

但是这个函子还是存在一些问题,比如空值的时候就会报错, 会让我们的函子变的不纯,我们需要去拦截空值错误,我们创建一个方法去判断是否为空值,如果是控制我们直接返回一个空值的函子,如果有值再去处理,这个时候就需要使用MayBe函子

  1. let x = Container.of(null).map(x => x + 1).map(x => x - 1)

MayBe 函子

我们在编程的过程中可能会遇到很多错误,需要对这些错误做相应的处理,MayBe函子的作用就是可以对外部的空值情况做处理(控制副作用在允许的范围)

  1. // MayBe 函子
  2. class MayBe {
  3. constructor (value) {
  4. this._value = value
  5. }
  6. map (fn) {
  7. return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
  8. }
  9. isNothing () {
  10. return this._value === undefined || this._value === null
  11. }
  12. static of (value) {
  13. return new MayBe(value)
  14. }
  15. }
  16. let x = MayBe.of(null)
  17. .map(x => x + 1)
  18. .map(x => x - 1)
  19. console.log(x)

这个时候我们已经能正常执行了,但是现在出现了空值的函子,但是我们不知道那个地方出现了空值,所以我们创建两个函子一个是正常的处理一个是出现错误情况处理,正常的就按照正常的方式创建,错误的是是否我们把map方法改造一下让她不再处理回调函数,直接返回一个空值的MayBe函子,这样就记录下了错误信息Eitcher 函子就是来处理这种情况的

Either函子

Eitcher 类似于 if else 的处理,两者中的任何一个,异常会让函数变的不纯,Eitcher函子可以用来做异常处理

  1. // 因为是二选一,所以定义两个类 Left 和 Right
  2. // 记录错误信息的
  3. class Left {
  4. constructor (value) {
  5. this._value = value
  6. }
  7. map (fn) {
  8. return this
  9. }
  10. static of (value) {
  11. return new Left(value)
  12. }
  13. }
  14. // 正常处理
  15. class Rgiht {
  16. constructor (value) {
  17. this._value = value
  18. }
  19. map (fn) {
  20. return Rgiht.of(fn(this._value))
  21. }
  22. static of (value) {
  23. return new Rgiht(value)
  24. }
  25. }
  26. function parseJson (str) {
  27. try {
  28. return Rgiht.of(JSON.parse(str))
  29. } catch (err) {
  30. return Left.of({ message: err.message })
  31. }
  32. }
  33. // 故意传入错误的数据
  34. let r = parseJson('{ name: "2" }')
  35. r.map(x => x.name.toUpperCase())
  36. console.log(r)

IO 函子

IO 函子中的 _value 是一个函数, 这里把函数作为值来处理, IO 函子可以吧不纯的动作储存到_value中,延迟这个不纯的操作(惰性执行),保证当前的操作是纯的,延迟把不纯的操作到调用者来处理

  1. const fp = require('lodash/fp')
  2. // IO 函子
  3. class IO {
  4. constructor (fn) {
  5. this._value = fn
  6. }
  7. static of (value) {
  8. return new IO(function () {
  9. return value
  10. })
  11. }
  12. map (fn) {
  13. // 把当前的value 和传入的fn 函数组合成一个新的函数
  14. return new IO(fp.flowRight(fn, this._value))
  15. }
  16. }
  17. let r = IO.of(process).map(x => x.execPath)
  18. console.log(r)
  19. console.log(r._value())

IO 函子内部帮我们包装了一些函数,当我们传递函数的时候有可能这个函数是一个不纯的操作,不管这个函数纯与不纯,IO这个函子在执行的过程中它返回的这个结果始终是一个纯的操作,我们调用map的时候始终返回的是一个函子,但是IO函子这个_value属性他里面要去合并很多函数,所以他里面可能是不纯的,把这些不纯的操作延迟到了调用的时候,也就是我们通过IO函子控制了副作用的在可控的范围内发生

实现 liunx 下 cat 命令

  1. const fp = require('lodash/fp')
  2. // IO 函子
  3. class IO {
  4. constructor (fn) {
  5. this._value = fn
  6. }
  7. static of (value) {
  8. return new IO(function () {
  9. return value
  10. })
  11. }
  12. map (fn) {
  13. // 把当前的value 和传入的fn 函数组合成一个新的函数
  14. return new IO(fp.flowRight(fn, this._value))
  15. }
  16. }
  17. let r = IO.of(process).map(x => x.execPath)
  18. function readFile (fileName) {
  19. return new IO(() => fs.readFileSync(fileName, 'utf-8'))
  20. }
  21. function print (x) {
  22. return new IO(() => {
  23. console.log(x)
  24. return x
  25. })
  26. }
  27. let cat = fp.flowRight(print, readFile)
  28. console.log(cat('package.json')._value()._value())

此时IO函子出现了嵌套的问题,导致调用嵌套函子中的方法就必须要要._value()._value() 这样来执了,嵌套了几层就需要几层调用

Folktale

Folktale 是一个标准的函数式编程库,和lodash不同的是,他没有提供很多功能函数,只提供了一些函数式处理的操作,例如:compose、curry等,一些函子 Task、Either、MayBe等,

Folktale 中的currycompose的简单使用

  1. const { compose, curry } = require('folktale/core/lambda')
  2. const { toUpper, first } = require('lodash/fp')
  3. // 与lodash区别,第一个参数指明后面参数的个数
  4. let f = curry(2, (n1, n2) => n1 + n2)
  5. console.log(f(1, 2))
  6. // compose 就是函数组合 lodash 中的函数组合是 flowRight
  7. let f2 = compose(toUpper, first)
  8. console.log(f2(['one', 'two']))

Folktale 中的 task 函子

函子可以处理异步任务,在异步任务中会通往地狱之门的回调,而使用task 函子可以避免回调的嵌套,详细请看官方文档

  1. // Task 异步任务
  2. const { task } = require('folktale/concurrency/task')
  3. const { split, find } = require('lodash/fp')
  4. const fs = require('fs')
  5. function readFile (filename) {
  6. return task(resolver => {
  7. fs.readFile(filename, 'utf-8', (err, data) => {
  8. if (err) {
  9. resolver.reject(err)
  10. }
  11. resolver.resolve(data)
  12. })
  13. })
  14. }
  15. readFile('package.json')
  16. .map(split('\n'))
  17. .map(find(x => x.includes('version')))
  18. // 执行读取文件
  19. .run()
  20. .listen({
  21. onRejected(err) {
  22. console.log(err)
  23. },
  24. onResolved(value) {
  25. console.log(value)
  26. }
  27. })

Pointed函子

Pointed函子 是实现了of静态方法, of 方法是为了避免使用new 来创建对象,更深层次含义是of方法把值放到上下文Context(把值放到容器中,使用map 来处理值)

  1. class Container {
  2. constructor (value) {
  3. this._value = value
  4. }
  5. static of () {
  6. return new Container(value)
  7. }
  8. map (fn) {
  9. return new Container(fn(this._value))
  10. }
  11. }

Monad函子

解决函子嵌套的问题,Monad 函子是可以变扁的 Pointed 函子 IO(IO),一个函子如果具有joinof两个方法并遵循一些定律就是一个Monad

  1. class IO {
  2. constructor (fn) {
  3. this._value = fn
  4. }
  5. static of (value) {
  6. return new IO(function () {
  7. return value
  8. })
  9. }
  10. map (fn) {
  11. return new IO(fp.flowRight(fn, this._value))
  12. }
  13. join () {
  14. return this._value()
  15. }
  16. // 同时调用 join 和 map
  17. flatMap (fn) {
  18. return this.map(fn).join()
  19. }
  20. }
  21. function readFile (fileName) {
  22. return new IO(() => fs.readFileSync(fileName, 'utf-8'))
  23. }
  24. function print (x) {
  25. return new IO(() => {
  26. return x
  27. })
  28. }
  29. let r = readFile('package.json').flatMap(print).join()
  30. console.log(r)

当我们想要去调用一个方法,这个方法返回一值的时候我们去调用map方法,当我们想要去调用一个方法,这个方法返回一个函子的时候我们去调用flatMap方法

原文地址:https://kspf.xyz/archives/17

更多内容微信公众号搜索充饥的泡饭

小程序搜一搜开水泡饭的博客

JavaScript函数式编程之函子的更多相关文章

  1. 转: JavaScript函数式编程(二)

    转: JavaScript函数式编程(二) 作者: Stark伟 上一篇文章里我们提到了纯函数的概念,所谓的纯函数就是,对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环 ...

  2. 转:JavaScript函数式编程(三)

    转:JavaScript函数式编程(三) 作者: Stark伟 这是完结篇了. 在第二篇文章里,我们介绍了 Maybe.Either.IO 等几种常见的 Functor,或许很多看完第二篇文章的人都会 ...

  3. 转:JavaScript函数式编程(一)

    转:JavaScript函数式编程(一) 一.引言 说到函数式编程,大家可能第一印象都是学院派的那些晦涩难懂的代码,充满了一大堆抽象的不知所云的符号,似乎只有大学里的计算机教授才会使用这些东西.在曾经 ...

  4. JavaScript 函数式编程读书笔记2

    概述 这是我读<javascript函数式编程>的读书笔记,供以后开发时参考,相信对其他人也有用. 说明:虽然本书是基于underscore.js库写的,但是其中的理念和思考方式都讲的很好 ...

  5. JavaScript 函数式编程读书笔记1

    概述 这是我读<javascript函数式编程>的读书笔记,供以后开发时参考,相信对其他人也有用. 说明:虽然本书是基于underscore.js库写的,但是其中的理念和思考方式都讲的很好 ...

  6. 一文带你了解JavaScript函数式编程

    摘要: 函数式编程入门. 作者:浪里行舟 Fundebug经授权转载,版权归原作者所有. 前言 函数式编程在前端已经成为了一个非常热门的话题.在最近几年里,我们看到非常多的应用程序代码库里大量使用着函 ...

  7. javascript函数式编程和链式优化

    1.函数式编程理解 函数式编程可以理解为,以函数作为主要载体的编程方式,用函数去拆解.抽象一般的表达式 与命令式相比,这样做的好处在哪?主要有以下几点: (1)语义更加清晰 (2)可复用性更高 (3) ...

  8. JavaScript函数式编程(纯函数、柯里化以及组合函数)

    JavaScript函数式编程(纯函数.柯里化以及组合函数) 前言 函数式编程(Functional Programming),又称为泛函编程,是一种编程范式.早在很久以前就提出了函数式编程这个概念了 ...

  9. 在JavaScript函数式编程里使用Map和Reduce方法

    所有人都谈论道workflows支持ECMAScript6里出现的令人吃惊的新特性,因此我们很容易忘掉ECMAScript5带给我们一些很棒的工具方法来支持在JavaScript里进行函数编程,这些工 ...

随机推荐

  1. Apache Dolphinscheduler 1.3.x 系列配置文件指南

    前言 本文档为dolphinscheduler配置文件指南,针对版本为 dolphinscheduler-1.3.x 版本. 考虑公众号对markdown文件格式支持不那么友好的问题,建议大家在PC端 ...

  2. java-方法创建与使用

    1.方法: 1)封装一段特定的业务逻辑功能 2)方法尽可能的独立,一个方法只干一件事(低耦合) 3)方法可以被反复调用多次(高复用) 4)减少代码重复,有利于代码维护,有利于团队协作开发2.方法的定义 ...

  3. Spring Boot部署方法

    Spring Boot部署方法     网上搜到的部署方法无非是打成jar包,然后shell执行nohup java调用jar命令,或者是打成war包然后部署到tomcat或者jetty容器上面. S ...

  4. Dynamic CRM一对多关系的数据删除时设置自动删除关联的数据

    在业务实体中主子表非常常见,然后子表可能有会有自己的子表或者多对多关系,在删除的业务场景下,删除主数据,剩余的子数据就成了脏数据, 之前的做法是,监听主表的删除事件,然后在插件中找到其下的子表数据然后 ...

  5. react实战系列 —— React 中的表单和路由的原理

    其他章节请看: react实战 系列 React 中的表单和路由的原理 React 中的表单是否简单好用,受控组件和非受控是指什么? React 中的路由原理是什么,如何更好的理解 React 应用的 ...

  6. java.lang.UnsatisfiedLinkError报错

    是因为使用maven时,运行web项目时,在maven的依赖包没有打包到tomcat中(out目录中),所以要手动加上

  7. 【设计模式】Java设计模式 - 组合模式

    Java设计模式 - 组合模式 不断学习才是王道 继续踏上学习之路,学之分享笔记 总有一天我也能像各位大佬一样 原创作品,更多关注我CSDN: 一个有梦有戏的人 准备将博客园.CSDN一起记录分享自己 ...

  8. C#/VB.NET 在Word文档中插入分页符

    分页符是分页的一种符号,上一页结束以及下一页开始的位置.通查用于在指定位置强制分页.本文将分为两部分来介绍如何在Word文档中插入分页符.并附上C#/VB.NET以供参考,详情请阅读以下内容. 在特定 ...

  9. 图与A*算法

    同时根据每条边的实际情况,采用权重来表示他们的不同,权重可以是负的. 往这个图中添加顶点的成本非常昂贵,因为新的矩阵结果必须重新按照新的行/列创建,然后将已有的数据复制 到新的矩阵中. 图的数据结构: ...

  10. 我的Go并发之旅、01 并发哲学与并发原语

    注:本文所有函数名为中文名,并不符合代码规范,仅供读者理解参考. 上下文 上下文(Context)代表了程序(也可以是进程,操作系统,机器)运行时的环境和状态,联系程序整个生命周期与资源调用,是程序可 ...