Haskell中任意多参数函数的通用无参组合方法技术问询
你遇到的这个问题其实是Haskell里很典型的多参数函数组合场景——想要把多个单参数转换函数的输出,自动喂给一个多参数的核心函数,又不想写一堆重复的参数传递或者绕脑子的flip操作,更不想为每个参数数量N手动写composeN函数对吧?
下面给你几个最实用的优雅解决方案:
方案1:用Applicative(最推荐、最简洁)
Haskell的基础库中,函数类型(->) r本身就实现了Applicative实例,这刚好能完美解决你的问题。核心就是用<$>(即fmap)和<*>操作符来组合函数,完全不需要手动处理参数:
先看N=2的情况
对应你最初的例子:
type T1 = Int; type T2 = Int; type T3 = Int type X1 = Int; type X2 = Int g :: T1 -> T2 -> T3; g = (+) f1 :: X1->T1; f1 = (+1) f2 :: X2->T2; f2 = (+2) -- 替代你手写的c和复杂的flip版本 nc = g <$> f1 <*> f2
测试一下:nc 1 2会计算g (f1 1) (f2 2) = 2 + 4 = 6,和原函数功能完全一致,而且写法极其直观。
扩展到任意N的情况
比如你提到的N=3的场景:
type T4 = Int; type X3 = Int g3 :: T1->T2->T3->T4; g3 t1 t2 t3 = t1+t2+t3 f3 :: X3->T3; f3 = (+3) -- 直接追加<*>和对应的转换函数即可 c3 = g3 <$> f1 <*> f2 <*> f3
此时c3的类型是X1 -> X2 -> X3 -> T4,完全符合你的需求。测试c3 1 2 3会得到2+4+6=12,正确无误。
原理说明
<$>(fmap)把核心函数g的第一个参数替换成f1的输出,把T1->T2->T3转换成X1 -> (T2->T3);- 每一个
<*>都会把下一个转换函数(比如f2)的输出喂给当前的函数,逐步把多参数函数“展开”成接受所有输入类型的函数; - 不管N多大,只要跟着核心函数后面依次追加
<*> f1、<*> f2...直到覆盖所有参数即可,完全不用写重复的composeN。
方案2:用Arrow(适合复杂数据流场景)
如果你对Arrow类型类感兴趣,也可以用它来实现,但相比Applicative会稍显繁琐,更适合处理多分支的数据流:
首先导入Arrow模块:
import Control.Arrow
方式1:用proc notation(直观但类似显式参数)
c3_arrow :: X1 -> X2 -> X3 -> T4 c3_arrow = proc x1 x2 x3 -> do t1 <- arr f1 -< x1 t2 <- arr f2 -< x2 t3 <- arr f3 -< x3 arr g3 -< (t1, t2, t3)
方式2:纯组合式(需要自定义uncurryN)
如果想完全用组合子,需要先把多参数函数转换成接受元组的函数,再组合箭头:
-- 自定义三元组的uncurry函数 uncurry3 :: (a->b->c->d) -> ((a,b,c)->d) uncurry3 f (a,b,c) = f a b c c3_arrow' = (arr f1 &&& arr f2 &&& arr f3) >>> arr (uncurry3 g3)
不过这种方式需要为每个N写对应的uncurryN,不如Applicative简洁,所以更适合处理复杂的数据流转换场景。
关于Reader Monad的疑问
你提到的Reader Monad其实和方案1的本质是一样的——因为Reader Monad的类型就是Reader r a,等价于r -> a,它的Applicative实例就是我们方案1中用到的函数Applicative。所以方案1其实就是在隐式使用Reader的Applicative能力,完全满足你不同输入类型的需求。
总结
- 对于绝大多数场景,Applicative的
<$>+<*>组合是最优解,简洁、易读、无需额外扩展或自定义函数; - Arrow适合处理更复杂的数据流逻辑,比如分支、条件转换等;
- 完全不需要手动为每个N编写
composeN,Haskell的Applicative已经帮我们封装好了这个逻辑。
内容的提问来源于stack exchange,提问作者Evg

