【问题标题】:What would be a good example of an endofunctor that is not the identity functor?什么是不是恒等函子的内函子的好例子?
【发布时间】:2017-07-17 04:28:29
【问题描述】:

在Professor Frisby Introduces Composable Functional JavaScript中引入了恒等函子:

const Box = x => 
   ({ 
       map:  f => Box(f(x)),
       fold: f => f(x)           // for testing
   })

我花了一天的大部分时间来理解函子以及为什么上面的 JavaScript 代码实际上是恒等函子。所以我想我会改变它以获得一个不是恒等函子的“真实”函子。我想出了这个:

const Endo = x =>
   ({ 
       map:  f => Endo(f(x).split('')),
       fold: f => f(x).split('') // for testing
   })

我的理由是 Box、Id_Box: Box -> BoxId_Box f = f。 Endo 也会映射到自身,但 Endo(f): Endo(x) -> Endo(y)(如果 f: x -> y)。

我走对了吗?

编辑: 将 string 替换为更通用的 x,就像在原始示例中一样。

【问题讨论】:

  • 预期结果是什么?你想达到什么目的?
  • 我认为您的 Endo 甚至不是有效的仿函数,因为它仅适用于从数组创建字符串的函数 f。 Functor 需要处理任何类型的函数。
  • 您在了解什么是函子的过程中遇到过其他函子吗?如果没有,我建议你继续讲课,听起来 Frisby 教授会在接下来的课程中介绍更多的函子。
  • 在编程的上下文中,术语 endofunctor 只是描述了一个 (a -> b) -> (f a -> f b) 类型的函子,它使用纯函数将类型映射到类型。所以任何具有给定类型的函子都是一个内函子,Identity 只是最简单的一个。 Endofunctor 很有趣,因为它们的计算策略适用于任何类型,也就是说它们是多态的。因此,将您的 Box 函子限制为 Strings 并不是特别有用。
  • @toraritte 用于编程目的,all functors are endofunctors。所以不,理解这种差异对你毫无帮助。函子是一种可以映射的数据结构,它尊重恒等函数和组合。就是这样。不要过于复杂。

标签: javascript functional-programming category-theory


【解决方案1】:

正如this answer 中指出的那样,作为程序员,我们可以将所有函子视为内函子,因此不要太关注差异。

至于函子是什么,简单来说就是

  1. 一种数据结构(在您的示例中为 Box)
  2. 可以支持映射操作(想想Array.prototype.map
  3. 并且映射操作尊重身份:xs === xs.map(x => x)
  4. ...和组合:xs.map(f).map(g) === xs.map(f . g) 其中. 是函数组合。

就是这样。不多也不少。查看您的Box,它是一个具有map 函数(检查1 和2)的数据结构,并且该映射函数看起来应该尊重身份和组合(检查3 和4)。所以它是一个函子。但它没有任何事情,这就是为什么它是恒等函子。 fold 函数并不是绝对必要的,它只是提供了一种“打开”盒子的方法。

对于一个有用的函子,让我们看一下 JavaScript 数组。数组实际上一些事情:即它们包含多个值而不仅仅是一个值。如果一个数组只能有一个元素,那就是你的Box。出于我们的目的,我们将假设它们只能为简单的事物保存相同类型的值。因此,数组是一种数据结构,具有映射功能,尊重身份和组合。

let plus = x => y => x + y;
let mult = x => y => x * y;
let plus2 = plus(2);
let times3 = mult(3);
let id = x => x;
let compose = (...fs) => arg => fs.reverse().reduce((x, f) => { return f(x) }, arg);  

// Here we need to stringify the arrays as JS will compare on 
// ref rather than value. I'm omitting it after the first for
// brevity, but know that it's necessary.
[1,2,3].map(plus2).toString() === [3,4,5].toString(); // true
[1,2,3].map(id) === [1,2,3]; // true
[1,2,3].map(plus2).map(times3) === [1,2,3].map(compose(times3, plus2)); // true

因此,当我们 map 一个函子(数组)上的函数时,我们会返回同一个函子(一个新数组)的另一个实例,该函数应用于函子(数组)所持有的任何内容。

现在让我们看看另一个无处不在的 JavaScript 数据结构,对象。对象没有内置的 map 函数。我们可以让它们成为函子吗?再次假设对象是同质的(只有一种类型值的键,在本例中为 Number):

let mapOverObj = obj => f => {
  return Object.entries(obj).reduce((newObj, [key, value]) => {
    newObj[key] = f(value);
    return newObj;
  }, {});
};

let foo = { 'bar': 2 };
let fooPrime = mapOverObj(foo)(plus2); // { 'bar': 4 }

您可以继续准确地测试该函数(尽可能在 JavaScript 中)是否支持标识和组合以满足函子定律。

【讨论】:

  • 重新阅读您的答案、链接的问题和其他几个来源 (en.m.wikipedia.org/wiki/Type_theory#Relation_to_category_theory . wiki.haskell.org/Category_theory ) 我意识到我困惑的主要原因是我武断地用类型来识别类别,而类别是特定类型理论(例如 Haskell's、Purescript's 等),这也是为什么函数式编程中的函子几乎总是内函子的原因。
  • @toraritte 是的,你明白了。有时很难将直觉与概念的本质区别开来。
  • 尝试建议对以下内容进行编辑,但遭到拒绝,因此有此评论:(1)compose 函数需要slice(),因为在当前形式中它会替换输入数组, 在每次迭代中翻转顺序 (let compose = (...fs) => arg => fs.**slice()**.reverse().reduce((x, f) => { return f(x) }, arg);) (2) mapOverObj 缺少新值的函数应用程序:newObj[key] = f(value)。再次感谢@JaredSmith,因为我也刚刚学会了一些新的 ES6 技巧!
  • @toraritte 而reverse 确实在原地工作,它仅在调用组合函数时发生。它不会发生在每次迭代中。因为数组是由 splat 运算符创建的,而不是作为参数传递,所以破坏性地重新排列它并不是什么大问题。很好的地图功能应用程序。
  • @toraritte 奇怪。尽管如此,我写的代码主要是为了说明:为了实际使用,我推荐一个经过实战测试的 fp 库,如 ramda 或 lodash-fp。几年前,正是 Brian Lonsdorf(又名弗里斯比教授)的一次演讲向我介绍了 ramda。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-04-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-10-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多