Skip to content

Latest commit

 

History

History
772 lines (589 loc) · 31.1 KB

File metadata and controls

772 lines (589 loc) · 31.1 KB

五、范畴论

托马斯·沃森曾说过一句著名的话:“我认为世界上可能有五台电脑的市场”。那是在 1948 年。那时,每个人都知道计算机只会用于两件事:数学和工程。即使是科技界最有头脑的人也无法预测,有一天,计算机将能够把西班牙语翻译成英语,或者模拟整个天气系统。当时,最快的机器是 IBM 的 SSEC,以每秒 50 次乘法计时,显示终端直到 15 年后才到期,多处理意味着多个用户终端共享一个处理器。晶体管改变了一切,但科技界的远见卓识者们仍然没有抓住这一点。肯·奥尔森在 1977 年做了另一个著名的愚蠢预测,他说“没有理由任何人会想要一台电脑在他们的家里”。

现在对我们来说很明显,计算机不仅仅是科学家和工程师的专利,但这是事后诸葛亮。70 年前,机器可以做的不仅仅是数学,这种想法一点也不直观。沃森不仅没有意识到计算机如何改变一个社会,他也没有意识到数学的变革和进化的力量。

但是计算机和数学的潜力并没有在每个人身上消失。约翰·麦卡锡在 1958 年发明了 Lisp ,这是一种革命性的基于算法的语言,开创了计算的新时代。从一开始,Lisp 就有助于使用抽象层——编译器、解释器、虚拟化——来推动计算机从核心数学机器发展到今天的样子。

来自 Lisp 的是 Scheme ,JavaScript 的直接祖先。这就给我们带来了一个完整的循环。如果说计算机的核心是只做数学的机器,那么基于数学的编程范式会出类拔萃是理所当然的。

“数学”这个术语在这里不是用来描述计算机显然能做的“数字运算”,而是用来描述离散数学:对离散数学结构的研究,如逻辑语句或计算机语言指令。通过将代码视为离散的数学结构,我们可以将数学中的概念和思想应用于它。这就是函数式编程在人工智能、图形搜索、模式识别和计算机科学的其他重大挑战中如此重要的原因。

在这一章中,我们将试验其中的一些概念,以及它们在日常编程挑战中的应用。它们将包括:

  • 范畴理论
  • 变形
  • 函子
  • 我的基地
  • 承诺
  • 镜头
  • 功能组合

有了这些概念,我们将能够非常容易和安全地编写整个库和 API。我们将从解释范畴理论到用 JavaScript 正式实现它。

范畴论

范畴论是赋能功能构成的理论概念。品类理论和功能构成走在一起就像发动机排量和马力,就像 NASA 和航天飞机,就像好啤酒和一个杯子倒进去。基本上,你不能有一个没有另一个。

范畴理论简单来说

范畴理论真的不是一个太难的概念。它在数学中的地位足以填满整个研究生水平的大学课程,但它在计算机编程中的地位可以很容易地总结出来。

爱因斯坦曾经说过:“如果你不能向一个 6 岁的孩子解释,那你自己也不知道”。因此,本着给一个 6 岁的孩子解释的精神,范畴理论只是连接点。虽然它可能严重过度简化了范畴理论,但它确实很好地解释了我们需要知道的东西。

首先你需要知道一些术语。品类只是同类型的套装。在 JavaScript 中,它们是数组或对象,包含显式声明为数字、字符串、布尔值、日期、节点等的变量。变形 是纯函数,当给定一组特定的输入时,总是返回相同的输出。同态操作限于单个类别,而多态操作 可以对多个类别进行操作。比如同态函数乘法只对数字起作用,但是多态函数加法也可以对字符串起作用。

Category theory in a nutshell

下图显示了三个类别——a、b 和 c——以及两个变形—ɡ

范畴理论告诉我们,当我们有两个态射,其中第一个态射的范畴是另一个态射的预期输入时,那么它们可以组成*到下面:

Category theory in a nutshell

o g符号是吗啡tg 的组成。现在我们可以把这些点联系起来。

Category theory in a nutshell

这就是全部真正的样子,只是连接点。

类型安全

让我们连接一些点。类别包含两件事:

  1. 对象(在 JavaScript 中,类型)。
  2. Morphisms(在 JavaScript 中,只对类型起作用的纯函数)。

