为什么我必须调用 `sum` 两次才能对 `Maybe Integer` 的列表求和?
Why do I have to call `sum` twice to sum a list of `Maybe Integer`s?
我写了一个非常简单的练习题的解决方案:
Calculate the number of grains of wheat on a chessboard given that the number on each square doubles. Write code that shows how many grains are on a given square, and the total number of grains on the chessboard.
如果输入小于 1 或大于 64,函数必须 return 一个 Maybe Integer
和 return Nothing
。
square :: Integer -> Maybe Integer
square n = if (n < 1 || n > 64) then Nothing else Just (2^(pred n))
total :: Integer
total = sum (fmap sum (map square [1..64]))
我尝试将 fmap sum
应用于 GHCI 中 map square
(Maybe Integer
的列表)的一些测试输出,并惊讶地发现它是 return 的列表整数(sans Just
)而不是它们的总和。所以在上面的解决方案中,我第二次应用 sum
来实际获得总和。
我想从概念上理解为什么会这样:换句话说,为什么 sum
在这种情况下表现得像一个 Maybe Ints 到 Ints 的转换器,而不是添加东西?
我已经解决了一些依赖辅助函数的类似练习,以避免对 Maybe
值进行计算的复杂性,也许在这种情况下我应该只计算 total
的值而不使用 square
,即:
total = sum [2^n | n <- [0..63]]
然而,由于我已经编写了一个有用的函数,所以我的第一直觉是重用它,这导致了一些无法预料的行为。
我们来看一个例子。
map square [0..2] = [Nothing, Just 1, Just 2]
fmap sum (map square [0..2]) = [sum Nothing, sum (Just 1), sum (Just 2)]
由于Maybe
是一个Foldable
容器,计算其元素的总和是有意义的:
sum Nothing = 0
sum (Just a) = a
所以
fmap sum (map square [0..2]) = [0, 1, 2]
现在我不知道你实际上希望用 Maybe
做什么,但这就是你得到你得到的东西的原因。
我们来看看sum
的类型:
sum :: (Foldable t, Num a) => t a -> a
通常,对于初学者来说,这可以通过假设 t ~ []
来简化,因此我们使用 sum
作为
sum :: Num a => [a] -> a
如果我们尝试在您的示例中使用此类型的 sum
,我们将收到类型错误,因为您有一个 Maybe 数字列表,而不是数字列表。相反,您编写 fmap sum [Just 1]
,将 sum
和 fmap
专门化为:
sum :: Maybe Integer -> Integer
fmap :: (Maybe Integer -> Integer) -> [Maybe Integer] -> [Integer]
所以问题实际上不是“为什么 sum
不添加东西”,而是“当给定一个 Maybe Integer 时,sum
怎么能有一个有意义的定义?”。 =31=]
如果您不熟悉如何将 sum
解释为 Foldable
或 Maybe
如何折叠,则回答该问题的一种方法是尝试实施它自己。实际上只有一种合理的实现方式:
sum :: Maybe Integer -> Integer
sum Nothing = 0
sum (Just x) = x
对吧?有人问你“这里的数字总数是多少”,然后给了你 0 个或 1 个数字。很容易加起来。这正是 sum
为 Maybe 工作的方式,除了它通过 Foldable 而不是专用于 Maybe。
在此之后,当然很简单:您已经将 [Maybe Integer]
变成了 [Integer]
,当然 sum
ming 得到了非 Nothing
个条目。
值得内化的一件事;当您将函数映射到列表1 时,结果是总是 将成为具有相同数量元素的列表。该函数将分别应用于列表的每个元素;它不能将它们组合成一个汇总值。这不是 fmap
所做的。
因此,考虑到该原则,我们知道 fmap sum squares
(其中 squares = map square [1..64]
)不可能得出 squares
的总和。它将是 [ sum (square 1), sum (square 2), ... , sum (square 64) ]
。如果我们真的想把它们加起来,我们将需要再次对整个列表应用 sum
。
这只是解释了 sum (square 1)
等实际上在做什么(即 sum
在应用于 Maybe
值时做了什么)。 sum
的正确类型是 sum :: (Foldable t, Num a) => t a -> a
。 Foldable
基本上是结构的类型-class,您可以按顺序扫描 0 个或多个元素(本质上:那些可以转换为列表的元素)。 sum
所做的只是将存在的元素相加,无论元素有多少(如果没有元素,则使用 0
作为“起始值”)。 Maybe
有一个 Foldable
实例;它总是有 0 或 1 个元素,并且 sum
可以将这些元素相加,就像它可以将恰好只有 0 或 1 个元素的列表相加一样。
当然,效果 对零或一个数字求和只是如果输入为零,结果为 0,如果输入为 1,则结果等于输入数字。 +
这个“总和”实际上从来没有被调用过,这让它感觉有点毫无意义。但是 sum
不知道;它适用于任何 Foldable
,无论它们包含多少元素。而Maybe
并不知道它最终会被用来“求和而不实际添加”;它只是实现了 Foldable
以便任何其他想要从结构中扫描可变数量元素的函数都可以扫描 Maybe
s.
如果您认为这很愚蠢,请不要将 sum
用于该工作;请改用 fromMaybe 0
。
1 您可以将其推广到列表之外的其他函子;当您 fmap
一个数据结构上的函数时,结果将具有与输入完全相同的结构。只有“叶节点”会有所不同。
首先,
中的fmap
total = sum (fmap sum (map square [1..64]))
只是 map
:
total = sum ( map sum (map square [1..64]))
和map
通过组合它们的映射函数来组合,
total = sum ( map (sum . square) [1..64] )
并且 map
将其函数应用于列表的每个元素,
total = sum [sum (square x) | x <- [1..64] ]
虽然组合函数分步查找结果,
total = sum [sum y | x <- [1..64], y <- [square x] ]
而内部 sum
正在处理 square
的结果,即 Maybe
数字,
total = sum [ n | x <- [1..64], Just n <- [square x] ]
= sum [ n | x <- [1..64], Just n <- list (square x) ]
where list y = [y]
并将 sum y
变成 n
是可行的,因为 sum
找到了一个和,而 0
是它的标识,所以添加一个 0
是相同的因为根本不添加任何东西。意思是,Just n
模式匹配 失败 当 square
产生 Nothing
(在我们的例子中是 never,但没关系)所以 that x
导致它被 跳过 .
total = sum [ n | x <- [1..64], n <- maybeToList (square x) ]
= sum [ n | x <- [1..64], n <- case (square x) of
Just n -> [n]
Nothing -> [] ]
但是,我们知道这永远不会发生,所有这些相同号码的重新包装都是徒劳的,所以最后你的代码相当于
total = sum [ n | x <- [1..64], Just n <- [Just (2^(x-1))] ]
= sum [ n | x <- [1..64], n <- [ 2^(x-1) ] ]
= sum [ 2^(x-1) | x <- [1..64] ]
如您所愿。
眼见为实(好吧,记住/想象,这在使用高阶函数时是必需的……当然除非一个人有清晰的记忆,所以对他们来说相信是 看到)。它甚至可能为我们提供进一步简化代码、进行一些转换,甚至最终可能进行一些计算优化的新想法:
total = sum [ 2^x | x <- [0..63] ]
= sum [ product (replicate x 2) | x <- [0..63] ]
= sum $ [ product (replicate 0 2) ]
++ [ product (replicate x 2) | x <- [1..63] ]
= sum $ [ product [] ]
++ [ 2 * product (replicate (x-1) 2) | x <- [1..63] ]
= sum $ [ 1 ]
++ [ 2 * product (replicate x 2) | x <- [0..62] ]
= sum [1] + sum ( [2*n0] ++
[ 2 * product (replicate x 2) | x <- [1..62] ] )
where { n0=1 }
= n0 + sum [n1] + sum( [2*n1] ++
[ 2 * product (replicate x 2) | x <- [2..62] ] )
where { n0=1 ; n1=2*n0 }
= n0 + n1 + n2 + sum( [2*n2] ++
[ 2 * product (replicate x 2) | x <- [3..62] ] )
where { n0=1 ; n1=2*n0 ; n2=2*n1 }
= ....
= sum . take 64 $ iterate (2*) 1
它们又出现了,棋盘的 64 个方格,以及第一粒米粒,它翻了一番,又翻了一番,就像我们之前沿着这些方格走的那样。
我写了一个非常简单的练习题的解决方案:
Calculate the number of grains of wheat on a chessboard given that the number on each square doubles. Write code that shows how many grains are on a given square, and the total number of grains on the chessboard.
如果输入小于 1 或大于 64,函数必须 return 一个 Maybe Integer
和 return Nothing
。
square :: Integer -> Maybe Integer
square n = if (n < 1 || n > 64) then Nothing else Just (2^(pred n))
total :: Integer
total = sum (fmap sum (map square [1..64]))
我尝试将 fmap sum
应用于 GHCI 中 map square
(Maybe Integer
的列表)的一些测试输出,并惊讶地发现它是 return 的列表整数(sans Just
)而不是它们的总和。所以在上面的解决方案中,我第二次应用 sum
来实际获得总和。
我想从概念上理解为什么会这样:换句话说,为什么 sum
在这种情况下表现得像一个 Maybe Ints 到 Ints 的转换器,而不是添加东西?
我已经解决了一些依赖辅助函数的类似练习,以避免对 Maybe
值进行计算的复杂性,也许在这种情况下我应该只计算 total
的值而不使用 square
,即:
total = sum [2^n | n <- [0..63]]
然而,由于我已经编写了一个有用的函数,所以我的第一直觉是重用它,这导致了一些无法预料的行为。
我们来看一个例子。
map square [0..2] = [Nothing, Just 1, Just 2]
fmap sum (map square [0..2]) = [sum Nothing, sum (Just 1), sum (Just 2)]
由于Maybe
是一个Foldable
容器,计算其元素的总和是有意义的:
sum Nothing = 0
sum (Just a) = a
所以
fmap sum (map square [0..2]) = [0, 1, 2]
现在我不知道你实际上希望用 Maybe
做什么,但这就是你得到你得到的东西的原因。
我们来看看sum
的类型:
sum :: (Foldable t, Num a) => t a -> a
通常,对于初学者来说,这可以通过假设 t ~ []
来简化,因此我们使用 sum
作为
sum :: Num a => [a] -> a
如果我们尝试在您的示例中使用此类型的 sum
,我们将收到类型错误,因为您有一个 Maybe 数字列表,而不是数字列表。相反,您编写 fmap sum [Just 1]
,将 sum
和 fmap
专门化为:
sum :: Maybe Integer -> Integer
fmap :: (Maybe Integer -> Integer) -> [Maybe Integer] -> [Integer]
所以问题实际上不是“为什么 sum
不添加东西”,而是“当给定一个 Maybe Integer 时,sum
怎么能有一个有意义的定义?”。 =31=]
如果您不熟悉如何将 sum
解释为 Foldable
或 Maybe
如何折叠,则回答该问题的一种方法是尝试实施它自己。实际上只有一种合理的实现方式:
sum :: Maybe Integer -> Integer
sum Nothing = 0
sum (Just x) = x
对吧?有人问你“这里的数字总数是多少”,然后给了你 0 个或 1 个数字。很容易加起来。这正是 sum
为 Maybe 工作的方式,除了它通过 Foldable 而不是专用于 Maybe。
在此之后,当然很简单:您已经将 [Maybe Integer]
变成了 [Integer]
,当然 sum
ming 得到了非 Nothing
个条目。
值得内化的一件事;当您将函数映射到列表1 时,结果是总是 将成为具有相同数量元素的列表。该函数将分别应用于列表的每个元素;它不能将它们组合成一个汇总值。这不是 fmap
所做的。
因此,考虑到该原则,我们知道 fmap sum squares
(其中 squares = map square [1..64]
)不可能得出 squares
的总和。它将是 [ sum (square 1), sum (square 2), ... , sum (square 64) ]
。如果我们真的想把它们加起来,我们将需要再次对整个列表应用 sum
。
这只是解释了 sum (square 1)
等实际上在做什么(即 sum
在应用于 Maybe
值时做了什么)。 sum
的正确类型是 sum :: (Foldable t, Num a) => t a -> a
。 Foldable
基本上是结构的类型-class,您可以按顺序扫描 0 个或多个元素(本质上:那些可以转换为列表的元素)。 sum
所做的只是将存在的元素相加,无论元素有多少(如果没有元素,则使用 0
作为“起始值”)。 Maybe
有一个 Foldable
实例;它总是有 0 或 1 个元素,并且 sum
可以将这些元素相加,就像它可以将恰好只有 0 或 1 个元素的列表相加一样。
当然,效果 对零或一个数字求和只是如果输入为零,结果为 0,如果输入为 1,则结果等于输入数字。 +
这个“总和”实际上从来没有被调用过,这让它感觉有点毫无意义。但是 sum
不知道;它适用于任何 Foldable
,无论它们包含多少元素。而Maybe
并不知道它最终会被用来“求和而不实际添加”;它只是实现了 Foldable
以便任何其他想要从结构中扫描可变数量元素的函数都可以扫描 Maybe
s.
如果您认为这很愚蠢,请不要将 sum
用于该工作;请改用 fromMaybe 0
。
1 您可以将其推广到列表之外的其他函子;当您 fmap
一个数据结构上的函数时,结果将具有与输入完全相同的结构。只有“叶节点”会有所不同。
首先,
中的fmap
total = sum (fmap sum (map square [1..64]))
只是 map
:
total = sum ( map sum (map square [1..64]))
和map
通过组合它们的映射函数来组合,
total = sum ( map (sum . square) [1..64] )
并且 map
将其函数应用于列表的每个元素,
total = sum [sum (square x) | x <- [1..64] ]
虽然组合函数分步查找结果,
total = sum [sum y | x <- [1..64], y <- [square x] ]
而内部 sum
正在处理 square
的结果,即 Maybe
数字,
total = sum [ n | x <- [1..64], Just n <- [square x] ]
= sum [ n | x <- [1..64], Just n <- list (square x) ]
where list y = [y]
并将 sum y
变成 n
是可行的,因为 sum
找到了一个和,而 0
是它的标识,所以添加一个 0
是相同的因为根本不添加任何东西。意思是,Just n
模式匹配 失败 当 square
产生 Nothing
(在我们的例子中是 never,但没关系)所以 that x
导致它被 跳过 .
total = sum [ n | x <- [1..64], n <- maybeToList (square x) ]
= sum [ n | x <- [1..64], n <- case (square x) of
Just n -> [n]
Nothing -> [] ]
但是,我们知道这永远不会发生,所有这些相同号码的重新包装都是徒劳的,所以最后你的代码相当于
total = sum [ n | x <- [1..64], Just n <- [Just (2^(x-1))] ]
= sum [ n | x <- [1..64], n <- [ 2^(x-1) ] ]
= sum [ 2^(x-1) | x <- [1..64] ]
如您所愿。
眼见为实(好吧,记住/想象,这在使用高阶函数时是必需的……当然除非一个人有清晰的记忆,所以对他们来说相信是 看到)。它甚至可能为我们提供进一步简化代码、进行一些转换,甚至最终可能进行一些计算优化的新想法:
total = sum [ 2^x | x <- [0..63] ]
= sum [ product (replicate x 2) | x <- [0..63] ]
= sum $ [ product (replicate 0 2) ]
++ [ product (replicate x 2) | x <- [1..63] ]
= sum $ [ product [] ]
++ [ 2 * product (replicate (x-1) 2) | x <- [1..63] ]
= sum $ [ 1 ]
++ [ 2 * product (replicate x 2) | x <- [0..62] ]
= sum [1] + sum ( [2*n0] ++
[ 2 * product (replicate x 2) | x <- [1..62] ] )
where { n0=1 }
= n0 + sum [n1] + sum( [2*n1] ++
[ 2 * product (replicate x 2) | x <- [2..62] ] )
where { n0=1 ; n1=2*n0 }
= n0 + n1 + n2 + sum( [2*n2] ++
[ 2 * product (replicate x 2) | x <- [3..62] ] )
where { n0=1 ; n1=2*n0 ; n2=2*n1 }
= ....
= sum . take 64 $ iterate (2*) 1
它们又出现了,棋盘的 64 个方格,以及第一粒米粒,它翻了一番,又翻了一番,就像我们之前沿着这些方格走的那样。