Kle*_*ano 10 javascript monads functional-programming
我对函数式编程的研究还比较陌生,一切进展顺利,直到我不得不处理错误和承诺。尝试以 \xe2\x80\x9cright\xe2\x80\x9d 的方式做到这一点,我得到了很多 Monad 的参考资料,认为这是更好的解决方案,但在研究它时,我最终得到了我诚实地称之为“引用地狱”,其中存在大量数学或编程中的引用和子引用,对于同一事物,该事物对于相同的概念有不同的名称,这确实令人困惑。因此,在坚持这个主题之后,我现在尝试总结和澄清它,这就是我到目前为止得到的:
\n为了便于理解,我将其过度简化。
\n\n\nMonoids:是将两个事物连接/求和并返回同一组的东西,因此在 JS 中,任何数学加法或字符串的连接都是 Monoids 的定义以及函数的组合。
\n
\n\n映射:映射只是将函数应用于组中每个元素的方法,而不更改组本身的类别或其长度。
\n
\n\n函子:函子只是具有返回函子本身的 Map 方法的对象。
\n
\n\nMonad: Monad 是使用 FlatMap 的 Functor。
\n
\n\nFlatMaps: FlatMaps 是具有处理承诺/获取或总结接收到的值的能力的映射。
\n
\n\n要么,也许,绑定,然后:都是 FlatMap,但根据您使用它的上下文具有不同的名称。
\n(我认为它们都是 FlatMaps 的定义,但它们的使用方式有所不同,因为有像Monets.js这样的库,它同时具有 Maybe 和 Either 函数,但我不\xe2\x80\x99t 得到用例差异)。
\n
所以我的问题是:这些概念正确吗?
\n如果有人能让我放心,到目前为止我做对的事情,纠正我做错的事情,甚至扩展我错过的事情,我将非常感激。
\n感谢任何花时间的人。
\n//================================================== =============//
\n编辑: \n我应该在这篇文章中更多地强调,但这些肯定和简化的定义仅来自“JavaScript 的实用角度”(我知道不可能对一个巨大而复杂的模型进行如此小的简化)像这样的主题,特别是如果您添加另一个领域(例如数学)。
\n//================================================== =============//
\nlef*_*out 11
\n\n幺半群:是将两个事物连接/相加并返回同一组的事物的任何东西......
\n
First thing that I don\'t like here is the word \xe2\x80\x9cgroup\xe2\x80\x9d. I know, you\'re just trying to use simple language and all, but the problem is that group has a very specific mathematical meaning, and we can\'t just ignore this because groups and monoids are very closely related. (A group is basically a monoid with inverse elements.) So definitely don\'t use this word in any definition of monoids, however informal. You could say \xe2\x80\x9cunderlying set\xe2\x80\x9d there, but I\'d just say type. That may not match the semantics in all programming languages, but certainly in Haskell.
\nSo,
\n\n\nMonoids: are anything that concatenate/sum two things returning a thing of the same type so in JS any math addition or just concatenation from a string are Monoids for definition as well the composition of functions.
\n
Ok. Specifically, concatenation of endofunctions is a monoid. In JavaScript, all functions are in a sense endofunctions, so you can get away with this.
\nBut that\'s actually describing only a semigroup, not a monoid. (See, there\'s out groups... confusingly, monoids are in between semigroups and groups.) A monoid is a semigroup that has also a unit element, which can be concatenated to any other element without making a difference.
\nEven with unit elements, your characterization is missing the crucial feature of a semigroup/monoid: the concatenation operation must be associative. Associativity is a strangely un-intuitive property, perhaps because in most examples it seems stupidly obvious. But it\'s actually crucial for much of the maths that is built on those definitions.
\nTo make the importance of associativity clear, it helps to look at some things that are not semigroups because they aren\'t associative. The type of integers with subtraction is such an example. The result is that you need to watch out where to put your parentheses in maths expressions, to avoid incurring sign errors. Whereas strings can just be concatenated in either order \xe2\x80\x93 ("Hello"+", ")+"World" is the same as "Hello"+(", "+"World").
\n\nMaps: Maps are just methods that apply a function to each element of a group, without changing the category of the group itself or its length.
\n
Here we have the next badly chosen word: categories are again a specific maths thing that\'s very closely related to all we\'re talking about here, so please don\'t use the term with any other meaning.
\nIMO your definitions of \xe2\x80\x9cmaps\xe2\x80\x9d and functors are unnecessary. Just define functors, using the already known concept of functions.
\nBut before we can do that \xe2\x80\x93
\n\n\nFunctors: Functors are just objects...
\n
here we go again, with the conflict between mathematical terminology and natural language. Mathematically, objects are the things that live in a category. Functors are not objects per se (although you can construct a specific category in which they are, by construction, objects). And also, itself already conflicting: in programming, \xe2\x80\x9cobject\xe2\x80\x9d usually means \xe2\x80\x9cvalue with associated methods\xe2\x80\x9d, most often realized via a class.
\nYour usage of the terms seems to match neither of these established meanings, so I suggest you avoid it.
\nMathematically, a functor is a mapping between two categories. That\'s hardly intuitive, but if you consider the category as a collection of types then a functor simply maps types to types. For example, the list functor maps some type (say, the type of integers) to the type of lists containing values of that type (the type of lists of integers).
\nHere of course we\'re running a bit into trouble when considering it all with respect to JS. In dynamic languages, you can easily have lists containing elements of multiple different types. But it\'s actually ok if we just treat the language as having only one big type that all values are members of. The list functor in Python maps the universal type to itself.
\nBlablatheory, what\'s the point of this all? The actual feature of a functor is not the type-mapping, but instead that it lifts a function on the contained values (i.e. on the values of the type you started with, in my example integers) to a function on the container-values (on lists of integers). More generally, the functor F lifts a function a -> b to a function F(a) -> F(b), for any types a and b. What you called \xe2\x80\x9ccategory of the group itself\xe2\x80\x9d means that you really are mapping lists to lists. Even in a dynamically typed language, the list functor\'s map method won\'t take a list and produce a dictionary as the result.
I suggest a different understandable-definition:
\n\n\nFunctors: Functors wrap types as container-types, which have a mapping method that applies functions on contained values to functions on the whole container.
\n
What you said about length is true of the list functor in particular, but it doesn\'t really make sense for functors in general. In Haskell we often talk about the fact that functor mapping preserves the \xe2\x80\x9cshape\xe2\x80\x9d of the container, but that too isn\'t actually part of the mathematical definition.
\nWhat is part of the definition is that a functor should be compatible with composition of the functions. This boils down to being able to map as often as you like. You can always map the identity function without changing the structure, and if you map two functions separately it has the same effect as mapping their composition in one go. It\'s kind of intuitive that this amounts to the mapping being \xe2\x80\x9cshape-preserving\xe2\x80\x9d.
\n\n\nMonad: Monad 是使用 FlatMap 的 Functor。
\n
很公平,但当然这只是将一切转移到:什么是 FlatMap?
\n>>=从数学上来说,一开始不考虑 FlatMap / 操作实际上更容易,而只考虑扁平化操作以及单例注入器。举例来说:列表 monad 是列表函子,配备有
再次强调,这些操作必须遵守法律,这一点很重要。这些也类似于幺半群定律,但不幸的是,它们更不直观,因为它们同时难以思考,而且又几乎微不足道,以至于看起来有点无用。但具体来说,列表的结合律可以很好地表述:
\n展平双重嵌套列表中的内部列表,然后展平外部列表,与先展平外部列表,然后展平内部列表具有相同的效果。
\n[[[1,2,3],[4,5]],[[6],[7,8,9]]] \xe2\x9f\xbc [[1,2,3,4,5],[6,7,8,9]] \xe2\x9f\xbc [1,2,3,4,5,6,7,8,9]\n[[[1,2,3],[4,5]],[[6],[7,8,9]]]\xe2\x9f\xbc[[1,2,3],[4,5],[6],[7,8,9]]\xe2\x9f\xbc[1,2,3,4,5,6,7,8,9]\nRun Code Online (Sandbox Code Playgroud)\n
坦率地说,我认为你所有的定义都非常糟糕,除了“幺半群”定义。
这是思考这些概念的另一种方式。从某种意义上说,它不是“实用的”,因为它会告诉你为什么列表单子上的平面图应该展平嵌套列表,但我认为它是“实用的”,因为它应该告诉你为什么我们关心在单子中使用单子进行编程。第一名,以及从函数式程序中的实际角度来看,monad通常应该完成什么,无论它们是用 JavaScript 还是 Haskell 或其他语言编写的。
在函数式编程中,我们编写将某些类型作为输入并产生某些类型作为输出的函数,并通过组合输入和输出类型匹配的函数来构建程序。这是一种优雅的方法,可以产生漂亮的程序,也是函数式程序员喜欢函数式编程的主要原因之一。
函子提供了一种系统地转换类型的方法,为原始类型添加有用的功能。例如,我们可以使用函子向“普通类型”添加功能,允许其缺失或不存在或“null”( Maybe) 或表示成功计算的结果或错误条件 ( Either) 或允许其表示多个可能的值而不是只有一个(列表),或者允许在将来的某个时间计算它(承诺),或者需要评估上下文(Reader),或者允许这些东西的组合。
映射允许我们以某种自然的方式在这些已由函子转换的新类型上重用为普通类型定义的函数。如果我们已经有一个将整数加倍的函数,我们可以在整数的各种函子转换上重复使用该函数,例如将可能丢失的整数加倍(映射 a Maybe)或将尚未计算的整数加倍(映射到一个承诺)或加倍列表中的每个元素(映射到一个列表)。
单子涉及将函子概念应用于函数的输出类型以产生具有附加功能的“操作”。通过无 monad 函数式编程,我们编写的函数接受“正常类型”的输入并产生“正常类型”的输出,但是 monad 允许我们接受“正常类型”的输入并产生转换类型的输出,就像上面的那样。这样的一元操作可以表示一个接受输入并Maybe产生输出的函数,或者接受输入并承诺稍后产生输出的函数,或者接受输入并产生输出列表的函数。
平面映射将普通类型上的函数组合(即,我们构建无单子功能程序的方式)概括为单子操作的组合,适当地“链接”或组合由单子操作的转换输出类型提供的额外功能。因此,也许 monad 上的平面映射将组合函数,只要它们不断产生输出,并在其中一个函数缺少输出时放弃;承诺单子上的平面映射会将一系列操作(每个操作接受输入并承诺输出)转换为单个组合操作,该组合操作接受输入并承诺最终输出;列表 monad 上的平面映射会将一系列操作(每个操作接受一个输入并产生多个输出)转换为一个接受输入并产生多个输出的组合操作。
请注意,这些概念之所以有用,是因为它们的便利性和它们所采用的系统方法,而不是因为它们为函数式程序添加了我们原本不会拥有的神奇功能。当然,我们不需要函子来创建列表数据类型,也不需要monad来编写接受单个输入并生成输出列表的函数。它最终成为“接受输入并承诺产生错误消息或输出列表的操作”方面的有用思考,将其中 50 个操作组合在一起,并最终得到一个接受输入的组合操作并承诺错误消息或输出列表(不需要手动解析嵌套承诺的深层嵌套列表——因此具有“扁平化”的价值)。
(在实际的编程术语中,幺半群与其余的没有太多关系,除了对内函子类别进行搞笑。幺半群只是组合或“减少”一堆值的系统方法特定类型的单个值转换为该类型的单个值,其方式不依赖于首先或最后组合哪些值。)
简而言之,函子及其映射允许我们向类型添加功能,而monad及其平面映射提供了一种使用函子的机制,同时保留了简单函数组合的优雅外观,这使得函数式编程首先变得如此有趣。
一个例子可能会有所帮助。考虑执行文件树的深度优先遍历的问题。从某种意义上说,这是函数的简单递归组合。要生成filetree()以 at 为根的pathname,我们需要调用 上的函数pathname来获取其children(),然后我们需要递归地调用filetree()这些children()。在伪 JavaScript 中:
// to generate a filetree rooted at a pathname...
function filetree(pathname) {
// we need to get the children and generate filetrees rooted at their pathnames
filetree(children(pathname))
}
Run Code Online (Sandbox Code Playgroud)
但显然,这不会像真正的代码一样工作。一方面,类型不匹配。该filetree函数应该在单个路径名上调用,但children(pathname)会返回多个路径名。还有一些其他问题——不清楚递归应该如何停止,而且还有一个问题是,当pathname我们直接跳到它的子级及其文件树时,原始递归似乎在洗牌中迷失了。另外,如果我们尝试将其集成到具有基于 Promise 的架构的现有 Node 应用程序中,则不清楚此版本如何filetree支持基于 Promise 的文件系统 API。
但是,如果有一种方法可以向所涉及的类型添加功能,同时保持这种简单组合的优雅,该怎么办?例如,如果我们有一个函子,允许我们承诺返回多个值(例如,多个子路径名),同时记录字符串(例如,父路径名)作为处理的副作用,该怎么办?
正如我上面所说,这样的函子是类型的转换。这意味着它将把“普通”类型(例如“整数”)转换为“整数列表和字符串日志的承诺”。假设我们将其实现为包含单个 Promise 的对象:
function M(promise) {
this.promise = promise
}
Run Code Online (Sandbox Code Playgroud)
当解析时将产生一个以下形式的对象:
{
"data": [1,2,3,4] // a list of integers
"log": ["strings","that","have","been","logged"]
}
Run Code Online (Sandbox Code Playgroud)
作为函子,M将具有以下map功能:
M.prototype = {
map: function(f) {
return this.promise.then((obj) => ({
data: obj.data.map(f),
log: obj.log
}))
}
}
Run Code Online (Sandbox Code Playgroud)
这会将普通函数应用于承诺的数据(不影响日志)。
更重要的是,作为一个 monad,M将具有以下flatMap功能:
M.prototype = {
...
flatMap: function(f) {
// when the promised data is ready
return new M(this.promise.then(function(obj) {
// map the function f across the data, generating promises
var promises = obj.data.map((d) => f(d).promise)
// wait on all promises
return Promise.all(promises).then((results) => ({
// flatten all the outputs
data: results.flatMap((result) => result.data),
// add to the existing log
log: obj.log.concat(results.flatMap((result) => result.log))
}))
}))
}
}
Run Code Online (Sandbox Code Playgroud)
我不会详细解释,但想法是,如果我在 monad 中有两个 monadic 操作M,它们接受“普通”输入并生成M转换后的输出,代表提供值列表和日志的承诺,我可以flatMap在第一个操作的输出上使用该方法将其与第二个操作组合,从而产生一个复合操作,该复合操作采用单个“纯”输入并生成转换M后的输出。
通过children在 monad 中定义为一元操作M,承诺采用父路径名,将其写入日志,并生成该路径名的子级列表作为其输出数据:
function children(parent) {
return new M(fsPromises.lstat(parent)
.then((stat) => stat.isDirectory() ? fsPromises.readdir(parent) : [])
.then((names) => ({
data: names.map((x) => path.join(parent, x)),
log: [parent]
})))
}
Run Code Online (Sandbox Code Playgroud)
filetree我可以像上面的原始函数一样优雅地编写递归函数,作为和 递归调用函数flatMap的辅助组合:childrenfiletree
function filetree(pathname) {
return children(pathname).flatMap(filetree)
}
Run Code Online (Sandbox Code Playgroud)
为了使用filetree,我需要“运行”它来提取日志,并将其打印到控制台。
// recursively list files starting at current directory
filetree(".").promise.then((x) => console.log(x.log))
Run Code Online (Sandbox Code Playgroud)
完整代码如下。不可否认,其中有相当多的部分,其中一些非常复杂,因此函数的优雅filetree似乎付出了相当大的代价,因为我们显然只是将所有复杂性(以及其中一些)转移到了M单子。然而,Mmonad 是一个通用工具,并不是专门用于执行文件树的深度优先遍历。M此外,在理想的情况下,复杂的 JavaScript monad 库将允许您使用几行代码从 monadic 片段(promise、list 和 log)构建monad。
var path = require('path')
var fsPromises = require('fs').promises
function M(promise) {
this.promise = promise
}
M.prototype = {
map: function(f) {
return this.promise.then((obj) => ({
data: obj.data.map(f),
log: obj.log
}))
},
flatMap: function(f) {
// when the promised data is ready
return new M(this.promise.then(function(obj) {
// map the function f across the data, generating promises
var promises = obj.data.map((d) => f(d).promise)
// wait on all promises
return Promise.all(promises).then((results) => ({
// flatten all the outputs
data: results.flatMap((result) => result.data),
// add to the existing log
log: obj.log.concat(results.flatMap((result) => result.log))
}))
}))
}
}
// not used in this example, but this embeds a single value of a "normal" type into the M monad
M.of = (x) => new M(Promise.resolve({ data: [x], log: [] }))
function filetree(pathname) {
return children(pathname).flatMap(filetree)
}
function children(parent) {
return new M(fsPromises.lstat(parent)
.then((stat) => stat.isDirectory() ? fsPromises.readdir(parent) : [])
.then((names) => ({
data: names.map((x) => path.join(parent, x)),
log: [parent]
})))
}
// recursively list files starting at current directory
filetree(".").promise.then((x) => console.log(x.log))
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
1010 次 |
| 最近记录: |