【问题标题】:Function Composition With Monads...not working使用 Monad 的函数组合...不起作用
【发布时间】:2017-06-13 08:00:26
【问题描述】:

我有一些丑陋的数据,这需要很多丑陋的空检查。我的目标是编写一套函数,以无点、声明式的风格访问/修改它,使用 Maybe monad 将空检查保持在最低限度。理想情况下,我可以将 Ramda 与 monad 一起使用,但效果不是很好。

这行得通:

const Maybe = require('maybe');
const R = require('ramda');
const curry = fn => (...args) => fn.bind(null, ...args);
const map = curry((fn, monad) => (monad.isNothing()) ? monad : Maybe(fn(monad.value())));
const pipe = (...fns) => acc => fns.reduce((m, f) => map(f)(m), acc);
const getOrElse = curry((opt, monad) => monad.isNothing() ? opt : monad.value());
const Either = (val, d) => val ? val : d;

const fullName = (person, alternative, index) => R.pipe(
  map(R.prop('names')),
  map(R.nth(Either(index, 0))),
  map(R.prop('value')),
  map(R.split('/')),
  map(R.join('')),
  getOrElse(Either(alternative, ''))
)(Maybe(person));

但是,必须输入 'map()' 十亿次似乎不是很干,也不是很好看。我宁愿有一个特殊的管道/组合函数,它将每个函数包装在一个 map() 中。

注意我是如何使用 R.pipe() 而不是我的自定义 pipe() 的?我的自定义实现总是抛出一个错误,'isNothing() is not a function,' 在执行传递给它的最后一个函数时。

我不确定这里出了什么问题,或者是否有更好的方法可以做到这一点,但我们非常感谢任何建议!

【问题讨论】:

  • 您能否举例说明person 中的数据是什么样的?
  • 你对作曲做了什么测试?你应该能够做到R.compose(R.props(..), R.nth(..), R.split(...) 然后R.map(composedFuncs, Maybe(person))
  • 提示:在现代浏览器中.bind(thisArg) 速度非常快。同时,.bind(thisArg, ...funcArgs) 非常慢。在这种情况下,闭包是更好(更快)的选择:const curry = fn => (...args) => (...brgs) => fn.apply(null, args.concat(brgs));
  • 这就是我不喜欢 FP 的地方。当我不得不扭曲和弯曲时,只是为了适应范式。我喜欢声明式风格,因为正确使用它可以作为注释代码的两倍,但如果我必须将它用于每个微小的步骤/操作/操作符,则不会。我会将您的代码编写为const fullName = (person, alternative = "", index=0) => string( fetch(person, ["names", index, "value"]) ) .split("/").join("") || alternative;,其中我的两个实用程序stringfetch 包含的代码不超过您所有的实用程序。但这并不真正符合主题,或者是吗?
  • @atomrc 数据看起来像{名称:[{value:'Bob / Johnson',来源:{...}},{...etc.}]}。没有一个 Ramda 函数可以自己访问 Maybe 值,所以我试图以某种方式分离 map 并使用自定义 compose/pipe 函数。

标签: javascript functional-programming ramda.js


【解决方案1】:

首要任务

  1. Maybe 实现 (link) 非常垃圾 - 您可能需要考虑选择一个不需要您实现 Functor 接口的实现(就像您对 map 所做的那样) - 我可能会建议Data.Maybe 来自民间故事。或者,既然你显然不害怕自己实现,那就自己做一个 Maybe ^_^

  1. 您的map 实现不适用于任何实现仿函数接口的仿函数。即,您的适用于Maybe,但map 应该足够通用以适用于任何可映射,如果有这样的词。

    不用担心,Ramda 在框中包含map - 只需将其与实现.map 方法的Maybe 一起使用(例如上面引用的Data.Maybe)


  1. 您的curry 实现并不完全正确地使用函数。它只适用于 arity 为 2 的函数——curry 应该适用于任何函数长度。

    // given, f
    const f = (a,b,c) => a + b + c
    
    // what yours does
    curry (f) (1) (2) (3) // => Error: curry(...)(...)(...) is not a function
    
    // because 
    curry (f) (1) (2) // => NaN
    
    // what it should do
    curry (f) (1) (2) (3) // => 6
    

    如果你已经在使用 Ramda,你真的没有理由自己实现 curry,因为它已经包含了 curry


  1. 您的pipe 实现混合了函数组合和映射函子的关注点(通过使用map)。我建议专门为函数组合保留pipe

    同样,不确定您为什么要使用 Ramda,然后重新发明了很多。 Ramda 已经包含pipe

    我注意到的另一件事

    // you're doing
    R.pipe (a,b,c) (Maybe(x))
    
    // but that's the same as
    R.pipe (Maybe,a,b,c) (x)
    

  1. 您创建的Either 可能不是您想的 Either 函子/monad。请参阅Data.Either(来自folktale)以获得更完整的实现

  1. 没有观察到一个单子——您的问题是关于单子的函数组合,但您只是在代码中使用函子接口。这里的一些混淆可能来自Maybe 实现Functor Monad 的事实,因此它可以同时作为两者(和它实现的任何其他接口一样)!在这种情况下,Either 也是如此。

    您可能希望查看 Kleisli category 以了解单子函数组合,尽管它可能与您无关这个特定问题。


功能接口受法律管辖

您的问题源于缺乏对函子定律的了解/理解——这些意味着如果您的数据类型遵守这些定律,只有 那么 可以可以说你的类型一个仿函数。在所有其他情况下,您可能正在处理 仿函数,但实际上不是仿函数。

函子定律

其中map :: Functor f => (a -> b) -> f a -> f bid 是标识函数a -> af :: b -> cg :: a -> b

// identity
map(id) == id

// composition 
compose(map(f), map(g)) == map(compose(f, g))

这告诉我们的是,我们可以对map 单独调用每个函数,或者我们可以先组合所有函数,然后再组合map一次。 – 请注意,在组合法则的左侧,我们如何调用 .map 两次以应用两个函数,但在右侧,.map 仅被调用一次。每个表达式的结果都是相同的。

单子定律

虽然我们在这方面,但我们也可以涵盖 monad 法则 - 再次,如果您的数据类型遵守这些法则,只有 then 可以将其称为 monad .

mreturn :: Monad m => a -> m ambind :: Monad m => m a -> (a -> m b) -> m b 的位置

// left identity
mbind(mreturn(x), f) == f(x)

// right identity
mbind(m, mreturn) == m

// associativity
mbind(mbind(m, f), g) == mbind(m, x => mbind(f(x), g))

使用 Kleisli 组合函数,composek 可能更容易看到规律——现在很明显,Monads 真正遵守结合律

使用 Kleisli 组合定义的单子定律

在哪里composek :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)

