【问题标题】:Understanding Haskell seq了解 Haskell 序列
【发布时间】:2015-08-16 06:20:57
【问题描述】:

我是一个 Haskell 新手,在理解 seq 时遇到问题,我在下面的示例中对此进行了总结。

这是相同(愚蠢)功能的 2 个实现。该函数采用正整数 n 并返回元组 (n,n)。答案是通过使用带有元组累加器的辅助函数从 (0,0) 向上计数产生的。为了避免建立一个大的 thunk,我使用了 seq 来使累加器变得严格。

testSeq1 中,元组的内容是严格的,而在testSeq2 中,元组本身是严格的。我认为这两种实现会执行相同的操作。但实际上,testSeq1 的“使用总内存”仅为 1MB,而testSeq2 则为 187MB(使用 n = 1000000 进行测试时)。

testSeq2 有什么问题?

testSeq1 :: Int -> (Int,Int)
testSeq1 n = impl n (0,0) where
    impl 0 (a,b) = (a,b)
    impl i (a,b) = seq a $ seq b $ impl (i-1) (a+1, b+1)

testSeq2 :: Int -> (Int,Int)
testSeq2 n = impl n (0,0) where
    impl 0 acc = acc
    impl i acc = seq acc $ impl (i-1) ((fst acc)+1, (snd acc)+1)

【问题讨论】:

    标签: haskell


    【解决方案1】:

    seq一个元组只会强制对其进行评估,就像公开其元组构造函数一样,但不会评估其组件。

    也就是说,

    let pair = id (1+1, 2+2)
    in seq pair $ ...
    

    将应用id 并生成(_thunk1, _thunk2),其中_thunks 指向添加,此时未评估。

    在您的第二个示例中,您正在强制累加器 acc,但不是它的组件,因此仍然会累积一些大的 thunk。

    您可以使用所谓的评估策略,例如:

    evalPair :: (a,b) -> ()
    evalPair (a,b) = seq a $ seq b ()
    
    testSeq2 :: Int -> (Int,Int)
    testSeq2 n = impl n (0,0) where
    impl 0 acc = acc
    impl i acc = seq (evalPair acc) $ impl (i-1) ((fst acc)+1, (snd acc)+1)
    

    但是,testSeq1 是一种更简单的方法。

    作为另一种选择,使用strict tuples。这样的元组永远不会有组件的 thunk,而只是评估结果。如您所料,强制元组构造函数也会强制组件。

    import Data.Strict.Tuple
    
    testSeq2 :: Int -> Pair Int Int
    testSeq2 n = impl n (0 :!: 0) where
    impl 0 acc = acc
    impl i acc = seq acc $ impl (i-1) ((fst acc + 1) :!: (snd acc + 1))
    

    【讨论】:

      【解决方案2】:

      seq 仅强制对其第一个参数进行浅层评估。你可以看到这两个例子:

      errorTuple :: (Int, Int)
      errorTuple = undefined
      
      errorTupleContents :: (Int, Int)
      errorTupleContents = (undefined, undefined)
      
      case1 = errorTuple `seq` (1, 1)
      case2 = errorTupleContents `seq` (1, 1)
      

      case1 将失败并出现undefined 错误,因为seq 试图强制评估errorTuple,即undefined,但是,case2 不会,因为元组构造函数被评估并且返回一个其内容未计算的元组。如果他们被评估,他们会是undefined

      【讨论】:

      • 这是对元组构造函数的特殊处理,还是说对任何构造函数的参数都不会进行评估?假设我创建了自己的元组数据类型而不是使用内置元组,结果会一样吗?
      • 这不是元组特有的;发生这种情况是因为 Haskell 是懒惰的,并且只在必要时进行评估。您可以通过使用 Just undefined 或您自己的数据类型做一个类似的示例来测试这一点。
      • 这让我很震惊。我认为 seq 的全部意义在于能够覆盖 Haskell 的默认“惰性”并强制它完全评估表达式。这解释了为什么我尝试使用 foldl' 没有按预期工作 - 我在累加器函数中使用元组构造函数。
      • @BillyBadBoy 这就是 seq 的重点,但惰性/严格不是一个全有或全无的命题;数据结构可以有很多字段并且嵌套很深,如果你在实现一个函数时调用一个完全惰性的函数,那么急切地评估部分而不是全部这样的结构可能很有意义。 seq 基本上是增加严格性的“最小单位”。还有一个 deepseq 可以对事物进行全面评估,但这是有代价的(遍历整个结构以检查 thunk)。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-06-01
      • 2016-01-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-02-02
      相关资源
      最近更新 更多