我知道这个问题已经有将近 4 年的历史了,并且已经有了答案,但是为了将来遇到这个问题的任何人,我想补充一些额外的信息。具体来说,我想尝试回答2个问题:
- 如何将多个返回单个值的 Select 组合成一个返回一系列值的 Select?
- 当解决方案路径注定失败时,是否可以提前返回?
链接选择
Select 在transformers 库中被实现为一个monad 转换器(如图),但让我们看看如何单独为Select 实现>>=:
(>>=) :: Select r a -> (a -> Select r b) -> Select r b
Select g >>= f = Select $ \k ->
let choose x = runSelect (f x) k
in choose $ g (k . choose)
我们首先定义一个新的Select,它接受a -> r 类型的输入k(回想一下Select 包装了(a -> r) -> a 类型的函数)。您可以将k 视为一个函数,它为给定的a 返回r 类型的“分数”,Select 函数可以使用它来确定返回哪个a。
在我们的新Select 中,我们定义了一个名为choose 的函数。此函数将一些x 传递给函数f,这是一元绑定的a -> m b 部分:它将m a 计算的结果转换为新的计算m b。所以f 将采用x 并返回一个新的Select,choose 然后使用我们的评分函数k 运行。您可以将choose 视为一个函数,它询问“如果我选择x 并将其传递给下游,最终结果会是什么?”
在第二行,我们返回choose $ g (k . choose)。函数k . choose是choose和我们原来的评分函数k的组合:它接受一个值,计算选择该值的下游结果,并返回该下游结果的分数。换句话说,我们创建了一种“千里眼”的评分函数:它不是返回给定值的分数,而是返回我们将得到的最终结果的分数如果我们选择了那个值 .通过将我们的“千里眼”评分函数传递给g(我们绑定到的原始Select),我们能够选择导致我们正在寻找的最终结果的中间值。一旦我们有了这个中间值,我们只需将它传回choose 并返回结果。
这就是我们如何能够将单值 Select 串在一起,同时传入一个对值数组进行操作的评分函数:每个 Select 都在对选择值的假设最终结果进行评分,而不一定是值本身。 applicative 实例遵循相同的策略,唯一的区别是下游 Select 的计算方式(而不是将候选值传递给 a -> m b 函数,它将候选函数映射到第二个 Select 上。)
提前返回
那么,我们如何在提早返回的同时使用 Select 呢?我们需要某种方式在构建 Select 的代码范围内访问评分函数。一种方法是在另一个 Select 中构造每个 Select,如下所示:
sequenceSelect :: Eq a => [a] -> Select Bool [a]
sequenceSelect [] = return []
sequenceSelect domain@(x:xs) = select $ \k ->
if k [] then runSelect s k else []
where
s = do
choice <- elementSelect (x:|xs)
fmap (choice:) $ sequenceSelect (filter (/= choice) domain)
这允许我们测试正在进行的序列,并在递归失败时将其短路。 (我们可以通过调用k [] 来测试序列,因为评分函数包括我们递归排列的所有前置。)
这是整个解决方案:
import Data.List
import Data.List.NonEmpty (NonEmpty(..))
import Control.Monad.Trans.Select
validBoard :: [Int] -> Bool
validBoard qs = all verify (tails qs)
where
verify [] = True
verify (x:xs) = and $ zipWith (\i y -> x /= y && abs (x - y) /= i) [1..] xs
nqueens :: Int -> [Int]
nqueens boardSize = runSelect (sequenceSelect [1..boardSize]) validBoard
sequenceSelect :: Eq a => [a] -> Select Bool [a]
sequenceSelect [] = return []
sequenceSelect domain@(x:xs) = select $ \k ->
if k [] then runSelect s k else []
where
s = do
choice <- elementSelect (x:|xs)
fmap (choice:) $ sequenceSelect (filter (/= choice) domain)
elementSelect :: NonEmpty a -> Select Bool a
elementSelect domain = select $ \p -> epsilon p domain
-- like find, but will always return something
epsilon :: (a -> Bool) -> NonEmpty a -> a
epsilon _ (x:|[]) = x
epsilon p (x:|y:ys) = if p x then x else epsilon p (y:|ys)
简而言之:我们递归地构造一个 Select,在使用它们时从域中删除元素,如果域已用尽或我们走错了路,则终止递归。
另一个新增功能是epsilon 函数(基于希尔伯特的epsilon operator)。对于大小为 N 的域,它最多会检查 N - 1 个项目......这听起来可能不会节省很多,但正如您从上面的解释中知道的那样,p 通常会启动整个计算的其余部分,所以最好尽量减少谓词调用。
sequenceSelect 的好处在于它的通用性:它可以用来创建任何Select Bool [a] 的地方
- 我们在不同元素的有限域内进行搜索
- 我们想要创建一个序列,其中每个元素只包含一次(即域的排列)
- 我们想要测试部分序列并在它们未通过谓词时放弃它们
希望这有助于澄清事情!
附:这是一个 Observable 笔记本的链接,我在其中用 Javascript 实现了 Select monad 以及 n-queens 求解器的演示:https://observablehq.com/@mattdiamond/the-select-monad