这些是数学家给范畴理论的术语,所以有一些不幸的术语超载了我们的 JavaScript 术语。对象 在范畴理论中更像是具有显式数据类型的变量,而不是像对象的 JavaScript 定义中那样的属性和值的集合。变形 只是使用这些类型的纯函数。

因此,将范畴理论的思想应用于 JavaScript 非常容易。在 JavaScript 中使用类别理论意味着每个类别使用一种特定的数据类型。数据类型有数字、字符串、数组、日期、对象、布尔值等等。但是,由于 JavaScript 中没有严格的类型系统,事情可能会出错。所以我们必须实现我们自己的方法来确保数据是正确的。

JavaScript 中有四种基本数据类型:数字、字符串、布尔值和函数。我们可以创建类型的安全函数,要么返回变量,要么抛出错误。这满足了范畴的对象公理

var str = function(s) {
  if (typeof s === "string") {
    return s;
  }
  else {
    throw new TypeError("Error: String expected, " + typeof s + " given.");   
  }
}
var num = function(n) {
  if (typeof n === "number") {
    return n;
  }
  else {
    throw new TypeError("Error: Number expected, " + typeof n + " given.");   
  }
}
var bool = function(b) {
  if (typeof b === "boolean") {
    return b;
  }
  else {
    throw new TypeError("Error: Boolean expected, " + typeof b + " given.");   
  }
}
var func = function(f) {
  if (typeof f === "function") {
    return f;
  }
  else {
    throw new TypeError("Error: Function expected, " + typeof f + " given.");   
  }
}

然而,这里有很多重复的代码,这不是很实用。相反,我们可以创建一个函数返回另一个函数,即类型安全函数。

var typeOf = function(type) {
  return function(x) {
    if (typeof x === type) {
      return x;
    }
    else {
      throw new TypeError("Error: "+type+" expected, "+typeof x+" given.");
    }
  }
}
var str = typeOf('string'),
  num = typeOf('number'),
  func = typeOf('function'),
  bool = typeOf('boolean');

现在,我们可以使用它们来确保我们的功能按预期运行。

// unprotected method:
var x = '24';
x + 1; // will return '241', not 25

// protected method
// plusplus :: Int -> Int
function plusplus(n) {
  return num(n) + 1;
}
plusplus(x); // throws error, preferred over unexpected output

让我们看一个更具体的例子。如果我们想检查由 JavaScript 函数Date.parse(),返回的 Unix 时间戳的长度,不是作为字符串而是作为数字,那么我们将不得不使用我们的str()函数。

// timestampLength :: String -> Int
function timestampLength(t) { return num(str(t).length); }
timestampLength(Date.parse('12/31/1999')); // throws error
timestampLength(Date.parse('12/31/1999')
  .toString()); // returns 12

像这样将一种类型显式转换为另一种类型(或相同类型)的函数称为 变形这满足了范畴论的态射公理。这些通过类型安全函数的强制类型声明以及使用它们的变形是我们在 JavaScript 中表示类别概念所需要的一切。

对象标识

还有一个其他重要的数据类型:对象。

var obj = typeOf('object');
obj(123); // throws error
obj({x:'a'}); // returns {x:'a'}

但是,对象不同。它们是可以遗传的。任何不是原语的东西——数字、字符串、布尔值和函数——都是对象,包括数组、日期、元素等等。

没有办法知道某物是什么类型的对象,就像从typeof关键字知道一个 JavaScript 对象’是什么子类型一样,所以我们必须随机应变。对象有一个toString()功能,我们可以为此劫持。

var obj = function(o) {
  if (Object.prototype.toString.call(o)==="[object Object]") {
    return o;
  }
  else {
    throw new TypeError("Error: Object expected, something else given."); 
  }
}

同样,有了所有的对象,我们应该实现一些代码重用。

var objectTypeOf = function(name) {
  return function(o) {
    if (Object.prototype.toString.call(o) === "[object "+name+"]") {
      return o;
    }
    else {
      throw new TypeError("Error: '+name+' expected, something else given.");
    }
  }
}
var obj = objectTypeOf('Object');
var arr = objectTypeOf('Array');
var date = objectTypeOf('Date');
var div = objectTypeOf('HTMLDivElement');

这些对我们的下一个主题非常有用:函子。

函子

态射是类型之间的映射,函子 是类别之间的映射。它们可以被认为是将值从容器中取出、变形、然后放入新容器的函数。第一个输入是类型的变形,第二个输入是容器。

