定义像
这样的运算符非常容易(@) :: [x -> y] -> [x] -> [y]
获取函数列表和输入列表,并返回输出列表。有两种明显的方法可以实现这一点:
要么定义也同样微不足道。关于它的好处是,您现在可以执行类似
的操作foo :: X -> Y -> Z -> R
bar :: [X] -> [Y] -> [Z] -> [R]
bar xs ys zs = [foo] @@ xs @@ ys @@ zs
这概括为任意数量的函数参数。
到目前为止一切顺利。现在针对问题:如何更改@@
的类型签名,使bar
的类型签名变为
bar :: [X] -> [Y] -> [Z] -> [[[R]]]
实现这种类型的功能并不困难;其中任何一个都会这样做:
bar xs ys zs = map (\ x -> map (\ y -> map (\ z -> foo x y z) zs) ys) zs
bar xs ys zs = map (\ z -> map (\ y -> map (\ x -> foo x y z) xs) ys) zs
我对于得到的结果并不挑剔。但我无法弄清楚如何调整@@
运算符来执行此操作。
一个显而易见的尝试是
(@@) :: [x -> y] -> [x] -> [[y]]
实现这一点并不难,但它对你没有帮助。现在你有了
[foo] @@ xs :: [[Y -> Z -> R]]
这不是@@
的有效输入。没有明显的方法可以知道有多少级别的列表可以通过该功能获得;显然这种方法是死路一条。
我已经尝试了其他几种可能的类型签名,但没有一种能让我更接近答案。有人可以给我一个解决方案,或解释为什么不存在?
答案 0 :(得分:8)
你已经找到了为什么这很麻烦。您的功能(@@)
适用于不同类型的输入(例如[x->y]
,[[x -> y]]
等。这意味着您@@
的类型签名限制性太强;你需要添加一些多态性,使其更加通用,可以将它与嵌套列表一起使用。由于Haskell使用类型类实现多态,这是一个很好的尝试方向。
实际上,如果您知道LHS的类型,则会遇到此问题,您可以唯一地确定RHS和结果。当输入的类型为[a->b]
时,RHS必须为[a]
,结果必须为[[b]]
。这可以简化为a->b
的输入,[a]
的RHS和[b]
的结果。由于LHS确定了其他参数和结果,我们可以使用fundeps或类型族来表示其他类型。
{-# LANGUAGE TypeFamilies, UndecidableInstances #-}
class Apply inp where
type Left inp :: *
type Result inp :: *
(@@) :: inp -> [Left inp] -> [Result inp]
现在我们有了一个类型类,我们可以为函数创建一个明显的实例:
instance Apply (a -> b) where
type Left (a -> b) = a
type Result (a -> b) = b
(@@) = map
列表实例也不错:
instance Apply f => Apply [f] where
type Left [f] = Left f
type Result [f] = [Result f]
l @@ r = map (\f -> f @@ r) l
-- or map (@@ r) l
现在我们的类方法@@
应该适用于任意深度列表。以下是一些测试:
*Main> (+) @@ [1..3] @@ [10..13]
[[11,12,13,14],[12,13,14,15],[13,14,15,16]]'s
let foo :: Int -> Char -> Char -> String
foo a b c = b:c:show a
*Main> foo @@ [1,2] @@ "ab" @@ "de"
[[["ad1","ae1"],["bd1","be1"]],[["ad2","ae2"],["bd2","be2"]]]
您可能希望查看printf
实现以获得更多灵感。
编辑:发布此消息后不久,我意识到可以将Apply
类中的容器类型从List
推广到Applicative
,然后使用应用实例代替map。这将允许常规列表和ZipList
行为。
答案 1 :(得分:7)
实际上,这根本不需要类型类!通过避免类型类,你会失去一些方便,但就是这样。
关键是,尽管重用了单个组合子,但多态性允许每种用法的类型不同。这与Applicative
之类的f <$> xs <*> ys <*> zs
样式表达式背后的原理相同,此处的最终结果将类似。因此,我们会为任何Functor
执行此操作,而不仅仅是列表。
这与Applicative
版本之间的区别在于我们会在每个步骤中深入嵌套Functor
。必要的多态性需要最内层的灵活性,所以为了实现这一点,我们将使用continuation-ish技巧,其中每个组合子的结果是接受在最内层使用的变换的函数。
我们需要两个运营商,一个启动链,另一个继续递增。从后者开始:
(@@) :: (Functor f) => (((a -> b) -> f c) -> r) -> f a -> (b -> c) -> r
q @@ xs = \k -> q (\f -> k . f <$> xs)
这会在右侧采用新参数,在左侧采用正在进行的表达式。结果采用函数k
,它指定了如何获取最终结果。 k
与正在进行的表达式相结合,并且都映射到新参数上。这是令人费解的,但对于那些将CPS风格的代码区分开来的人来说应该很熟悉。
(<@) :: (Functor f, Functor g) => f (a -> b) -> g a -> (b -> c) -> f (g c)
fs <@ xs = (<$> fs) @@ xs
通过简单地映射第一个参数上的所有其他内容来启动链。
与更简单的Applicative
案例不同,我们还需要明确地结束链。与Cont
monad一样,最简单的方法是将结果应用于identity函数。我们将给出一个有用的名称:
nested = ($ id)
现在,我们可以做这样的事情:
test2 :: [X -> Y -> R] -> [X] -> [Y] -> [[[R]]]
test2 fs xs ys = nested (fs <@ xs @@ ys)
test3 :: [X -> Y -> Z -> R] -> [X] -> [Y] -> [Z] -> [[[[R]]]]
test3 fs xs ys zs = nested (fs <@ xs @@ ys @@ zs)
不像类型类版本那么漂亮,但它可以工作。