如何优雅地处理算法的自定义点及其参数的约束?

问题描述

以下面的算法为例,计算两个序列的最长公共子序列,复制自Rosetta Code

longest xs ys = if length xs > length ys then xs else ys
 
lcs [] _ = []
lcs _ [] = []
lcs (x:xs) (y:ys) 
  | x == y    = x : lcs xs ys
  | otherwise = longest (lcs (x:xs) ys) (lcs xs (y:ys))

lcs 隐含地假设两个参数都是 Eq a => [a] 类型;实际上,如果我尝试明确地给出签名 lcs :: [a] -> [a] -> [a],那么 x == y 所在的行会发生错误,而签名 lcs :: Eq a => [a] -> [a] -> [a] 有效。

现在让我们假设我有两个列表 l1l2,都是 [(a,b)] 类型,并且我想要它们之间的 LCS,但是以使用 {{1 == 定义中的 }} 运算符仅在每个元素的 lcs 之间(显然 snd 必须属于 b 类型类)。

我不仅可以为 Eq 提供两个列表,还可以提供相等运算符,在上面的特定示例中为 lcs(==) `on` snd 的签名将是 lcs

然而,即使在想要使用普通 (a -> a -> Bool) -> [a] -> [a] -> [a] 的微不足道的情况下,这也会迫使用户提供相等运算符,并且将 (==) 参数包装在 (a -> a) 中不会也无济于事。

换句话说,我觉得在一般情况下,通过 Maybe 对参数的约束是可以的,但在其他情况下,人们可能希望传递一个自定义相等运算符来删除该约束,这使我成为功能的东西重载。

我应该怎么做?

解决方法

您只需提供两个 - 自定义运算符版本 lcsBy 和根据该版本实现的 Eq a => 版本。

lcsBy :: (a->a->Bool) -> [a] -> [a] -> [a]
...
lcsBy compOp (x:xs) (y:ys) 
 | compOp x y  = ...
 ...

lcs :: Eq a => [a] -> [a] -> [a]
lcs = lcsBy (==)

这类似于基础库如何提供 maximummaximumBy

,

提供两者是更简单的选择,正如@leftaroundabout 所写。

不过,在某些特定情况下,还是有一些方法可以调用像

这样的函数
lcs :: Eq a => [a] -> [a] -> [a]

提供像您这样的自定义相等运算符。例如,

{-# LANGUAGE ScopedTypeVariables #-}
import Data.Coerce
import Data.Function

newtype OnSnd a b = OnSnd { getPair :: (a,b) }
instance Eq b => Eq (OnSnd a b) where
   (==) = (==) `on` (snd . getPair)

lcsOnSnd :: forall a b. Eq b => [(a,b)] -> [(a,b)]
lcsOnSnd xs ys = coerce $ lcs (coerce xs) (coerce ys :: [OnSnd a b])
-- OR turn on TypeApplications,then:
-- lcsOnSnd = coerce (lcs @(OnSnd a b))

使用零成本安全强制将 [(a,b)] 转换为 [OnSnd a b],应用 lcs(将使用 == 的自定义 OnSnd a b),并转换结果返回(零成本,再次)。

然而,要使这种方法起作用,== 必须在顶层定义,即它不能是依赖于 lcsOnSnd 的附加参数的泛型闭包。

>