【问题标题】:What is a "side-effect" in Scala?Scala 中的“副作用”是什么?
【发布时间】:2021-04-27 14:11:36
【问题描述】:

我目前正在学习使用 Scala 进行函数式编程。

我还在学习循环以及如何避免由于副作用而导致的循环。

这是什么意思?

【问题讨论】:

    标签: scala functional-programming side-effects


    【解决方案1】:

    纯函数式编程语言中的函数完全像数学中的函数:它们根据参数值生成结果值,仅根据参数值。 p>

    副作用(通常称为效果)是其他一切。 IE。不读取参数并返回结果的所有内容都是副作用。

    这包括但不限于:

    • 变异状态,
    • 从控制台读取输入,
    • 将输出打印到控制台,
    • 读取、创建、删除或写入文件,
    • 从网络读取或写入,
    • 反射,
    • 取决于当前时间,
    • 启动或中止线程或进程,或
    • 任何类型的 I/O,最重要的是
    • 调用不纯函数。

    最后一点非常重要:调用不纯函数会使函数不纯。从这个意义上说,副作用是传染性的

    请注意,说“您只能阅读参数”有些简化。一般来说,我们认为函数的环境也是一种“不可见”的论点。这意味着,例如,允许 Closure 从它关闭的环境中读取变量。允许函数读取全局变量。

    Scala 是一种面向对象的语言,并且有方法,其中有一个不可见的this 参数,允许它们读取。

    这里的重要属性称为参考透明度。如果您可以在不改变程序含义的情况下用其值替换函数或表达式(反之亦然),则函数或表达式是引用透明

    请注意,通常情况下,术语“纯”或“纯功能”、“参照透明”和“无副作用”可以互换使用。

    例如,在下面这个程序中,(子)表达式2 + 3 是引用透明的,因为我可以用它的值5 替换它而不改变程序的含义:

    println(2 + 3)
    

    意思完全一样
    println(5)
    

    但是,println 方法是引用透明的,因为如果我用它的值替换它,程序的含义就会改变:

    println(2 + 3)
    

    意思一样吗
    ()
    

    这就是()(读作“单位”)的简单值,也就是println的返回值。

    这样做的结果是,当传递相同的参数时,引用透明函数总是返回相同的结果值。对于所有代码,您应该为相同的输入获得相同的输出。或者更一般地说,如果你一遍又一遍地做同样的事情,同样的结果应该会一遍又一遍地发生。

    这就是循环和副作用之间的联系所在:循环一遍又一遍地做同样的事情。所以,它应该一遍又一遍地得到相同的结果。但它不会:它至少有一次会有不同的结果,即它会完成。 (除非它是一个无限循环。)

    为了使循环有意义,它们必须具有副作用。但是,纯功能程序不得有副作用。因此,在纯函数式程序中,循环不可能有意义

    【讨论】:

    • 我会说非终止/发散和部分函数会带来副作用
    【解决方案2】:

    作为@Jörg 的另一个示例,使用用 Scala 编写的命令式语言来实现这个简单循环:

    def printUpTo(limit: Int): Unit = {
      var i = 0
      while(i <= limit)
      {
        println("i = " + i)
        i += 1
        // in another part of the loop
        if (i % 5 == 0) { i += 1 } // ops. We should not evaluate "i" here.
      }
    }
    

    在这个循环中,有一个变量声明为var i,它是一个在每次迭代中都会改变的状态。虽然从外部看不到这种状态变化(每次输入函数时都会创建一个新的变量副本),但 var 通常意味着代码中存在不必要的混乱并且可以简化。确实可以。

    作为函数式程序员,我们必须努力在任何地方使用不可变状态。在这个循环示例中,如果有人在另一个地方更改了var i 的值,例如在if (i % 5 == 0) { i += 1 } 中由于缺乏注意,将很难调试和查找。这是我们必须避免的副作用。因此,使用不可变状态可以避免此类错误。这是使用不可变状态的相同示例。

    def printUpToFunc1(limit: Int): Unit = {
      for(i <- (0 to limit)) {
        println("i = " + i)
      }
    }
    

    我们可以只使用foreach 使代码更清晰:

    def printUpToFunc2(limit: Int): Unit = {
      (0 to limit).foreach {
        i => println("i = " + i)
      }
     }
    

    而且更小...

    def printUpToFunc3(limit: Int): Unit = (0 to limit).foreach(println)
    

    【讨论】:

      【解决方案3】:

      所有这些都是很好的答案。如果您来自另一种语言,请快速添加一点。

      虚空函数

      一个函数什么都不返回,比如void,意味着有副作用。

      例如,如果你在 c# 中有这段代码

      void Log (string message) => Logger.WriteLine(message); 
      

      这会导致副作用,将某些内容写入记录器。

      这有关系吗?可能你不在乎。但是,这个呢?

      def SubmitOrder(order: Order): Unit = 
      {
         // code that submits an order 
      }
      

      这不会很好。稍后见。

      为什么副作用不好?

      除了一些明显的原因,包括:

      • 难以推理:必须阅读整个函数体才能看到发生了什么;
      • 可变状态:容易出错并且可能不是线程安全的

      最重要的是,测试起来很烦人。

      如何避免副作用?

      一个简单的方法是总是尝试返回一些东西。 (当然,还是尽量不要在内部改变状态,闭包就好)。

      例如前面的例子,如果不是Unit,我们有:

      def SubmitOrder(order: Order): Either[SubmittedOrder, OrderSubmissionError] = 
      {
         // code that submits an order 
      }
      

      这样会好很多,它会告诉读者有副作用以及可能发生的情况。

      循环中的副作用

      现在回到你关于循环的问题,不分析你的真实案例,很难建议如何避免循环的副作用。

      但是,如果您正在编写一个函数,然后您想编写一个调用该函数的循环,请确保该函数不会修改局部变量或其他地方的状态。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2010-11-07
        • 2012-07-22
        • 2017-04-15
        • 2011-07-21
        • 1970-01-01
        • 2010-12-21
        • 2021-01-13
        • 2023-04-10
        相关资源
        最近更新 更多