【问题标题】:Are there ways to call two functions (one just after another) in purely functional language? (in non-io mode)有没有办法用纯函数式语言调用两个函数(一个接一个)? (在非 io 模式下)
【发布时间】:2014-04-09 13:52:24
【问题描述】:

我试图用纯函数式语言理解执行顺序。

我知道在纯函数式语言中,没有必要的执行顺序。

所以我的问题是:

假设有两个函数。 我想知道我可以通过哪些方式一个接一个地调用一个函数(从另一个函数嵌套调用一个函数除外)(以及 io 模式除外)。

我想查看 Haskell 或伪代码中的示例。

【问题讨论】:

  • 你根本不能一个接一个地调用函数。也许您正在考虑其他语言错误地调用函数的过程!
  • 你的意思是调用一个函数,忽略它的返回值,然后调用另一个函数?为什么要在非 IO 环境中这样做?由于函数没有副作用,调用一个函数然后忽略它的结果并不能真正完成很多事情。
  • 致 David Young:我只是想了解如何在纯函数式语言的世界中生活。
  • @uintptr_t 你能否举一个你想看看它是如何用纯函数式语言表达的场景的例子?
  • “在纯函数式语言的世界里,”你永远​​不需要关心函数的执行顺序。

标签: haskell lambda functional-programming


【解决方案1】:

如果函数是完全独立的并且在调用另一个函数时不使用其中一个函数的结果,则无法按照您的描述进行操作。

这是因为没有理由这样做。在无副作用的设置中,调用一个函数然后忽略其结果与在调用该函数所需的时间内什么都不做(留出内存使用量)完全相同。

可能seq x y 将评估x,然后评估y,然后将y 作为结果,但不能保证此评估顺序。 p>

现在,如果我们确实有副作用,例如如果我们在 Monad 或 Applicative 中工作,这可能很有用,但我们并没有真正忽略结果,因为有上下文被隐式传递。例如,你可以这样做

main :: IO ()
main = putStrLn "Hello, " >> putStrLn "world"

在 IO Monad 中。另一个例子是 Monad 列表(可以认为是表示不确定性计算):

biggerThanTen :: Int -> Bool
biggerThanTen n = n > 10

example :: String
example = filter biggerThanTen [1..15] >> return 'a'  -- This evaluates to "aaaaa"

请注意,即使在这里我们也没有真的忽略结果。我们忽略了具体的值,但我们使用了结果的结构(在第二个例子中,结构是filter biggerThanTen [1..15]的结果列表有5个元素)。

不过,我应该指出,以这种方式排序的事物不一定按照它们编写的顺序进行评估。您可以通过列表 Monad 示例看到这一点。这在更大的例子中变得更加明显:

example2 :: [Int]
example2 =
  [1,2,3] >>=
    (\x -> [10,100,1000] >>=
             (\y -> return (x * y)))   --  ==> [10,100,1000,20,200,2000,30,300,3000]

这里的主要内容是评估顺序(在没有像 IO 和忽略底部这样的副作用的情况下)不会影响 Haskell 中代码的最终含义(除了可能的效率差异,但这是另一个话题)。因此,永远没有理由以问题中描述的方式“一个接一个地”调用两个函数(也就是说,调用完全相互独立)。

做符号

Do 表示法实际上完全等同于使用>>=>>(实际上还涉及另一件事来处理模式匹配失败,但这与手头的讨论无关)。编译器实际上采用 do 表示法编写的内容,并通过称为“去糖”的过程将它们转换为 >>=>>(因为它删除了语法糖)。以下是上面用 do 表示法编写的三个示例:

IO 示例

main :: IO ()
main = do
  putStrLn "Hello, "
  putStrLn "World"

第一个列表示例

biggerThanTen :: Int -> Bool
biggerThanTen n = n > 10

example :: String -- String is a synonym for [Char], by the way
example = do 
  filter biggerThanTen [1..15]
  return 'a'

第二个列表示例

example2 :: [Int]
example2 = do
  x <- [1,2,3]
  y <- [10,100,1000]
  return (x * y)

