Mostly adequate guide to FP reading notes

date
Oct 6, 2020
slug
FP
status
Published
tags
FP
summary
函数式编程指北
type
Post
 

Functor

functor的作用对象是一个容器,
functor apply container 类似于 function map array
 

Container / Identity

但是Container不会要求像array那样的数据结构,就像container的名字那样,他可以装任何东西。
class Container {
  constructor(x) {
    this.$value = x;
  }

  static of(x) {
    return new Container(x);
  }
}

Container.of(3)
// => Container(3)

Container.of('Container')
// => Container('Container')

Container.of(Container.of({name: "yoda"}))
//=> Container(Container({name: "yoda" }))
//禁止套娃
 
有了Container,就需要functor来操作Container里的东西了。我们需要让Container提供一个接口
Container.prototype.map = function(f){
	return Container.of(f(this._value))
}

Container.of(2).map(_ => _ + 2)
// Container(4)

Container.of('jojo').map(s => s.toUpperCase())
// Container('JOJO')

Container.of('Experienza').map(concat('doro')).map(_.prop('length'))
// Container(14)
 
这么做的好处就是数据一旦放入Container之后,我们只需要通过map(transform)的方式来实现逻辑,而不需要关心和维护中间变量和结果变量。
 
把值装进一个容器,而且只使用map来处理它 ,即让容器自己去运用函数
 

Maybe

Identity是一种最简单朴素的盒子了,如果我们把Identity加上一个新功能(可以一眼判断盒子是不是空的),就变成Maybe这个稍微厉害一点的盒子了

class Maybe {
  static of(x) {
    return new Maybe(x);
  }

	constructor(x) {
    this.$value = x;
  }
	// 以上都和Container一摸一样嘛

	// 判断盒子是不是空的
  get isNothing() {
    return this.$value === null || this.$value === undefined;
  }

	// 相应的给map也增加新功能,可以避免在map的时候遇到空值了
  map(fn) {
    return this.isNothing ? this : Maybe.of(fn(this.$value));
  }

  inspect() {
    return this.isNothing ? 'Nothing' : `Just(${inspect(this.$value)})`;
  }
}
在实际使用中,Maybe也比Id有更高的容错,再遇到空值的时候可以不崩
Maybe.of(null).map(match(/a/ig))
// => Maybe(null)

Maybe.of({name: "Joseph"}).map(_.prop("stand"));
//=> Maybe(null)

Maybe.of({name: "Jobana", stand: 'golden experience'}).map(_.prop("stand")).map(s => s.toUpperCase()));
//=> Maybe('GOLDEN EXPERIENCE')
 
点记法 ⇒ point free,
//  map :: Functor f => (a -> b) -> f a -> f b
const map = curry((f, any_functor) => {
	return any_functor.map(f)
});
 

Examples

//  safeHead :: [a] -> Maybe(a)
const safeHead = function(xs) {
	return Maybe.of(xs[0])
}
safeHead 比起一般的head类似,但是增加了类型安全。
但是归根结底Maybe并不会处理错误,它只是诚实的预告它可能的失败并抛出风险,即返回一个Maybe(null) 来通知我们。所以我们要被迫处理狡猾的null了
//  withdraw :: Number -> Account -> Maybe(Account)
var withdraw = curry(function(amount, account) {
  return account.balance >= amount ?
    Maybe.of({balance: account.balance - amount}) :
    Maybe.of(null);
});

//  finishTransaction :: Account -> String
var finishTransaction = compose(remainingBalance, updateLedger); // <- 假定这两个函数已经在别处定义好了

//  getTwenty :: Account -> Maybe(String)
var getTwenty = compose(map(finishTransaction), withdraw(20));


getTwenty({ balance: 200.00});
// Maybe("Your balance is $180.00")

