【问题标题】:Dynamic List Comprehension in HaskellHaskell 中的动态列表理解
【发布时间】:2013-02-14 11:02:22
【问题描述】:

假设我有一个列表推导,它返回一个序列列表,其中选择的元素相互依赖(参见下面的示例)。有没有办法(方便地)根据早期计算对元素的数量及其相关条件进行编程?例如,返回类型 [[a,b,c]] 或 [[a,b,c,d,e]] 取决于程序中的另一个值?此外,除了列表理解之外,还有其他/更好的方法来制定相同的想法吗?

(我认为有可能,虽然麻烦且有限,但可以编写一个更大的列表推导式开始并通过向 s 添加一个参数和辅助函数来修剪它,这些函数可以使一个或多个元素一个可以在以后轻松过滤的值,并且默认情况下关联的条件为True。)

s = [[a, b, c, d] | a <- list, someCondition a, 
                    b <- list, b /= a, not (someCondition b), 
                    otherCondition a b,
                    c <- list, c /= a, c /= b, not (someCondition c),
                    otherCondition b c,
                    d <- list, d /= a, d /= b, d /= c,
                    someCondition d, someCondition (last d),
                    otherCondition c d]

【问题讨论】:

  • 你能发布你的实际代码吗?
  • @krshekhar:原来的格式是正确的。
  • @DietrichEpp ...当然,它在这里:stackoverflow.com/questions/14843466/… ...它计算序列以解决迷宫,但路径的长度基于另一个计算。
  • 旁注:不要写someCondition a == True,只写someCondition a。同样,not (someCondition b) 而不是 someCondition b == False。这种方式更具可读性。
  • @DietrichEpp 对不起,我错过了理解。

标签: haskell


【解决方案1】:

这个问题非常难以理解。

有没有办法(方便地)根据早期计算对元素的数量及其相关条件进行编程?

问题是“程序”在这句话中并不是一个真正可以理解的动词,因为人类对计算机进行编程,或对 VCR 进行编程,但您不能“对数字进行编程”。所以我不明白你在这里想说什么。

但是我可以给你代码审查,也许通过代码审查我可以理解你提出的问题。

不请自来的代码审查

听起来你可能正在尝试通过消除死胡同来解决迷宫。

你的代码实际上做了什么:

  1. 生成非死角或与死角相邻的单元格列表,称为filtered

  2. 从第 1 步生成相邻单元格的序列,sequences

  3. 将四个这样的相邻序列连接成一条路线。

主要问题:只有在正确的路线正好是八格长时才有效!尝试解决这个迷宫:

[E]-[]-[]-[] | [ ]-[ ]-[ ]-[ ] | [ ]-[ ]-[ ]-[ ] | [ ]-[ ]-[ ]-[ ] | [ ]-[ ]-[ ]-[E]

所以,从代码审查开始,听起来你的问题是:

如果我不知道提前多长时间,如何生成列表?

解决方案

您可以通过搜索解决迷宫(DFS、BFS、A*)。

import Control.Monad

-- | Maze cells are identified by integers
type Cell = Int

-- | A maze is a map from cells to adjacent cells
type Maze = Cell -> [Cell]

maze :: Maze
maze = ([[1],     [0,2,5],     [1,3],   [2],
         [5],     [4,6,1,9],   [5,7],   [6,11],
         [12],    [5,13],      [9],     [7,15],
         [8,16],  [14,9,17],   [13,15], [14,11],
         [12,17], [13,16,18],  [17,19], [18]] !!)

-- | Find paths from the given start to the end
solve :: Maze -> Cell -> Cell -> [[Cell]]
solve maze start = solve' [] where
  solve' path end =
    let path' = end : path
    in if start == end 
       then return path'
       else do neighbor <- maze end
               guard (neighbor `notElem` path)
               solve' path' neighbor

函数solve 通过深度优先搜索工作。它不是将所有内容都放在一个列表推导中,而是递归地工作。

  1. 为了找到从startend的路径,如果start /= end

  2. 查看与末尾相邻的所有单元格,neighbor &lt;- maze end

  3. 确保我们没有回溯到单元格guard (negihbor `notElem` path)

  4. 尝试查找从startneighbor 的路径。

不要试图一下子理解整个函数,只要理解一点递归即可。

总结

如果要找到cell 0到cell 19的路由,递归:我们知道cell 18和cell 19是相连的(因为它们是直接相连的),所以我们可以尝试解决从cell 0到cell的路由问题单元格 0 到单元格 18。

这是递归。

脚注

守卫,

someCondition a == True

等价于,

