问题描述
在范畴论中,Functor的概念如下:
https://ncatlab.org/nlab/show/functor
在Haskell中,Functor
类型可以表示为:
fmap :: (a -> b) -> f a -> f b
https://hackage.haskell.org/package/base-4.14.0.0/docs/Data-Functor.html
我可以看到两者之间确实很对应。
但是,一旦我们实际尝试将Functor概念简化为代码,似乎就不可能像上图所示那样简单地定义F
或fmap
。
实际上,有一篇关于Functor / Monad的著名文章。
Functors,Applicatives,And Monads In Pictures
足够简单。让我们通过说任何值都可以在上下文中来扩展这一点。现在,您可以将上下文视为可以在其中添加值的框:
或
这是我们编写fmap(+3)(仅2月)时幕后发生的事情:
我对Functor
的总体看法是类别理论中的 Functor概念,而与“包装箱”进行包装的概念与之相匹配。
问题点1。
fmap :: (a -> b) -> f a -> f b
https://hackage.haskell.org/package/base-4.14.0.0/docs/Data-Functor.html
在Haskell中“ Box”与“包装”之间的实际实现在哪里?
问题点2。
为什么分类理论中的 Functor概念和与“包装箱”进行包装的概念不匹配?
编辑:
即使对于IO
函子,在编写过程中,f
也已展开:
// f is unwrapped in composition process
const compose = g => f => x => g(f(x));
const fmap = compose;
const print = a => () => console.log(a);
// safely no side-effect
const todo = fmap(print("bar"))(print("foo"));
//side effect
todo(undefined); // foo bar
// with pipleline operator (ES.next)
//
// const todo = print("foo")
// |> fmap(print("bar"))
// |> fmap(print("baz"));
// todo(undefined); // foo bar baz
解决方法
类别理论的思想是如此抽象,以至于任何试图提供直观介绍的人都有将概念简化到可能使人感到困惑的风险。作为an article series in this space的作者,我可以证明,在有人误解文本之前,并不需要太多不精确的语言。
我不知道那篇文章,但我相信它可能表现出相同的特征。包装/解开隐喻适合大部分函子(例如^(Q:.*?\n) # Matches "Q:" at the beginning of the line,followed by
# some optional text ending with a line-feed.
(?!Q:) # Not immediately followed by another "Q:".
( # Start of the second capturing group.
[\s\S]+? # Matches one or more characters (including line breaks) - non-greedy.
(?=^Q:|\Z) # Stop matching if either followed by "Q:" or is at the end of the string.
) # End of the second capturing group.
,Maybe
,[]
等),但并非全部。
众所周知,您是not supposed to unwrap IO
;那是设计使然。在这一点上,包装/展开的隐喻崩溃了。面对Either l
,它不再有效。
确实,这些概念不匹配。我想说,wrap / unwrap隐喻可能是有用的介绍,但一如既往,可以扩展隐喻的数量是有限制的。
如何实现IO
实例? Haskell的大多数介绍将向您展示如何为Functor
,fmap
和其他几种类型编写Maybe
。如果有机会的话,自己实施也是一种很好的练习。
GHC及其生态系统是开源的,因此,如果您想知道如何实现特定实例,可以随时查看源代码。
同样,[]
是该规则的一个大例外。据我了解,其IO
,Functor
,Applicative
等实例不是在(Safe)Haskell中实现的,而是在一小部分不安全代码(C或C ++,我相信)构成了编译器和/或运行时环境的核心。 Monad
没有进行任何(明确,可见,安全的)解包操作。我认为将IO
的{{1}}实例看成是保留结构的映射会更有用。
有关类别理论与Haskell之间对应关系的更多详细信息,我建议使用Bartosz Milewski's article series on the topic。
,查看图片中的箭头。从功能器级别回到非功能器级别是没有办法。您将需要一个从F(x)
到x
的函数,但是-如您所见-没有定义。
有一些特定的函子(例如Maybe
)具有“展开”功能,但此功能始终是附加功能,它是函子的顶部。例如,您可能会说:Maybe
是一个函子,它还具有一个有趣的属性:有一个部分函数将Maybe X
映射到X,并反转pure
。
更新(在出现其他问题之后)盒子和函子的概念根本不匹配。而且,据我所知,没有找到对函子(或monad或应用程序)的很好的隐喻-并不是因为缺乏尝试。这甚至不足为奇:大多数抽象都缺乏良好的隐喻,正是因为抽象和隐喻是相反的(从某种意义上来说)。
抽象将概念剥离到其核心,仅保留最重要的要素。另一方面,隐喻扩展了概念,扩大了语义空间,暗示了更多的含义。当我说:“您的眼睛有巧克力的颜色”时,我是在抽象“颜色”的概念。但是,我还隐喻地将眼睛和巧克力联系起来:我建议它们比颜色具有更多的共同点:柔滑的质地,甜美,愉悦-所有这些概念都存在,尽管都没有命名。如果我说“您的眼睛有排泄物的颜色”,则使用的抽象将完全相同-但隐喻的含义是:非常不同。即使逻辑学家从技术上理解这句话并不令人反感,我也不会对逻辑学家说出来。
在进行自动驾驶时,大多数人会以隐喻而不是抽象的方式思考。在用前者解释后者时必须小心,因为含义会溢出。当您听到“盒子”的声音时,脑海中的自动驾驶仪会告诉您可以放入和取出物品。函子不是那样的。因此,这个比喻具有误导性。
Functor体现了...盒子或包装纸的抽象,但使我们无需拆开即可处理它们的内容。缺少解包正是使函子变得有趣的原因:否则fmap
仅仅是解包,应用函数并将结果包装起来的语法糖。研究函子使我们了解不解包价值的可能性—更好的方法和更具启发性的功能,使我们了解不解包的不可能。导致应用程序,箭头和monad的步骤向我们展示了如何通过允许执行其他操作来克服某些限制,但stukk 绝不允许展开,因为如果我们允许展开,则这些步骤将毫无意义(即变得微不足道)。