getTwenty({ balance: 10.00});
// Maybe(null)
以上代码模拟一个去银行取钱的场景,要是钱不够,withDraw会嗤之以鼻的返回我们一个Maybe(null),这样map会切断后续代码的执行,即finishTransaction。(如果取款失败,则不更新或显示账户余额
 

释放容器中的值 maybe

如果一个程序运行之后没有可观察到的作用,那它到底运行了没有?他有没有只是浪费几个cpu周期然后去睡觉了...
我们把数据放到容器中,在map若干个transform的逻辑,但是到最终,我们还是需要把它从容器中取出来,看看他是否最终如我们逻辑所预期的那样。
如果我们的值没有完成它的使命,一般来说是由于代码的分支逻辑造成的,我们的代码,在某个特定时间段有两种状态,而且应该保持这种状态直到最后一个函数为止。
就是即使实际上有很多分支逻辑,但是最终看上去要是一个线性的工作流
因此对于容器里的值,我们想返回一个自定义的值,并且还能继续执行后面的代码的话,我们需要借助一个帮助函数maybe来实现
//  maybe :: b -> (a -> b) -> Maybe a -> b

const maybe = currey((x, f, m) => {
	return m.isNothing() ? x : f(m._value)
})

//  getTwenty :: Account -> String
var getTwenty = compose(
  maybe("You're broke!", finishTransaction), withdraw(20)
);


getTwenty({ balance: 200.00});
// "Your balance is $180.00"

getTwenty({ balance: 10.00});
// "You're broke!"
 
这样maybe两边的返回值都是一个类型了。maybe是我们避免了map中的命令式的 if / else 语句
 

纯的错误处理 Either

以前我们学到的throw和catch来进行错误处理并不是pure的,我们没有收到任何返回值,反而得到一个error。
在函数式编程中我们可以使用Either来处理错误逻辑
class Either {
  static of(x) {
    return new Right(x);
  }

  constructor(x) {
    this.$value = x;
  }
}

class Left extends Either {
  map(f) {
    return this;
  }

  inspect() {
    return `Left(${inspect(this.$value)})`;
  }
}

class Right extends Either {
  map(f) {
    return Either.of(f(this.$value));
  }

  inspect() {
    return `Right(${inspect(this.$value)})`;
  }
}

const left = x => new Left(x);
// Left和Right的构造方法和of是一摸一样的,区别只是他们应对transform的map方法不一致
Left和Right是Either的抽象类型的两个子类,可以看一下他俩运行起来是什么样的。
 
Right.of("rain").map(function(str){ return "b"+str; });
// Right("brain")

Left.of("rain").map(function(str){ return "b"+str; });
// Left("rain")

Right.of({host: 'localhost', port: 80}).map(_.prop('host'));
// Right('localhost')

Left.of("rolls eyes...").map(_.prop("host"));
// Left('rolls eyes...')
Left看上去不太聪明的样子,map啥都被它无视。而Right的作用和Identity一样,map啥就做啥。
 
但是傻也有傻的好处(bushi,这样我们可以在Left的内部嵌入一个错误信息(可怜的傻子只能记住一句话...
const moment = require('moment');

//  getAge :: Date -> User -> Either(String, Number)
const getAge = curry((now ,user) => {
	const birthdate = moment(user.birthdate, 'YYYY-MM-DD');
	if (!birthdate.isValid()) {
		return Left.of('Birth date could not be parsed')
	}
	return Right.of(now.diff(birthdate, 'year'))
})

getAge(moment(), {birthdate: '1998-06-129'});
// Right(23)

getAge(moment(), {birthdate: 'balloons!'});
// Left("Birth date could not be parsed")


// fortune :: Number -> String
const fortune = compose(concat('If you survive, you will be '), toString, add(1));

// zoltar :: User -> Either(String, _)
const zoltar = compose(map(console.log), map(fortune), getAge(moment()));

zoltar({ birthDate: '2005-12-12' });
// 'If you survive, you will be 10'
// Right(undefined)

zoltar({ birthDate: 'balloons!' });
// Left('Birth date could not be parsed')
有点类似与Maybe里面是个null的时候,当返回一个Left的时候就让程序短路。但比Maybe(null)更厉害一点的是,多亏了Left这个傻子记住的信息,我们在程序没有符合预期时有了一点头绪。
 
如果birthdate不合法,我们会收到一个清楚的错误消息的Left,而且这个消息也是稳稳当当呆在它的容器中。这样的感觉像是程序虽然在抛错,但是是以一种温和平静的方式,而不是像一个小孩子一样,有什么不对劲就大喊大叫(指throw error
 

either

类似与Maybe类型和maybe函数,Either类型也有对应的either函数。他们的作用也类似,只不过一个接受两个返回类型相同的函数作为参数,一个接受一个函数和一个静态变量作为参数
// either :: (a -> c) -> (b -> c) -> Either a b -> c
const either = curry((f, g, e) => {
  let result;

  switch (e.constructor) {
    case Left:
      result = f(e.$value);
      break;

    case Right:
      result = g(e.$value);
      break;

    // No Default
  }

  return result;
});

// zoltar :: User -> _
const zoltar = compose(console.log, either(id, fortune), getAge(moment()));
// getAge的返回类型为Either(String, Number),正好传入either的两个参数id和fortuneid的签名是string -> string,fortune的签名是number -> string,由于either的特性,最终又统一成了string,最后传给log

zoltar({ birthDate: '2005-12-12' });
// 'If you survive, you will be 10'
// undefined

zoltar({ birthDate: 'balloons!' });
// 'Birth date could not be parsed'
// undefined
 

处理Effects IO

我们之前见过一个奇怪的纯函数的例子,这个函数包含了副作用,但是我们把它包裹在另一个函数里面,然后就声称它是一个纯函数了
// getFromStorage :: String -> (_ -> String)
const getFromStorage = key => () => localStorage[key];
如果我们没把 key ⇒ localStorage[key]包裹住,它的输出就可能根据localStorage异步的各种情况变得不稳定。但是我们用key ⇒ () ⇒ localStorage[key]之后,我们传入任意key,都能得到一个() ⇒ localStorage[key]的函数,输出和输入完全mapping,简直太纯了(bushi
 
实际上,这其实并没有什么用处,就像把手办回盒,不能拿出来舔的话就没有什么意义了。如果有什么办法可以隔着盒子舔到的话...办法是有的,那就是 IO
class IO {
	static of(x) {
		return new IO(() => x);
	}
	
	constructor(fn){
		this.$value = fn;
	}

	map(fn) {
		return new IO(compose(fn, this.$value));
	}

	inspect() {
		return `IO(${inspect(this.$value)})`;
	}
}
IO和之前的functor的区别是它的$value是一个函数,不过我们最好忽略细节,先就不把它当作一个函数。
IO的具体操作和getFromStorage例子一样,IO把不纯的动作放到包裹函数中来延迟执行非纯的操作。因此IO的value是被包裹函数的返回值,而不是包裹函数本身。
这在IO的of上体现的很明显,new IO(() => x) 仅仅是为了避免执行,其实我们得到的是 IO(x)。
简单来说,我们把IO中的$value作为最终结果。但是实际上,直到你释放effects的时候你才能知道最终结果,即$value是啥。(类似于这种情景,想舔手办的时候,记录一条舔的操作,到最终要舔的时候,把记录的操作都执行一边
 

© Itisssennsinn 2020 - 2025