规范外连接 zip 函数

Posted

技术标签:

【中文标题】规范外连接 zip 函数【英文标题】:Canonical outer join zip function 【发布时间】:2012-03-01 04:31:39 【问题描述】:

如果您将列表中每个元素的(隐式)索引视为它们的键,那么zipWith 有点像关系内连接。它只处理两个输入都有值的键:

zipWith (+) [1..5] [10..20] == zipWith (+) [1..11] [10..14] == [11,13,15,17,19] 

是否有对应外连接的规范对应函数?比如:

outerZipWith :: (a -> b -> c) -> a -> b -> [a] -> [b] -> [c]
outerZipWith _ _ _ [] [] = []
outerZipWith f a' b' [] (b:bs) = f a' b : outerZipWith f a' b' [] bs
outerZipWith f a' b' (a:as) [] = f a b' : outerZipWith f a' b' as []
outerZipWith f a' b' (a:as) (b:bs) = f a b : outerZipWith f a' b' as bs

或许

outerZipWith' :: (a -> b -> c) -> Maybe a -> Maybe b -> [a] -> [b] -> [c]
outerZipWith' _ _ _ [] [] = []
outerZipWith' _ Nothing _ [] _ = []
outerZipWith' _ _ Nothing _ [] = []
outerZipWith' f a' b' [] (b:bs) = f (fromJust a') b : outerZipWith f a' b' [] bs
outerZipWith' f a' b' (a:as) [] = f a (fromJust b') : outerZipWith f a' b' as []
outerZipWith' f a' b' (a:as) (b:bs) = f a b : outerZipWith f a' b' as bs

这样我就可以了

outerZipWith (+) 0 0 [1..5] [10..20]  == [11,13,15,17,19,15,16,17,18,19,20]
outerZipWith (+) 0 0 [1..11] [10..14] == [11,13,15,17,19,6,7,8,9,10,11]

我发现自己时不时需要它,我宁愿使用一个常见的习惯来使我的代码更可写(并且更易于维护),而不是实现outerZipWith,或者做if length as < length bs then zipWith f (as ++ repeat a) bs else zipWith f as (bs ++ repeat b)

【问题讨论】:

【参考方案1】:

这看起来很尴尬,因为您试图跳到最后而不是处理原始操作。

首先,zipWith 在概念上是zip,后跟map (uncurry ($))。这是很重要的一点,因为 (un)currying 是 zipWith 完全可能的原因。给定类型为[a][b] 的列表,要应用函数(a -> b -> c) 并获得[c] 类型的东西,你显然需要每个输入之一。执行此操作的两种方法正是列表的两个 Applicative 实例,zipWith 是其中之一的 liftA2。 (另一个实例是标准实例,它提供笛卡尔积——如果您愿意,可以使用交叉连接)。

您想要的语义不对应于任何明显的Applicative 实例,这就是为什么它要困难得多。首先考虑outerZip :: [a] -> [b] -> [?? a b] 及其类型。结果列表的每个元素都可以是ab,或两者兼而有之。这不仅不对应于任何标准数据类型,而且用它们来表达也很尴尬,因为您无法从表达式 (A + B + A*B) 中考虑任何有用的内容。

这样的数据类型有它自己的用途,这就是为什么我有my own package defining one。我记得也有一个关于 hackage 的(我认为辅助功能比我的少),但我不记得它叫什么了。

坚持标准的东西,你最终需要一个合理的“默认值”,它大致转化为拥有一个Monoid 实例并使用标识值来填充列表。但是,使用 newtype 包装器等在适当的 Monoid 之间进行转换可能不会比您当前的实现更简单。


顺便说一句,您关于列表索引作为键的评论实际上可以进一步发展;任何具有类似键概念的Functor 都与Reader monad 同构,也就是从键到值的显式函数。 Edward Kmett 和往常一样,有一堆对这些抽象概念进行编码的包,在本例中是从the representable-functors package 构建的。可能会有所帮助,如果您不介意编写十几个实例只是为了至少开始......

【讨论】:

不会outerZip :: a -> b -> [a] -> [b] -> [(a,b)] 更像outerZip :: (a -> c) -> (b -> d) -> c -> d -> [a] -> [b] -> [(c, d)] 包容或类型(如您的These)可能是必要的第一步。至少,这是一个很好的起点。 outerZip f a b as bs = map (f . fromThese a b) $ zipThese as bs 非常干净。我喜欢它。 @rampion: 哦,还有——因为收集像这样的输入作为中间步骤显然是用于像These 这样的类型,所以我尝试非常全面地使用预测、案例分析功能,和These 的谓词,特别是为了支持干净地定义像你的outerZip 这样的东西。 :]【参考方案2】:

与其用一个常数填充较短的列表,为什么不提供一个值列表,直到到达较长列表的末尾?这也消除了对 Maybe 的需要,因为列表可以为空(或有限长度)。

我找不到任何标准,但没有按照您展示的方式完全重新实现 zipWith,我开发了您的 length 测试版本,如下所示:

outerZipWith :: (a -> b -> c) -> [a] -> [b] -> [a] -> [b] -> [c]
outerZipWith f as bs as' bs' = take n $ zipWith f (as ++ as') (bs ++ bs')
  where n = max (length as) (length bs)

【讨论】:

这不会编译(至少对我来说)。【参考方案3】:

这种事情经常出现。这是PACT algebra 的cogroup 操作。在我工作的地方,我们使用类型类来区分这三种操作:

    zip:结构交叉。 align: 结构联合。 liftA2:结构笛卡尔积。

Paul Chiusano on his blog 对此进行了讨论。

【讨论】:

Paul Chiusano 的博客文章直截了当且富有洞察力。感谢您链接。

以上是关于规范外连接 zip 函数的主要内容,如果未能解决你的问题,请参考以下文章

结合左外连接和内连接 + 聚合函数 - 空结果集问题

数据库编程1 Oracle 过滤 函数 分组 外连接 自连接

在左外连接中使用 Sum 函数

数据库编程2 Oracle 过滤 函数 分组 外连接 自连接

MySQL的几种表外连接及PHP操作MySQL的函数

表本身的完全外连接并运行一些窗口函数