以下是转换的并排比较:

do          --
  m         --  m >> n
  n         --


do          --
  x <- m    -- m >>= (\x ->
  ...       --           ...)

理解 do 表示法的最佳方法是首先理解 &gt;&gt;=return,因为正如我所说,这就是编译器将 do 表示法转换成的内容。

作为旁注,&gt;&gt;&gt;&gt;= 相同,它只是忽略了左参数的“结果”(尽管它保留了“上下文”或“结构”)。所以&gt;&gt;的所有定义都必须等价于m &gt;&gt; n = m &gt;&gt;= (\_ -&gt; n)

在第二个列表示例中扩展&gt;&gt;=

为了帮助理解 Monad 通常不是不纯的这一点,让我们扩展第二个列表示例中的 &gt;&gt;= 调用,使用列表的 Monad 定义。定义是:

instance Monad [] where
  return x = [x]
  xs >>= f = concatMap f xs

我们可以将example2 转换为:

第 0 步(我们已有的)

example2 :: [Int]
example2 =
  [1,2,3] >>=
    (\x -> [10,100,1000] >>=
             (\y -> return (x * y)))

第 1 步(转换第一个 &gt;&gt;=

example2 =
  concatMap
    (\x -> [10,100,1000] >>=
              (\y -> return (x * y)))
    [1,2,3]

第 2 步

example2 =
  concatMap
    (\x -> concatMap
             (\y -> return (x * y))
             [10,100,1000])
    [1,2,3]

第 3 步

example2 =
  concatMap
    (\x -> concatMap
             (\y -> [x * y])
             [10,100,1000])
    [1,2,3]

所以,这里没有魔法,只是正常的函数调用。

【讨论】:

  • 我喜欢你的回答,但对我来说仍然很难掌握。当我试图找到真实世界的 Haskell 示例时,我发现 do(或 IO)在这些示例中被大量使用。所以乍一看,它看起来类似于命令式程序。你能否给我一个真实世界的 Haskell 程序的例子,其中有很多纯函数代码和 doIO 的使用最少。
  • @uintptr_t:实际上,仅仅因为某些东西使用了do,并不一定意味着它是不纯的或者它使用了IO。 do 表示法实际上被转换为对&gt;&gt;=&gt;&gt; 的调用。我在我的回答中添加了一些关于 do 表示法的更多信息(我知道这与你在这里问的相反,但我想指出 do 和 Monads 与纯度和 IO 的概念完全不同)。例如,对于列表,xs &gt;&gt;= f = concatMap f xs 所以那里没有杂质。事实上,IO 是规则的exception。大多数 Monad 只涉及纯函数调用。
  • @uintptr_t 我认为你很难理解它是如何工作的原因是缺乏经验。我建议像Learn you a Haskell for Great Good 这样的好教程。一旦你习惯了很多纯函数的例子,就会更清楚为什么它会起作用。你说的就相当于第一次看代码就说“这怎么是应用程序?这怎么可能是我玩的游戏?”如果不先教你如何编写代码,就很难回答这个问题——对于纯函数式编程也是如此——这是一种新的思维方式,几乎没有简短的解释。
  • 想知道关于seq的部分是否准确:but this evaluation order isn't guaranteed.
  • @funct7 确实,Haskell 标准不保证seq 的特定评估顺序。唯一可以保证的是 seqboth 参数在 seq 调用完成评估时被评估为 WHNF(如果它首先被评估)。这在 Prelude here 的文档中进行了描述。另见this section of the Haskell 2010 Report
【解决方案2】:

您可以编写一个函数,其参数取决于另一个函数的评估:

-- Ads the first two elements of a list together
myFunc :: [Int] -> Int
myFunc xs = (head xs) + (head $ tail xs)

如果这就是你的意思。在这种情况下,如果不评估 head xshead $ tail xs(+),就无法获得 myFunc xs 的输出。这里有一个命令。但是,编译器可以选择执行head xshead $ tail xs 的顺序,因为它们不相互依赖,但如果没有其他两个结果,它就无法进行加法。它甚至可以选择并行评估它们,或者在不同的机器上评估它们。关键是纯函数,因为它们没有副作用,所以在它们的结果相互依赖之前,不必按给定的顺序进行计算。


查看上述函数的另一种方式是作为图表:

                            myFunc
                              |
                             (+)
                            /   \
                           /     \
                         head    head
                           \      |
                            \    tail
                             \  /
                              xs

为了评估一个节点,必须首先评估它下面的所有节点,但可以并行评估不同的分支。首先必须评估xs,至少部分评估,但之后可以并行评估两个分支。由于惰性求值存在一些细微差别,但这本质上是编译器构造求值树的方式。


如果你真的想在另一个之前强制调用一个函数,你可以使用seq 函数。它需要两个参数,强制第一个被评估,然后返回第二个,例如

myFunc2 :: [Int] -> Int
myFunc2 xs = hxs + (hxs `seq` (head $ tail xs))
    where hxs = head xs

这将强制 head xshead $ tail xs 之前进行评估,但这更多地处理严格性而不是排序函数。

【讨论】:

  • 只有这样吗?假设func1func2 是独立的。怎么一个接一个的调用? (以不必要的顺序)
  • 您可以使用seq 函数强制在评估第二个参数之前先评估其第一个参数,但通常这种计算在 Haskell 中毫无意义。由于没有副作用,因此先评估一个(如果它们是独立的)并不重要。
  • 让我补充几句。当使用非严格函数时,顺序图的想法更难应用。例如。将f x ++ g x 减少到WHNF 不会导致g x 被调用,除非f x 是空列表。此外,与直觉相反,seq x y 不需要在y 之前评估x,而只需在x 为底部时评估到底部:实现可以先评估y,然后是@ 987654345@,然后返回y的结果。相反,pseq x y' does guarantee that x` 首先被评估,因为我们确实需要它来强制并行评估。
【解决方案3】:

这是一个简单的方法:

case f x of
    result1 -> case g y of
        result2 -> ....

不过,除非 g y 使用来自 result1 的东西,随后的计算来自 result2,或者模式是必须评估结果,否则不能保证 fg 是实际调用,也不按什么顺序。

不过,您仍然想要一种方法来调用一个又一个函数,而这就是这种方法。

【讨论】:

  • 与可变模式匹配的模式不会强制对术语开始匹配进行评估。仅当(非惰性)模式涉及构造函数时才会进行评估。 (相当令人困惑的是,在 GHC 核心语言中,它会评估该术语)
  • @chi 你确实阅读并理解了我在代码示例之后写的那段?我认为这清楚地表明result1result2 是严格模式的(元)变量。无论如何,无论何时评估什么,它仍然是“一个接一个地调用函数”的有效方式。
  • 重点,我确实错过了那条评论:-P。不过,我不会称其为“一个接一个地调用函数”,而且我相信 OP 并没有要求这样做。不过,我确实理解这是一个术语问题,因此每个意见都是有争议的。 (顺便说一句,我没有否决你的答案。)
  • @chi 否决票不是我关心的问题。但是看,“之后”可能有两个含义:时间和空间。在时间意义上,显然 case 构造一个接一个地调用一个函数,因为它不是用于并行性的语言构造。而在空间意义上,g 在程序文本中的应用是在 f 之后出现的......所以没有术语问题,除非我错过了“之后”的含义。
  • 您是否也认为let x1 = f x in let x2 = g x in ... 是一个可接受的解决方案?甚至let x1 = f x ; x2 = g x in ...?假设是这样,在空间意义上,我会同意你的看法。从时间的角度来看,不是这样,因为 Haskell 没有规定具体的评估顺序,我倾向于用“时间”来识别评估顺序。
猜你喜欢
  • 2021-08-15
  • 1970-01-01
  • 2021-06-29
  • 2021-06-05
  • 2019-01-04
  • 2017-02-11
  • 2011-11-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多