someCondition a

因此也等价于,

(someCondition a == True) == True

或者,

(someCondition a == (True == True)) == (True == (True == True))

或者,

someCondition a == (someCondition a == someCondition a)

第一个 someCondition a 很好。

关于do 符号的脚注

上例中的do 符号等价于列表推导,

do neighbor <- maze end
   guard (neighbor `notElem` path)
   solve' path' neighbor

列表理解语法中的等效代码是,

[result | neighbor <- maze end,
          neighbor `notElem` path,
          result <- solve' path' neighbor]

【讨论】:

  • ...感谢您花时间查看我的代码并解释 do 表示法如何等同于列表理解。递归迷宫求解器的一个非常简洁的例子!你能告诉我如何实现它吗?我尝试传递不同的参数来启动(有和没有特定的“结束”),但每次都收到错误消息。
  • ...只是一个旁注,看起来你在写的时候漏掉了一些东西,“主要问题:这只有在正确的路线正好是八格长的情况下才有效!”首先,我的求解器很灵活,并且在构建路径时允许在两个单元序列中重叠,因此,修剪重复项,路径可能是 8 或更少,因为它现在被评估。其次,我的问题完全是关于如何使路径长度(甚至)更灵活,并且仍然使用组合而不是递归来解决它。这就是我自我指导练习的重点。
  • ...Dietrich,也许不是(或除此之外)试图通过查看代码来理解我的问题,而是让我更好地向您解释我的问题。事实上,正因为如此,你没有回答我的问题,而是给了我们你自己的解决方案来解决不同的问题。我的问题不是要解决迷宫问题,而是要了解如何使列表理解(或类似的无需递归计算组合的过程)更加灵活。
  • 你从来没有在你的问题中提到递归,甚至一次也没有,那么我怎么知道你想要一个不使用递归的答案呢?我按照要求给了你一个列表理解,它取决于“以前的值”,方法是将它们累积在一个函数参数中。
  • ..我花了一些时间来学习/理解你的递归解决方案,我必须告诉你 - 对我来说它看起来很棒,它教会了我很多,谢谢!
【解决方案2】:

有没有办法(方便地)根据早期计算对元素的数量及其相关条件进行编程?例如,返回类型 [[a,b,c]] 或 [[a,b,c,d,e]] 取决于程序中的另一个值?

我想您想在类型签名中静态编码列表(或向量)的长度。无法在类型级别检查标准列表的长度。

One approach 这样做是使用phantom types,并引入将编码不同大小的虚拟数据类型:

newtype Vector d = Vector { vecArray :: UArray Int Float }

-- using EmptyDataDecls extension too
data D1
data D2
data D3

现在您可以创建具有不同类型的不同长度的向量:

vector2d :: Float -> Float -> Vector D2
vector2d x y = Vector $ listArray (1,2) [x,y]

vector3d :: Float -> Float -> Float -> Vector D3
vector3d x y z = Vector $ listArray (1,3) [x,y,z]

如果输出的长度取决于输入的长度,那么考虑使用type-level arithmetics 对输出进行参数化。 您可以通过谷歌搜索“Haskell 静态大小的向量”找到更多信息。