函子的类型签名如下所示:

// myFunctor :: (a -> b) -> f a -> f b

上面写着,“给我一个取a返回b的函数和一个包含a的盒子,我会返回一个包含b的盒子。

创建函子

原来我们已经有了一个函子:map()。它获取容器(一个数组)中的值,并对其应用一个函数。

[1, 4, 9].map(Math.sqrt); // Returns: [1, 2, 3]

然而,我们需要把它写成一个全局函数,而不是数组对象的一个方法。这将允许我们以后编写更干净、更安全的代码。

// map :: (a -> b) -> [a] -> [b]
var map = function(f, a) {
  return arr(a).map(func(f));
}

这个例子看起来像是一个做作的包装器,因为我们只是在搭载map()函数。但是它是有目的的。它为其他类型的地图提供了一个模板。

// strmap :: (str -> str) -> str -> str
var strmap = function(f, s) {
  return str(s).split('').map(func(f)).join('');
}

// MyObject#map :: (myValue -> a) -> a
MyObject.prototype.map(f{
  return func(f)(this.myValue);
}

数组和函子

数组是函数式 JavaScript 中处理数据的首选方式。

有没有更简单的方法来创建已经赋给态射的函子?是的,它叫做arrayOf。当你传入一个期望一个整数并返回一个数组的态射时,你得到一个期望一个整数数组并返回一个数组的态射。

它本身不是函子,但它允许我们从态射创建函子。

// arrayOf :: (a -> b) -> ([a] -> [b])
var arrayOf = function(f) {
  return function(a) {
    return map(func(f), arr(a));
  }
}

以下是如何使用态射创建函子:

var plusplusall = arrayOf(plusplus); // plusplus is our morphism
console.log( plusplusall([1,2,3]) ); // returns [2,3,4]
console.log( plusplusall([1,'2',3]) ); // error is thrown

arrayOf函子的有趣特性是它也适用于类型安全。当您传入字符串的类型安全函数时,您会得到字符串数组的类型安全函数。类型安全被视为身份功能变形。这个对于确保数组包含所有正确的类型非常有用。

var strs = arrayOf(str);
console.log( strs(['a','b','c']) ); // returns ['a','b','c']
console.log( strs(['a',2,'c']) ); // throws error

功能组合,重访

函数是另一种类型的原语,我们可以为其创建一个函子。那个函子叫做fcompose。我们将函子定义为从容器中获取一个值并对其应用一个函数的东西。当那个容器是一个函数时,我们只是调用它来获取它的内部值。

我们已经知道函数组合是什么,但是让我们看看它们在范畴理论驱动的环境中能做什么。

函数组合是关联的。如果你的高中代数老师像我一样,她教你什么是属性*,而不是它能做什么*。实际上,组合是关联属性可以做的事情。**

**Function compositions, revisited

Function compositions, revisited

我们可以做任何内部组合,不管它是如何分组的。这不能与交换性质混淆。o g并不总是等于g o。换句话说,字符串第一个单词的反义词与字符串反义词的第一个单词不同。

这一切意味着,只要每个函数的输入都来自前一个函数的输出,那么应用哪些函数、以什么顺序应用都没有关系。但是等等,如果右边的函数依赖左边的函数,那么就不能只有一个求值顺序吗?从左到右?没错,但是如果它被封装了,那么我们可以控制它,无论我们觉得如何合适。这就是 JavaScript 中赋予懒惰评估的力量。

Function compositions, revisited

让我们重写函数组合,不是作为函数原型的扩展,而是作为一个独立的函数,让我们从中获得更多。基本形式如下:

var fcompose = function(f, g) {
  return function() {
    return f.call(this, g.apply(this, arguments));
  };
};

但是我们需要它来处理任何数量的输入。

var fcompose = function() {
  // first make sure all arguments are functions
  var funcs = arrayOf(func)(arguments);

  // return a function that applies all the functions
  return function() {
    var argsOfFuncs = arguments;
    for (var i = funcs.length; i > 0; i -= 1) {
      argsOfFuncs  = [funcs[i].apply(this, args)];
    }
    return args[0];
  };
};

// example:
var f = fcompose(negate, square, mult2, add1);
f(2); // Returns: -36

既然我们已经封装了这些功能,我们就可以控制它们了。我们可以重写组合函数,使得每个函数接受另一个函数作为输入,存储它,并返回一个执行相同操作的对象。我们可以接受一个数组作为输入,用它做一些事情,然后为每个操作返回一个新的数组,我们可以为源中的每个元素接受一个单个数组,执行所有组合的操作(每个map()filter()等等,组合在一起),最后将结果存储在一个新的数组中。这是通过函数组合的懒惰评估。没有理由在这里重新发明轮子。许多图书馆都很好地实现了这个概念,包括Lazy.jsBacon.jswu.js图书馆。

由于这种不同的模型,我们可以做更多的事情:异步迭代、异步事件处理、惰性评估,甚至自动并行化。

自动并行化?在计算机科学行业有一个词来形容它:不可能。但是真的不可能吗?摩尔定律的下一个进化飞跃可能是一个编译器,它可以为我们并行化我们的代码,函数合成可能是它吗?

不,不是那样的。JavaScript 引擎才是真正进行并行化的引擎,它不是自动进行的,而是使用经过深思熟虑的代码。Compose 只是让引擎有机会将其拆分为并行进程。但这本身就很酷。

摩纳哥人

Monads 是帮助你组合函数的工具。

像基本类型一样,单子是可以用作函子“触及”的容器的结构。函子抓取数据,对它做一些事情,把它放入一个新的单子,然后返回它。

我们将关注三个单子:

  • 我的基地
  • 承诺
  • 镜头

所以除了数组(map)和函数(compose),我们还有五个函子(map,compose,也许,promise 和 lens)。这些只是存在的许多其他函子和单子中的一些。

也许吧

也许允许我们优雅地处理可能为空且有默认值的数据。也许是一个变量,要么有值,要么没有值。对打电话的人来说没关系。

就其本身而言,这似乎没什么大不了的。每个人都知道空检查很容易通过if-else语句完成:

if (getUsername() == null ) {
  username = 'Anonymous') {
else {
  username = getUsername();
}

但是通过函数式编程,我们正在摆脱程序性的、逐行的做事方式,转而使用函数和数据管道。如果为了检查值是否存在,我们不得不中断中间的链,我们将不得不创建临时变量并编写更多的代码。也许只是帮助我们保持逻辑在管道中流动的工具。

为了实现 maybes,我们首先需要创建一些构造函数。

// the Maybe monad constructor, empty for now
var Maybe = function(){}; 

// the None instance, a wrapper for an object with no value
var None = function(){}; 
None.prototype = Object.create(Maybe.prototype);
None.prototype.toString = function(){return 'None';};

// now we can write the `none` function
// saves us from having to write `new None()` all the time
var none = function(){return new None()};

// and the Just instance, a wrapper for an object with a value
var Just = function(x){return this.x = x;};
Just.prototype = Object.create(Maybe.prototype);
Just.prototype.toString = function(){return "Just "+this.x;};
var just = function(x) {return new Just(x)};

最后我们可以写maybe函数。它返回一个新的函数,要么什么都不返回,要么可能返回。是一个函子

var maybe = function(m){
  if (m instanceof None) {
    return m;
  }
  else if (m instanceof Just) {
    return just(m.x);   
  }
  else {
    throw new TypeError("Error: Just or None expected, " + m.toString() + " given."); 
  }
}

我们也可以创建一个函子生成器,就像我们创建数组一样。

var maybeOf = function(f){
  return function(m) {
    if (m instanceof None) {
      return m;
    }
    else if (m instanceof Just) {
      return just(f(m.x));
    }
    else {
      throw new TypeError("Error: Just or None expected, " + m.toString() + " given."); 
    }
  }
}

所以Maybe是单子,maybe是函子,maybeOf返回一个已经赋给态射的函子。

我们还需要一件事才能继续前进。我们需要给Maybe monad 对象添加一个方法,帮助我们更直观地使用它。

Maybe.prototype.orElse = function(y) {
  if (this instanceof Just) {
    return this.x;
  }
  else {
    return y;
  }
}

在其原始形式中,可能可以直接使用。

maybe(just(123)).x; // Returns 123
maybeOf(plusplus)(just(123)).x; // Returns 124
maybe(plusplus)(none()).orElse('none'); // returns 'none'

任何返回一个随后被执行的方法的都足够复杂,足以自找麻烦。所以我们可以通过调用我们的curry()函数来让它更干净一点。

maybePlusPlus = maybeOf.curry()(plusplus);
maybePlusPlus(just(123)).x; // returns 123
maybePlusPlus(none()).orElse('none'); // returns none

但是当抽象出直接调用none()just()函数的肮脏业务时,也许的真正力量就会变得清晰起来。我们将使用一个示例对象User来实现这一点,该对象使用“可能”作为用户名。

var User = function(){
  this.username = none(); // initially set to `none`
};
User.prototype.setUsername = function(name) {
  this.username = just(str(name)); // it's now a `just
};
User.prototype.getUsernameMaybe = function() {
  var usernameMaybe = maybeOf.curry()(str);
  return usernameMaybe(this.username).orElse('anonymous');
};

var user = new User();
user.getUsernameMaybe(); // Returns 'anonymous'

user.setUsername('Laura');
user.getUsernameMaybe(); // Returns 'Laura'

现在我们有了一个强大而安全的方法来定义默认值。记住这个User对象,因为我们将在本章稍后使用它。

承诺

承诺的本质是对不断变化的环境保持免疫。

-弗兰克·安德伍德,纸牌屋

在函数编程中,我们经常使用管道和数据流:函数链,其中每个函数产生一个数据类型,供下一个函数使用。然而,这些函数中有许多是异步的:readFile、事件、AJAX 等等。我们如何修改这些函数的返回类型来指示结果,而不是使用延续传递样式和深度嵌套回调?通过把它们包装在承诺里。

承诺就像回调的功能等价物。很明显,回调并不都是功能性的,因为如果不止一个函数变异了相同的数据,那么可能会有竞争条件和错误。承诺可以解决这个问题。

你应该用承诺来扭转这种局面:

fs.readFile("file.json", function(err, val) {
  if( err ) {
    console.error("unable to read file");
  }
  else {
    try {
      val = JSON.parse(val);
      console.log(val.success);
    }
    catch( e ) {
      console.error("invalid json in file");
    }
  }
});

进入下面的代码片段:

fs.readFileAsync("file.json").then(JSON.parse)
  .then(function(val) {
    console.log(val.success);
  })
  .catch(SyntaxError, function(e) {
    console.error("invalid json in file");
  })
  .catch(function(e){
    console.error("unable to read file")
  });

前面的代码来自于 蓝鸟的自述文件:一个性能特别好的全功能 承诺/A+ 实现。 Promises/A+ 是一个用 JavaScript 实现 Promises 的规范。考虑到它目前在 JavaScript 社区中的争论,我们将把实现留给 Promises/A+ 团队,因为它比可能要复杂得多。

但是这里有一个部分实现:

// the Promise monad
var Promise = require('bluebird');

// the promise functor
var promise = function(fn, receiver) {
  return function() {
    var slice = Array.prototype.slice,
    args = slice.call(arguments, 0, fn.length - 1),
    promise = new Promise();
    args.push(function() {
      var results = slice.call(arguments),
      error = results.shift();
      if (error) promise.reject(error);
      else promise.resolve.apply(promise, results);
    });
    fn.apply(receiver, args);
    return promise;
  };
};

现在我们可以使用promise()函子将接受回调的函数转换成返回承诺的函数。

var files = ['a.json', 'b.json', 'c.json'];
readFileAsync = promise(fs.readFile);
var data = files
  .map(function(f){
    readFileAsync(f).then(JSON.parse)
  })
  .reduce(function(a,b){
    return $.extend({}, a, b)
  });

镜片

程序员真正喜欢单子的另一个原因是它们让编写库变得非常容易。为了探索这一点,让我们用更多的函数来扩展我们的User对象来获取和设置值,但是,我们将使用镜头来代替使用获取器和设置器。

镜片是一级吸气剂和定形剂。它们不仅允许我们获取和设置变量,还允许我们在上面运行函数。但是,它们并没有改变数据,而是克隆并返回由函数修改的新数据。它们强制数据不可变,这对于安全性和一致性以及库来说都是非常好的。无论应用是什么,它们都非常适合优雅的代码,只要引入额外的阵列副本不会对性能造成严重影响。

在编写 lens()函数之前,我们先来看看它是如何工作的。

var first = lens(
  function (a) { return arr(a)[0]; }, // get
  function (a, b) { return [b].concat(arr(a).slice(1)); } // set
);
first([1, 2, 3]); // outputs 1
first.set([1, 2, 3], 5); // outputs [5, 2, 3]
function tenTimes(x) { return x * 10 }
first.modify(tenTimes, [1,2,3]); // outputs [10,2,3]

下面是lens()函数的工作原理。它返回一个定义了 get、set 和 mod 的函数。lens()函数本身就是一个函子。

var lens = fuction(get, set) {
  var f = function (a) {return get(a)};
  f.get = function (a) {return get(a)}; 
  f.set = set;
  f.mod = function (f, a) {return set(a, f(get(a)))};
  return f;
};

我们来举个例子。我们将从前面的例子中扩展我们的User对象。

// userName :: User -> str
var userName = lens(
  function (u) {return u.getUsernameMaybe()}, // get
  function (u, v) { // set
    u.setUsername(v);  
    return u.getUsernameMaybe(); 
  }
);

var bob = new User();
bob.setUsername('Bob');
userName.get(bob); // returns 'Bob'
userName.set(bob, 'Bobby'); //return 'Bobby'
userName.get(bob); // returns 'Bobby'
userName.mod(strToUpper, bob); // returns 'BOBBY'
strToUpper.compose(userName.set)(bob, 'robert'); // returns 'ROBERT'
userName.get(bob); // returns 'robert'

jQuery 是一个单子

如果你认为所有这些关于范畴、函子和单子的抽象胡言乱语都没有实际应用,那就再想想。jQuery 是一个流行的 JavaScript 库,它为使用 HTML 提供了一个增强的界面,实际上是一个单子库。

jQuery对象是单子,其方法是函子。真的,它们是一种特殊类型的函子,叫做 内函子内函子是返回与输入相同类别的函子,即F :: X -> X。每个jQuery方法取一个jQuery对象,返回一个jQuery对象,允许方法被链接,它们会有类型签名jFunc :: jquery-obj -> jquery-obj

$('li').add('p.me-too').css('color', 'red').attr({id:'foo'});

这也是 jQuery 插件框架的强大之处。如果插件以一个jQuery对象作为输入,返回一个作为输出,那么就可以插入到链中。

让我们看看 jQuery 是如何实现这一点的。

单子是函子“触及”以获取数据的容器。这样,数据可以由库保护和控制。jQuery 通过它的许多方法提供对底层数据的访问,底层数据是一组包装好的 HTML 元素。

jQuery对象本身是匿名函数调用的结果。

var jQuery = (function () {
  var j = function (selector, context) {
    var jq-obj = new j.fn.init(selector, context);
    return jq-obj;
  };

  j.fn = j.prototype = {
    init: function (selector, context) {
      if (!selector) {
        return this;
      }
    }
  };
  j.fn.init.prototype = j.fn;
  return j;
})();

在这个高度简化的 jQuery 版本中,它返回了一个定义j对象的函数,实际上只是一个增强的init构造函数。

var $ = jQuery(); // the function is returned and assigned to `$`
var x = $('#select-me'); // jQuery object is returned

就像函子从容器中取出值一样,jQuery 包装 HTML 元素并提供对它们的访问,而不是直接修改 HTML 元素。

jQuery 并不经常宣传这一点,但是它有自己的map()方法,用于将 HTML 元素对象从包装器中提升出来。就像fmap()方法一样,将元素提起,用它们做一些事情,然后将它们放回容器中。这就是 jQuery 的许多命令在后端工作的方式。

$('li').map(function(index, element) {
  // do something to the element
  return element
});

另一个处理 HTML 元素的库,Prototype,并不是这样工作的。原型通过助手直接改变 HTML 元素。因此,这在 JavaScript 社区中并不公平。

实施类别

时间差不多了我们正式把范畴理论定义为 JavaScript 对象。类别是对象(类型)和变形(只对这些类型起作用的函数)。这是一种极其高级的、完全声明式的编程方式,但它确保了代码极其安全可靠——非常适合担心并发性和类型安全的 API 和库。

首先,我们需要一个函数来帮助我们创建变形。我们称之为homoMorph(),因为它们将是同态的。它将返回一个期望传入函数的函数,并根据输入生成函数的组合。输入是态射接受作为输入并给出作为输出的类型。就像我们的类型签名,也就是// morph :: num -> num -> [num],只有最后一个是输出。

var homoMorph = function( /* input1, input2,..., inputN, output */ ) {
  var before = checkTypes(arrayOf(func)(Array.prototype.slice.call(arguments, 0, arguments.length-1)));
  var after = func(arguments[arguments.length-1])
  return function(middle) {
    return function(args) {
      return after(middle.apply(this, before([].slice.apply(arguments))));   
    }
  }
}

// now we don't need to add type signature comments
// because now they're built right into the function declaration
add = homoMorph(num, num, num)(function(a,b){return a+b})
add(12,24); // returns 36
add('a', 'b'); // throws error
homoMorph(num, num, num)(function(a,b){
  return a+b;
})(18, 24); // returns 42

homoMorph()功能相当复杂。它使用闭包(参见第 2 章函数编程基础)来返回接受函数并检查其输入和输出值的类型安全的函数。为此,它依赖于一个助手函数:checkTypes,定义如下:

var checkTypes = function( typeSafeties ) {
  arrayOf(func)(arr(typeSafeties));
  var argLength = typeSafeties.length;
  return function(args) {
    arr(args);
    if (args.length != argLength) {
      throw new TypeError('Expected '+ argLength + ' arguments');
    }
    var results = [];
    for (var i=0; i<argLength; i++) {
      results[i] = typeSafeties[i](args[i]);   
    }
    return results;
  }
}

现在让我们正式定义一些同态。

var lensHM = homoMorph(func, func, func)(lens);
var userNameHM = lensHM(
  function (u) {return u.getUsernameMaybe()}, // get
  function (u, v) { // set
    u.setUsername(v);
    return u.getUsernameMaybe(); 
  }
)
var strToUpperCase = homoMorph(str, str)(function(s) {
  return s.toUpperCase();
});
var morphFirstLetter = homoMorph(func, str, str)(function(f, s) {
  return f(s[0]).concat(s.slice(1));
});
var capFirstLetter = homoMorph(str, str)(function(s) {
  return morphFirstLetter(strToUpperCase, s)
});

最后,我们可以把它带回家。以下示例包括函数合成、镜头、同态等。

// homomorphic lenses
var bill = new User();
userNameHM.set(bill, 'William'); // Returns: 'William'
userNameHM.get(bill); // Returns: 'William'

// compose
var capatolizedUsername = fcompose(capFirstLetter,userNameHM.get);
capatolizedUsername(bill, 'bill'); // Returns: 'Bill'

// it's a good idea to use homoMorph on .set and .get too
var getUserName = homoMorph(obj, str)(userNameHM.get);
var setUserName = homoMorph(obj, str, str)(userNameHM.set);
getUserName(bill); // Returns: 'Bill'
setUserName(bill, 'Billy'); // Returns: 'Billy'

// now we can rewrite capatolizeUsername with the new setter
capatolizedUsername = fcompose(capFirstLetter, setUserName);
capatolizedUsername(bill, 'will'); // Returns: 'Will'
getUserName(bill); // Returns: 'will'

前面的代码是非常声明性的、安全的、可靠的和可靠的。

代码声明性意味着什么?在命令式编程中,我们编写指令序列,告诉机器如何做我们想做的事情。在函数式编程中,我们描述值之间的关系,这些值告诉机器我们想要它计算什么,机器计算出指令序列来实现它。函数式编程是声明性的。

整个库和 API 可以通过这种方式构建,允许程序员自由编写代码,而不用担心并发性和类型安全,因为这些担心都在后端处理。

总结

大约每 2000 人中就有一人患有通感症,这是一种神经现象,其中一种感觉输入会渗入另一种感觉输入。最常见的形式包括用字母分配颜色。然而,还有一种更罕见的形式,句子和段落与品味和感情联系在一起。

对于这些人来说,他们不会逐字逐句地阅读。他们查看整个页面/文档/程序,并对它的味道有所了解——不是在嘴里,而是在的脑海中。然后他们把课文的各个部分像拼图一样拼在一起。

这就是编写完全声明性代码的感觉:描述值之间关系的代码,告诉机器我们想要它计算什么。程序的各个部分不是按行顺序的指令。联觉可能能够自然地做到这一点,但只要稍加练习,任何人都可以学会如何将关系拼图拼在一起。

在这一章中,我们研究了应用于函数式编程的几个数学概念,以及它们如何允许我们在数据之间建立关系。接下来,我们将探讨递归和 JavaScript 中的其他高级主题。***