列出来自 IO 的第一个值
List with first value from IO
我创建了一个无限列表,它的第一个元素需要一些时间才能生成:
slowOne = do
threadDelay (10 ^ 6)
return 1
infiniteInts :: [IO Integer]
infiniteInts = loop slowOne
where
loop :: IO Integer -> [IO Integer]
loop ioInt = ioInt : loop (fmap (+1) ioInt)
当我打印列表时,我可以观察到不仅在第一个元素而且在 所有 个元素发生的延迟:
main =
mapM_
(\ioInt -> do
i <- ioInt
print i
)
infiniteInts
我正在努力提高我对 IO 的直觉:为什么每个元素都有延迟,而不仅仅是 slowOne
生成的第一个元素?
警告:
关于 运行ning 表达式,此答案不正确。请先参考 以更好地理解此 IO 值列表的工作原理。
我们可以通过
来理解这种行为
- 重写递归函数
- 记住 GHC doesn't cache results from IO
这里重写了 infiniteInts
以获得不同数量的元素:
获取一个元素
slowOne : loop (fmap (+1) slowOne)
由于 Haskell 的 :
运算符是非严格的,我们 运行 slowOne
一次 → 这需要一秒钟。
获取两个元素
slowOne : (fmap (+1) slowOne) : loop (fmap (+1) (fmap (+1) slowOne))
取两个元素(直到第二个 :
)导致 slowOne
被调用两次 → 这需要两秒钟。
获取三个元素
slowOne : (fmap (+1) slowOne) : (fmap (+1) (fmap (+1) slowOne)) : loop (fmap (+1) (fmap (+1) (fmap (+1) slowOne)))
取三个元素(直到第三个 :
)导致 slowOne
被调用三次 → 这需要三秒钟。
总结
从重写中我们可以看到 slowOne
为每个元素调用(例如,三个元素调用三次)并且假设 GHC 不会缓存在 IO 内部创建的结果(slowOne
) 因此每个元素都需要一秒钟的时间来创建。
每个号码都有延迟,因为它们都来自 slowOne
。考虑你的 loop
:
loop ioInt = ioInt : loop (fmap (+1) ioInt)
^----This is slowOne ^
└----This is also slowOne
我对你的 fmap
正在做什么的直觉只是作用于 IO 值(IO Integer
中的 Integer
),但保留其所有上下文( "IO" 部分)完好无损。
正如 Will Ness 评论的那样:
fmap (1+)
takes an IO value (a pure value of type IO t for some t) which describes I/O action that will return a pure value x :: t
when that action will run; and creates a new pure IO value describing an augmented I/O action that will return the pure value x+1
after performing the I/O actions as described by the first IO value.
例如,我们可以用另一个函数 timedOne
:
替换 slowOne
timedOne = do
time <- getPOSIXTime
putStrLn $ "time: " ++ show time
return 1
用 timedOne
而不是 slowOne
调用 loop
会打印出来,显示 fmap
如何在不影响上下文的情况下影响值:
time: 1583715559.051068s
1
time: 1583715559.051705s
2
time: 1583715559.052311s
3
... and so on
你看每个被使用的号码,仍然携带着自己的IO "baggage",只是这次行李是"get the time from the system clock and print it out"。如果要更改此行为以便仅延迟第一个数字,则需要清除 loop
构建的列表的尾部任何线程延迟 IO 包袱。一种方法是使用纯列表并将每个元素包装在 IO:
loop ioInt = ioInt : (return <$> [2..])
我不确定你的直觉(正如你在自己写的 to this question) is all that accurate. Let me try to give you some better intuition. You may also find 中描述的那样很有帮助,尽管那里的问题完全不同。
在 Haskell 中,类型 IO a
的值(对于任何类型 a
,因此 IO Int
或 IO String
或其他)有时是称为 "IO action",但正如@WillNess 在评论中提到的,最好将其视为 "IO recipe"。对于这些配方,我们认为 "evaluation" 和 "execution" 是完全独立的操作。 评估类型的表达式IO a
就像写下食谱。 评估类型IO Int
的表达式的结果是类型IO Int
的值。生成此值不会执行任何 I/O 并且无需花费任何时间,即使基础 I/O 涉及延迟或其他缓慢的操作。这个评估的 IO a
值可以传递、存储、复制、修改、与其他 IO
配方结合,或者完全忽略,所有这些都不需要执行任何实际的 I/O.
相比之下,执行生成的配方是实际执行I/O操作的过程。 执行一个IO Int
的结果是一个Int
。如果涉及 20 分钟的延迟、文件访问、and/or 信鸽申请以获得 Int
,操作将需要一段时间。如果您执行 相同的食谱两次,第二次它不会更快。
我们在 Haskell 中编写的几乎所有代码都会评估 IO 配方而不执行它们。
当代码:
slowOne = do
threadDelay (10 ^ 6)
return 1
是运行,它实际上只是评估(写下)一个IO配方。评估这个配方显然涉及评估一个 do-block。这不会 做 I/O;它只是评估(写下)do-block 中的每个配方,并将它们组合成一个更大的书面配方。
具体来说,评估 slowOne
涉及:
正在评估配方 threadDelay (10 ^ 6)
。这涉及计算算术表达式 10 ^ 6
并对其调用函数 threadDelay
。该函数实现(对于非线程运行时间)为:
threadDelay :: Int -> IO ()
threadDelay time = IO $ \s -> some_function_of_s
也就是说,它将一个函数包装在一个 IO
构造函数中以产生一个 IO ()
类型的值。至关重要的是,它实际上并没有延迟线程。它只是创建一个(包装的)功能值。顺便说一句,IO
构造函数并没有什么神奇之处。这个 threadDelay
函数类似于编写同样非魔法的:
justAFunction :: Int -> Maybe (Int -> Int)
justAFunction c = Just (\x -> c*x)
正在评估配方 return 1
。这也只是创建一个包装在 IO
构造函数中的值。具体来说,它是(包装且完全非魔法的)功能值,看起来像:
IO (\s -> (s, 1))
将这两个评估的配方按顺序组合成一个更长的配方。这个新的组合配方是类型 IO Int
的值,将分配给 slowOne
.
类似地,当对下面的代码求值时:
infiniteInts :: [IO Integer]
infiniteInts = loop slowOne
where
loop :: IO Integer -> [IO Integer]
loop ioInt = ioInt : loop (fmap (+1) ioInt)
您没有执行任何 IO。您只是在评估 IO 配方和包含 IO 配方的数据结构。具体来说,您正在将此表达式计算为 [IO Integer]
类型的值,该值由 IO Integer
values/recipes 的无限列表组成。列表中的第一个食谱是 slowOne
。列表中的第二个食谱是:
fmap (+1) slowOne
这需要一个词的解释。评估此表达式时,它会构造一个新配方,该配方可以使用等效的 do 块编写:
fmap_plus_one_of_slowOne = do
x <- slowOne
return (x + 1)
鉴于 slowOne
的定义方式,这实际上等同于我们通过评估得到的独立配方:
fmap_plus_one_of_slowOne = do
threadDelay (10 ^ 6)
return 2
同样,列表中的第三个食谱:
fmap (+1) (fmap (+1) slowOne)
相当于配方的计算结果:
fmap_plus_one_of_fmap_plus_one_of_slowOne = do
threadDelay (10 ^ 6)
return 3
现在,您程序的最后一部分是:
mapM_
(\ioInt -> do
i <- ioInt
print i
)
infiniteInts
您可能会惊讶地听到,在评估此代码时,我们仍然只是评估而不是正在执行 食谱。评估此 mapM_
函数时,它会构建一个新配方。它构建的配方可以用文字描述为:
"Take each recipe in the list infiniteInts
. Sorry about the bad choice of name -- this isn't a list of integers, but a list of IO recipes for making integers. It's a good thing you're a computer and won't get confused by this, huh? Anyway, take each of those recipes in sequence and pass them to this function I have here to generate a new recipe. Then, run that list of recipes in order. You're writing this down, right? Stop, don't execute anything yet! Just write it down!"
那么,让我们回顾一下:
slowOne
是食谱
do threadDelay (10 ^ 6)
return 1
fmap (+1) slowOne
与方子相同:
do threadDelay (10 ^ 6)
return 2
同样,fmap (+1) (fmap (+1) slowOne)
真的只是食谱
do threadDelay (10 ^ 6)
return 3
等等
因此,infiniteInts
是食谱列表:
infiniteInts =
[ do { threadDelay (10 ^ 6); return 1 }
, do { threadDelay (10 ^ 6); return 2 }
, do { threadDelay (10 ^ 6); return 3 }
, ... ]
鉴于 mapM_ ...
秘诀的含义,如果 Haskell 允许无限长的程序,我们可以从头开始编写整个秘诀,如下所示:
do -- first recipe
threadDelay (10 ^ 6)
i <- return 1
print i
-- second recipe
threadDelay (10 ^ 6)
i <- return 2
print i
-- third recipe
threadDelay (10 ^ 6)
i <- return 3
print i
-- etc.
这是计算 mapM_ ...
表达式的结果。
然后,最后,我们到达 程序的唯一部分 执行 IO 配方,而不是简单地评估它。那部分是:
main = ...
当您将食谱命名为 main
时,您告诉 Haskell 在程序为 运行 时执行它。从分配给 main
的配方的评估值可以看出,它是一个包含交错 threadDelay
和 print
配方的组合配方,因此当它被执行时,它会打印一个递增的在每个整数之前都有延迟的整数列表。
关于懒惰与严格的说明...懒惰在上述过程中没有任何作用(好吧,除了允许我们在不锁定机器的情况下构造无限列表)。当我在上面说 "evaluate" 时,无论评估是否严格并立即发生,或者评估在技术上延迟到需要时,都没有区别。需要它的时间点可能是在它被执行的时候,但是评估(编写配方)和执行(遵循配方)仍然是不同的过程,即使它们发生在一个正确的位置一个接着一个。
我创建了一个无限列表,它的第一个元素需要一些时间才能生成:
slowOne = do
threadDelay (10 ^ 6)
return 1
infiniteInts :: [IO Integer]
infiniteInts = loop slowOne
where
loop :: IO Integer -> [IO Integer]
loop ioInt = ioInt : loop (fmap (+1) ioInt)
当我打印列表时,我可以观察到不仅在第一个元素而且在 所有 个元素发生的延迟:
main =
mapM_
(\ioInt -> do
i <- ioInt
print i
)
infiniteInts
我正在努力提高我对 IO 的直觉:为什么每个元素都有延迟,而不仅仅是 slowOne
生成的第一个元素?
警告:
关于 运行ning 表达式,此答案不正确。请先参考
我们可以通过
来理解这种行为- 重写递归函数
- 记住 GHC doesn't cache results from IO
这里重写了 infiniteInts
以获得不同数量的元素:
获取一个元素
slowOne : loop (fmap (+1) slowOne)
由于 Haskell 的 :
运算符是非严格的,我们 运行 slowOne
一次 → 这需要一秒钟。
获取两个元素
slowOne : (fmap (+1) slowOne) : loop (fmap (+1) (fmap (+1) slowOne))
取两个元素(直到第二个 :
)导致 slowOne
被调用两次 → 这需要两秒钟。
获取三个元素
slowOne : (fmap (+1) slowOne) : (fmap (+1) (fmap (+1) slowOne)) : loop (fmap (+1) (fmap (+1) (fmap (+1) slowOne)))
取三个元素(直到第三个 :
)导致 slowOne
被调用三次 → 这需要三秒钟。
总结
从重写中我们可以看到 slowOne
为每个元素调用(例如,三个元素调用三次)并且假设 GHC 不会缓存在 IO 内部创建的结果(slowOne
) 因此每个元素都需要一秒钟的时间来创建。
每个号码都有延迟,因为它们都来自 slowOne
。考虑你的 loop
:
loop ioInt = ioInt : loop (fmap (+1) ioInt)
^----This is slowOne ^
└----This is also slowOne
我对你的 fmap
正在做什么的直觉只是作用于 IO 值(IO Integer
中的 Integer
),但保留其所有上下文( "IO" 部分)完好无损。
正如 Will Ness 评论的那样:
fmap (1+)
takes an IO value (a pure value of type IO t for some t) which describes I/O action that will return a pure valuex :: t
when that action will run; and creates a new pure IO value describing an augmented I/O action that will return the pure valuex+1
after performing the I/O actions as described by the first IO value.
例如,我们可以用另一个函数 timedOne
:
slowOne
timedOne = do
time <- getPOSIXTime
putStrLn $ "time: " ++ show time
return 1
用 timedOne
而不是 slowOne
调用 loop
会打印出来,显示 fmap
如何在不影响上下文的情况下影响值:
time: 1583715559.051068s
1
time: 1583715559.051705s
2
time: 1583715559.052311s
3
... and so on
你看每个被使用的号码,仍然携带着自己的IO "baggage",只是这次行李是"get the time from the system clock and print it out"。如果要更改此行为以便仅延迟第一个数字,则需要清除 loop
构建的列表的尾部任何线程延迟 IO 包袱。一种方法是使用纯列表并将每个元素包装在 IO:
loop ioInt = ioInt : (return <$> [2..])
我不确定你的直觉(正如你在自己写的
在 Haskell 中,类型 IO a
的值(对于任何类型 a
,因此 IO Int
或 IO String
或其他)有时是称为 "IO action",但正如@WillNess 在评论中提到的,最好将其视为 "IO recipe"。对于这些配方,我们认为 "evaluation" 和 "execution" 是完全独立的操作。 评估类型的表达式IO a
就像写下食谱。 评估类型IO Int
的表达式的结果是类型IO Int
的值。生成此值不会执行任何 I/O 并且无需花费任何时间,即使基础 I/O 涉及延迟或其他缓慢的操作。这个评估的 IO a
值可以传递、存储、复制、修改、与其他 IO
配方结合,或者完全忽略,所有这些都不需要执行任何实际的 I/O.
相比之下,执行生成的配方是实际执行I/O操作的过程。 执行一个IO Int
的结果是一个Int
。如果涉及 20 分钟的延迟、文件访问、and/or 信鸽申请以获得 Int
,操作将需要一段时间。如果您执行 相同的食谱两次,第二次它不会更快。
我们在 Haskell 中编写的几乎所有代码都会评估 IO 配方而不执行它们。
当代码:
slowOne = do
threadDelay (10 ^ 6)
return 1
是运行,它实际上只是评估(写下)一个IO配方。评估这个配方显然涉及评估一个 do-block。这不会 做 I/O;它只是评估(写下)do-block 中的每个配方,并将它们组合成一个更大的书面配方。
具体来说,评估 slowOne
涉及:
正在评估配方
threadDelay (10 ^ 6)
。这涉及计算算术表达式10 ^ 6
并对其调用函数threadDelay
。该函数实现(对于非线程运行时间)为:threadDelay :: Int -> IO () threadDelay time = IO $ \s -> some_function_of_s
也就是说,它将一个函数包装在一个
IO
构造函数中以产生一个IO ()
类型的值。至关重要的是,它实际上并没有延迟线程。它只是创建一个(包装的)功能值。顺便说一句,IO
构造函数并没有什么神奇之处。这个threadDelay
函数类似于编写同样非魔法的:justAFunction :: Int -> Maybe (Int -> Int) justAFunction c = Just (\x -> c*x)
正在评估配方
return 1
。这也只是创建一个包装在IO
构造函数中的值。具体来说,它是(包装且完全非魔法的)功能值,看起来像:IO (\s -> (s, 1))
将这两个评估的配方按顺序组合成一个更长的配方。这个新的组合配方是类型
IO Int
的值,将分配给slowOne
.
类似地,当对下面的代码求值时:
infiniteInts :: [IO Integer]
infiniteInts = loop slowOne
where
loop :: IO Integer -> [IO Integer]
loop ioInt = ioInt : loop (fmap (+1) ioInt)
您没有执行任何 IO。您只是在评估 IO 配方和包含 IO 配方的数据结构。具体来说,您正在将此表达式计算为 [IO Integer]
类型的值,该值由 IO Integer
values/recipes 的无限列表组成。列表中的第一个食谱是 slowOne
。列表中的第二个食谱是:
fmap (+1) slowOne
这需要一个词的解释。评估此表达式时,它会构造一个新配方,该配方可以使用等效的 do 块编写:
fmap_plus_one_of_slowOne = do
x <- slowOne
return (x + 1)
鉴于 slowOne
的定义方式,这实际上等同于我们通过评估得到的独立配方:
fmap_plus_one_of_slowOne = do
threadDelay (10 ^ 6)
return 2
同样,列表中的第三个食谱:
fmap (+1) (fmap (+1) slowOne)
相当于配方的计算结果:
fmap_plus_one_of_fmap_plus_one_of_slowOne = do
threadDelay (10 ^ 6)
return 3
现在,您程序的最后一部分是:
mapM_
(\ioInt -> do
i <- ioInt
print i
)
infiniteInts
您可能会惊讶地听到,在评估此代码时,我们仍然只是评估而不是正在执行 食谱。评估此 mapM_
函数时,它会构建一个新配方。它构建的配方可以用文字描述为:
"Take each recipe in the list
infiniteInts
. Sorry about the bad choice of name -- this isn't a list of integers, but a list of IO recipes for making integers. It's a good thing you're a computer and won't get confused by this, huh? Anyway, take each of those recipes in sequence and pass them to this function I have here to generate a new recipe. Then, run that list of recipes in order. You're writing this down, right? Stop, don't execute anything yet! Just write it down!"
那么,让我们回顾一下:
slowOne
是食谱do threadDelay (10 ^ 6) return 1
fmap (+1) slowOne
与方子相同:do threadDelay (10 ^ 6) return 2
同样,
fmap (+1) (fmap (+1) slowOne)
真的只是食谱do threadDelay (10 ^ 6) return 3
等等
因此,
infiniteInts
是食谱列表:infiniteInts = [ do { threadDelay (10 ^ 6); return 1 } , do { threadDelay (10 ^ 6); return 2 } , do { threadDelay (10 ^ 6); return 3 } , ... ]
鉴于 mapM_ ...
秘诀的含义,如果 Haskell 允许无限长的程序,我们可以从头开始编写整个秘诀,如下所示:
do -- first recipe
threadDelay (10 ^ 6)
i <- return 1
print i
-- second recipe
threadDelay (10 ^ 6)
i <- return 2
print i
-- third recipe
threadDelay (10 ^ 6)
i <- return 3
print i
-- etc.
这是计算 mapM_ ...
表达式的结果。
然后,最后,我们到达 程序的唯一部分 执行 IO 配方,而不是简单地评估它。那部分是:
main = ...
当您将食谱命名为 main
时,您告诉 Haskell 在程序为 运行 时执行它。从分配给 main
的配方的评估值可以看出,它是一个包含交错 threadDelay
和 print
配方的组合配方,因此当它被执行时,它会打印一个递增的在每个整数之前都有延迟的整数列表。
关于懒惰与严格的说明...懒惰在上述过程中没有任何作用(好吧,除了允许我们在不锁定机器的情况下构造无限列表)。当我在上面说 "evaluate" 时,无论评估是否严格并立即发生,或者评估在技术上延迟到需要时,都没有区别。需要它的时间点可能是在它被执行的时候,但是评估(编写配方)和执行(遵循配方)仍然是不同的过程,即使它们发生在一个正确的位置一个接着一个。