// kleisli left identity
composek(mreturn, f) == f

// kleisli right identity
composek(f, mreturn) == f

// kleisli associativity
composek(composek(f, g), h) == composek(f, composek(g, h))

寻找解决方案

那么这一切对你意味着什么?简而言之,你做的工作比你必须做的要多——尤其是实现了你选择的库 Ramda 已经附带的很多东西。现在,这并没有错(事实上,如果您审核我的许多 网站上的其他答案),但如果你弄错了一些实现,它可能会造成混乱。

由于您似乎主要关注map 方面,我将帮助您了解一个简单的转换。这利用了上面说明的 Functor 组合定律:

注意,这使用R.pipe,它从左到右而不是像R.compose那样从右到左组成。虽然I prefer right-to-left composition,但使用pipecompose 的选择取决于您——这只是符号的不同;无论哪种方式,法律都得到履行。

// this
R.pipe(map(f), map(g), map(h), map(i)) (Maybe(x))

// is the same as
Maybe(x).map(R.pipe(f,g,h,i))

我想提供更多帮助,但我不能 100% 确定您的函数实际上想要做什么。

  1. Maybe(person)开头
  2. 读取person.names属性
  3. 获取person.names 的第一个索引——它是一个数组还是什么?还是名字的第一个字母?
  4. 读取.value 属性??我们在这里期待一个单子吗? (查看.chain.map 在我从folktale 链接的MaybeEither 实现中的比较)
  5. 拆分/上的值
  6. 将值与'' 连接起来
  7. 如果我们有一个值,则返回它,否则返回一些替代项

这是我对正在发生的事情的最佳猜测,但我无法在此处描绘您的数据,也无法理解您尝试进行的计算。如果您提供更具体的数据示例和预期输出,我或许可以帮助您制定更具体的答案。


备注

几年前我也在你的船上;刚刚进入函数式编程,我的意思是。我想知道如何将所有小部分组合在一起并实际生成人类可读的程序。

函数式编程提供的大部分好处只有在将函数式技术应用于整个系统时才能体现出来。起初,您会觉得您必须引入大量依赖项才能以“功能方式”重写一个函数。但是,一旦您在程序中的 more 位置中使用了这些依赖项,您就可以开始左右削减复杂性。看到它真的很酷,但需要一段时间才能让你的程序(和你的头脑)在那里。

事后看来,这可能不是一个很好的答案,但我希望这对您有所帮助。这对我来说是一个非常有趣的话题,我很乐意协助回答您的任何其他问题^_^

【讨论】:

  • 感谢超级深入的回答!有趣的是,您如何设法缩小我的困惑的根源,尽管我对术语/实践的了解不够,无法正确地表述它。
  • 令我印象深刻的是,您看到了一种模式并认识到它可以改进,即使您无法立即看到如何改进。学习有一种优雅的策略/技术来处理特定问题总是很有趣^_^
  • 嗯,当人们也知道他们在说什么时,这令人印象深刻:)。您对进一步阅读/其他学习材料有什么建议吗?另外,对 Clojure 学习 FP 有什么看法吗?再次感谢您的出色回答。
  • JavaScript 具有一流的函数,因此非常适合学习许多函数概念。如果 JavaScript 对你来说已经很舒服了,我会去看看Professor Frisby's Mostly Adequate Guide to Functional Programming——或者,如果你想用一种对学习者非常友好的语言来探索一些基础知识,我会推荐Structure and Interpretation of Computer Programs (SICP)
  • (事后看来) - 不,这是一个很好的答案,解决了各种问题,有些是我之前看到这个但在电话上时想到的,有些我什至没有注意到。两个更正:“// but that's the same as R.pipe (Maybe,a,b,c) (x)”(Maybepipe 的结尾是错误的。)和“和fg 是函数(类型分别为(b -> c)(a -> b)。) " ((a -> b) 不与自己组合。)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-30
  • 1970-01-01
  • 1970-01-01
  • 2021-08-31
相关资源
最近更新 更多