一个更简单的解决方案是使用元组,它们固定长度的。如果您的函数可以生成 3 元组或 5 元组,请使用 Either 数据类型包装它们:`Either (a,b,c) (a,b,c,d,e)。

【讨论】:

    【解决方案3】:

    看起来您正试图通过从有限域中进行唯一选择来解决一些逻辑难题。参考这些:

    这对我们有帮助的方式是,我们在从中挑选域名时随身携带;并且下一个选择是从包含上一个选择之后剩下的内容的狭窄域中进行的,因此自然形成了一条链。例如

    p43 = sum [ fromDigits [v0,v1,v2,v3,v4,v5,v6,v7,v8,v9]
                | (dom5,v5) <- one_of [0,5] [0..9]   -- [0..9] is the
                , (dom6,v6) <- pick_any dom5         --   initial domain
                , (dom7,v7) <- pick_any dom6          
                , rem (100*d5+10*d6+d7) 11 == 0 
                ....
    
    -- all possibilities of picking one elt from a domain
    pick_any :: [a] -> [([a], a)]
    pick_any []     = [] 
    pick_any (x:xs) = (xs,x) : [ (x:dom,y) | (dom,y) <- pick_any xs]
    
    -- all possibilities of picking one of provided elts from a domain
    --                           (assume unique domains, i.e. no repetitions)
    one_of :: (Eq a) => [a] -> [a] -> [([a], a)]
    one_of ns xs = [ (ys,y) | let choices = pick_any xs, n <- ns,
                              (ys,y) <- take 1 $ filter ((==n).snd) choices ]
    

    作为列表理解的一部分,您可以简单地检查答案中的一些元素:

    s = [answer | a <- .... , let answer=[....] , length answer==4 ]
    

    或者只是根据条件创建不同的答案,

    s = [answer | a <- .... , let answer=if condition then [a,b,c] else [a]]
    

    【讨论】:

    • ...感谢您的回答和建议。看起来这些参考资料需要我进行一些研究。我试图在你的 cmets 中测试最后的建议但失败了(我试过 "[answer | a
    • @groovy 你的第一次测试:是的,[a] 的长度是 1,所以 (1==4) 是 False。这就是列表推导中的测试要点:清除不符合项。我已经更新了答案,为函数使用了更好的名称。
    • @groovy 如果你把[ ans | ... , let ans=[a,b,c,d]]放在那里,那么ans的长度是4!你是这样写的。如果您通过其他方式创建它,那么 [ ans | ..., length ans==4] 将只包含长度为 4 的 ans (所有其他将被跳过)。这是 Haskell 中的基本列表理解。 :)
    • 哦,我可以放...,长度 ans===someValueComputedElsewhere...?
    • @groovy 是的,您可以在测试中使用当时定义的任何变量。
    【解决方案4】:

    你有Data.List.subsequences

    你可以用一元形式写你的列表理解(见guards in Monad Comprehensions):

    (说明:monad 必须是支持失败的MonadPlus 的实例。

    guard False 使 monad 无法评估为 mzero。,后续结果将附加 mplus = (++) 用于 List monad。)

    import Control.Monad (guard)
    
    myDomain = [1..9]   -- or whatever
    
    validCombinations :: [a] -> [[a]]
    validCombinations domainList = do
            combi <- List.subsequences domainList
            case combi of
                    [a,b] -> do
                            guard (propertyA a && propertyB b)
                            return combi
    
                    [a,b,c] -> do
                            guard (propertyA a && propertyB b && propertyC c)
                            return combi
    
                    _ -> guard False
    
    main = do
             forM_ (validCombinations myDomain) print
    

    再次更新,递归获取元素,保存组合和检查

    import Control.Monad
    
    validCombinations :: Eq a => Int -> Int -> [a] -> [(a -> Bool)] -> [a] -> [[a]]
    validCombinations indx size domainList propList accum = do
    
        elt <- domainList   -- try all domain elements
    
        let prop = propList!!indx
        guard $ prop elt               -- some property
        guard $ elt `notElem` accum    -- not repeated 
    
        {-
        case accum of
            prevElt : _ -> guard $ some_combined_check_with_previous elt prevElt
            _ -> guard True
            -}
    
        if size > 1 then do
             -- append recursively subsequent positions
    
             other <- validCombinations (indx+1) (size-1) domainList propList (elt : accum)
    
             return $ elt : other
        else
             return [elt]
    
    myDomain = [1..3] :: [Int]
    
    myProps = repeat (>1)
    
    main = do
               forM_ (validCombinations 0 size myDomain myProps []) print 
       where
          size = 2 
    

    大小为 2 的结果具有非平凡的结果:

    [2,3] [3,2]

    【讨论】:

    • 我相信尽可能早地进行测试以剔除选择会更有效。您的代码将完整选择三元组,然后对其进行完整测试。这通常比第一次选择对,测试,然后选择第三个 elt 并测试更多(根据我的 Prolog 解决难题的经验)花费更多的时间。 :)
    • @Gabriel ...太棒了!但是,如果我希望 [a,b...] 的大小由程序中其他地方计算的值来确定,那么大小可以是 2 到 100 之间的任何值,例如,无需将每个值都写出来作为组合?
    • @Will ...哦,我想你可能误解了...我的求解器中的完整路径是由串在一起的列表组成的(每个列表包含两个单元格)。这是我正在努力解决的问题。迷宫路径会是 4,5..10,成对的单元格长吗? (我的条件允许它们重叠,因此路径可能会塌陷并以偶数或奇数长度结束)
    • 我知道(对于 Will Ness)有更好的解决方案,但我只想展示 Monad Comprehensions。
    • GabrielRiba 是的,谢谢。 @groovy 那么您需要使用递归求解器查看第一个答案。列表理解是您在代码中手工编写的内容,它具有严格的结构。使用递归解决方案研究该答案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-08
    • 1970-01-01
    • 2018-06-07
    相关资源
    最近更新 更多