我有一些丑陋的数据,需要大量丑陋的空检查。我的目标是编写一套函数来以无点的声明式样式访问/修改它,使用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()而不是我的自定义管道()?我的自定义实现总是抛出一个错误,'isNothing()不是函数',在执行传递给它的最后一个函数时。
我不确定这里出了什么问题,或者有更好的方法可以做到这一点,但任何建议都表示赞赏!
首先要做的事情
Maybe
实现(link)几乎是垃圾 - 你可能想考虑选择一个不需要你实现Functor接口的实现(就像你用map
做的那样) - 我可能会建议来自民间故事的Data.Maybe。或者因为你显然不害怕自己实施的东西,所以制作你自己的Maybe ^ _ ^map
实现不适用于任何实现仿函数接口的仿函数。即,你的只适用于Maybe
,但map
应该足够通用,可以与任何可映射的一起工作,如果有这样的话。
不用担心,Ramda在框中包含map
- 只需将其与Maybe
一起使用即可实现.map
方法(例如上面引用的Data.Maybe)curry
实现并没有很好地理解函数。它仅适用于arity为2的函数 - 咖喱应适用于任何函数长度。
// 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
,因为它已经包含了currypipe
实现混合了函数组合和映射函数的问题(通过使用map
)。我建议专门为功能组成保留pipe
。
再次,不知道为什么你使用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)
Either
可能不是你想到的任何一个仿函数/ monad。有关更完整的实现,请参阅Data.Either
(来自民间故事)Maybe
实现Functor
和Monad
的事实,因此它可以表现为两者(和它实现的任何其他接口一样)!在这种情况下,Either
也是如此。
您可能希望查看Kleisli category
的monadic函数组合,尽管它可能与此特定问题无关。功能接口受法律约束
你的问题源于缺乏对仿函数法则的曝光/理解 - 如果你的数据类型符合这些法则,这意味着什么,只有这样才能说你的类型是一个仿函数。在所有其他情况下,您可能正在处理像仿函数这样的东西,但实际上并不是仿函数。
算子法则
其中
map :: Functor f => (a -> b) -> f a -> f b
,id
是身份函数a -> a
,f :: b -> c
和g :: a -> b
// identity map(id) == id // composition compose(map(f), map(g)) == map(compose(f, g))
这对我们说的是,我们可以单独为每个函数组合多次调用map
,或者我们可以首先组合所有函数,然后map
。 - 注意组成法的左侧我们如何调用.map
两次来应用两个函数,但在右侧.map
只被调用一次。每个表达式的结果是相同的。
monad法律
在我们处理它的同时,我们也可以涵盖monad法则 - 再次,如果你的数据类型遵守这些法则,那么它只能被称为monad。
mreturn :: Monad m => a -> m a
,mbind :: 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组合定义的monad法则
哪里有
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,使用pipe
和compose
的选择取决于你 - 这只是一个符号差异;无论哪种方式,法律都得到了满足。
// 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%确定你的功能实际上是在做什么。
Maybe(person)
开始person.names
财产person.names
的第一个索引 - 它是一个数组还是什么?或者名字的第一个字母?.value
财产?我们在这里期待一个单子? (看看.chain
与.map
相比Maybe
和Either
实现我从民间故事中链接)/
上的值''
加入值这是我对正在发生的事情的最好猜测,但我不能在这里描绘您的数据或理解您正在尝试的计算。如果您提供更具体的数据示例和预期输出,我可能能够帮助您制定更具体的答案。
备注
几年前我也在你的船上;我的意思是,进入函数式编程。我想知道所有小部件是如何组合在一起的,并且实际上产生了一个人类可读的程序。
只有当功能技术应用于整个系统时,才能观察到函数式编程提供的大多数好处。首先,你会觉得你必须引入大量的依赖,只是以“功能方式”重写一个函数。但是一旦你在程序中的更多地方发挥了这些依赖关系,你就可以开始左右削减复杂性。这真的很酷,但需要一段时间才能让你的程序(以及你的头脑)在那里。
事后看来,这可能不是一个好的答案,但我希望这能帮助你。这对我来说是一个非常有趣的话题,我很乐意协助回答你的任何其他问题^ _ ^