这个答案分为两部分。第一部分直接解决了这个问题。第二部分切线(字面意思)为了挖掘第一部分背后的数学:因此它可能被证明是有限兴趣的困难材料,但我认为一些极端主义者可能会喜欢它。
到目前为止,我看到的答案巧妙地使用了列表推导或它们的一元等效项,但它们使用相等来排除重复项,因此需要额外的Eq
约束。这是一个使所有元素对位于两个不同位置的解决方案。
首先,我编写了一个方便的函数,它用其他位置的元素列表装饰列表的每个元素:“选择一个并留下其他”的所有方法。每当使用列表来收集东西以进行选择而不替换时,它都非常有用,而且我发现我经常使用它。
picks :: [x] -> [(x, [x])]
picks [] = []
picks (x : xs) = (x, xs) : [(y, x : ys) | (y, ys) <- picks xs]
请注意,map fst . picks = id
结果的每个位置中的选定元素都是原始列表中该位置的元素:这就是我所说的“装饰”。
现在很容易选择两个,使用与其他答案相同的列表理解方法。但是,我们可以从它的 中选择,而不是从列表本身中选择第一个组件,picks
同时获取第二个组件的候选列表。
allPairs :: [x] -> [(x, x)]
allPairs xs = [(y, z) | (y, ys) <- picks xs, z <- ys]
拿下三倍同样容易,拿picks
两次。
allTriples :: [x] -> [(x, x, x)]
allTriples ws = [(x, y, z) | (x, xs) <- picks ws, (y, ys) <- picks xs, z <- ys]
为了统一起见,几乎很想使代码效率稍低一些,编写(z, _) <- picks ys
而不是z <- ys
两者兼而有之。
如果输入列表没有重复项,您将不会在输出中得到任何重复的元组,因为元组从不同位置获取它们的元素。然而,你会得到
Picks> allPairs ["cat", "cat"]
[("cat","cat"),("cat","cat")]
为避免这种情况,请随意使用allPairs . nub
,它会在选择之前删除重复项,并再次要求Eq
元素类型的实例。
仅适用于极端分子:容器、微积分、comonads 和combinatorics ahoy!
picks
是由微积分产生的更一般构造的一个实例。这是一个有趣的事实,对于任何给定的容器类型的函子f
,它的数学导数 ∂f 表示 -f
结构,其中一个元素被删除。例如,
newtype Trio x = Trio (x, x, x) -- x^3
有导数
data DTrio x = Left3 ((), x, x) | Mid3 (x, (), x) | Right3 (x, x, ()) -- 3*x^2
许多操作可以与这种构造相关联。想象一下我们真的可以使用∂(我们可以使用类型族对其进行编码)。那么我们可以说
data InContext f x = (:-) {selected :: x, context :: ∂f x}
给出一种由上下文装饰的选定元素。我们当然应该期望有手术
plug :: InContext f x -> f x -- putting the element back in its place
如果我们在其节点被视为子树容器的树中快速移动,则此plug
操作会将我们移向根。
我们也应该期望InContext f
成为一个comonad,与
counit :: InContext f x -> x
counit = selected
突出选定的元素和
cojoin :: InContext f x -> InContext f (InContext f x)
用它的上下文装饰每个元素,展示你可以重新聚焦的所有可能方式,选择不同的元素。
不可估量的彼得汉考克曾经向我建议,我们也应该期望能够“向下”移动(意思是“远离根”),收集所有可能的方法来从整个结构中选择上下文中的元素。
picks :: f x -> f (InContext f x)
应该用它的上下文装饰x
输入结构中的每个元素。f
我们应该期待
fmap selected . picks = id
这是我们之前的法律,但也
fmap plug (picks fx) = fmap (const fx) fx
告诉我们每个装饰元素都是原始数据的分解。我们上面没有那条法律。我们有
picks :: [x] -> [(x, [x])]
用一些不太像它的上下文的东西来装饰每个元素:仅仅从其他元素的列表中,你看不到“洞”在哪里。事实上,
∂[] x = ([x], [x])
将孔之前的元素列表与孔之后的元素分开。可以说,我应该写
picks :: [x] -> [(x, ([x], [x]))]
picks [] = []
picks (x : xs) = (x, ([], xs)) : [(y, (x : ys, ys')) | (y, (ys, ys')) <- picks xs]
这当然也是一个非常有用的操作。
但真正发生的事情是相当明智的,只是轻微的滥用。在我最初编写的代码中,我在本地[]
表示有限包或无序列表。包是没有特定位置概念的列表,因此如果您选择一个元素,它的上下文就是其余元素的包。确实
∂Bag = Bag -- really? why?
所以正确的概念picks
确实是
picks :: Bag x -> Bag (x, Bag x)
代表Bag
,[]
这就是我们所拥有的。此外,对于袋子来说,plug
是公正的(:)
,并且直到袋子相等(即排列),第二个定律picks
确实成立。
另一种看待包包的方式是作为幂级数。袋子是任何大小的元组的选择,所有可能的排列(n!用于大小n)被识别。所以我们可以把它写成一个乘以阶乘的幂的大总和,因为你必须除以 x^n 除以 n!说明每个 n! 您可以选择 x 的订单会为您提供相同的包。
Bag x = 1 + x + x^2/2! + x^3/3! + ...
所以
∂Bag x = 0 + 1 + x + x^2/2! + ...
横向移动系列。事实上,你很可能已经认识到 for 的幂级数Bag
是 for exp
(或e ^x) 的幂级数,它以其自身的导数而闻名。
所以,呸!你去吧。由指数函数的数据类型解释自然产生的操作,作为它自己的导数,是用于解决基于无替换选择的问题的便捷工具包。