【问题标题】:Passing parameters to mutator functions in Scala将参数传递给Scala中的mutator函数
【发布时间】:2012-01-23 20:54:49
【问题描述】:

我正在使用 Scala 来尝试掌握它,所以这个代码示例只是学术性的。

我正在尝试将一个可变列表传递给一个函数,让该函数对其执行工作,然后在函数调用之后我可以使用更新后的列表。

var data: List[Int] = List()

// Call to function to fill a list
data = initList(data)

// Output only 0-100
data.foreach( num => if (num < 100) { println(num) })

def initList(var data: List[Int]) : List[Int] = {

    for ( i <- 0 to 1000 )
    {
        data = i :: data
    }

    data = data.reverse
    data
}

上面唯一无法编译的代码是def initList() 中的var,因为数据是val,所以我无法在函数内对其执行任何更改。

首先让我说,我知道在 Scala 中,mutators 通常不受欢迎,所以我不仅愿意直接回答我的问题,而且愿意接受更好的方法来完全做到这一点。偶尔在项目中会有一段数据从一个地方传到另一个地方要更新,但是如果你不能将数据传递给要更改的函数,那么有什么好的替代方法呢?

我已经阅读了教程和谷歌搜索,我假设我找不到太多关于它的信息,因为在 Scala 中通常不这样做。

【问题讨论】:

    标签: scala mutators


    【解决方案1】:

    首先要意识到,虽然data 是一个变量,但列表本身仍然是不可变的。虽然您可以将新列表分配给 data,但您不能更改实际列表本身。这也是为什么您不能将列表传递给函数并更改列表的原因。所以实际上你的 for 循环是在每次迭代时创建一个新列表。

    幸运的是,Scala 让编写函数式代码来创建列表和其他不可变数据结构变得非常容易。这是执行您想要的一般“功能”方式:

    def initList(data: List[Int]) = {
      def initLoop(num: Int, buildList: List[Int]): List[Int] = num match {
        case 0 => 0 :: buildList
        case n => initLoop(n - 1, n :: buildList)
      }
      initLoop(1000, data)
    }
    

    基本上这里发生的事情是我用尾递归函数替换了 for 循环。每次调用时,内部函数都会通过获取当前列表并添加下一个数字来构建一个新列表,直到它变为 0 并返回完成的列表。由于函数从 1000 开始并返回到 0,因此列表不必颠倒。

    为了让您了解其工作原理,以下是每次递归调用时内部函数的参数值(尽管我们从 3 而不是 1000 开始):

    initLoop(3, data)
    initLoop(2, 3 :: data)
    initLoop(1, 2 :: 3 :: data)
    initLoop(0, 1 :: 2 :: 3 :: data)
    

    所以当它最终变为 0 时,它会返回(假设数据为空)List(0, 1, 2, 3)

    此方法实际上比使用 for 循环更快,因为您不需要在末尾反转列表。 Scala 能够优化尾递归函数,因此您不必担心堆栈溢出或内存不足错误。

    还有很多其他方法可以创建和转换列表,我也可以这样做:

    val data: List[Int] = List()
    (0 to 1000).foldRight(data){ (num, buildList) => num :: buildList}
    

    甚至只是这样:

    (0 to 1000).toList
    

    【讨论】:

      【解决方案2】:

      您应该更喜欢其他答案中建议的功能/不可变解决方案。然而,当你真的需要这个工具时——通过引用传递一个值,你可以使用ML style mutable reference cells

      这是你声明它们的方式:

      val cell: Ref[SomeClass] = Ref(value)
      

      这是您获取其价值的方式:

      !cell
      

      这就是你改变它们的价值的方式:

      cell := !cell + 1
      // OR
      cell.modify(_ + 1)
      

      一个简单的参考单元实现:

      final class Ref[A] private(private var a: A) {
        def :=(newValue: A): Unit = {
          a = newValue
        }
      
        def unary_! : A = a
      
        def modify(f: A => A): Unit = {
          a = f(a)
        }
      }
      
      object Ref {
        def apply[A](value: A) = new Ref(value)
      }
      

      您可以为此添加许多有用的方法。例如整数值的递增、递减。

      这是您使用参考单元格重写的代码(按预期工作):

      def initList(data: Ref[List[Int]]): Unit = {
        for(i <- 0 to 1000)
          data := i :: !data
        data := (!data).reverse
      }
      
      val data = Ref(List.empty[Int])
      initList(data)    
      for(num <- !data; if num < 100)
        println(num)
      

      【讨论】:

        【解决方案3】:

        这样的事情怎么样:

        def initList(inData: List[Int]) : List[Int] = {
            var data = inData  // <----The one added line
            for ( i <- 0 to 1000 )
            {
                data = i :: data
            }
            data.reverse
        }
        

        【讨论】:

        • 成功了,我无法相信问题的答案是制作可变变量的不可变副本的可变副本。有没有办法在没有副本的情况下做到这一点?或者这被认为是 Scala 的标准做法?
        • 嗯,这实际上复制的很少;将“数据”声明为 var 只是您说数据指向的引用可以更改。 'inData' 在默认情况下基本上是一个 val,因此它在其生命周期内与 'data' 共享相同的引用(或者,至少与最初传递给函数的引用相同),然后 initList#data 也采用相同的引用最初的引用——这是三个同时具有相同引用的事物!——但它随后被 for{} 循环不断覆盖。
        • 那么...还有其他方法吗?是的...还有其他方法,但我认为没有比这更实用的方法了。它的成本非常低,并且可以完成您想要的。
        【解决方案4】:

        更好的代码是

        val data = 1000 to 0 by -1
        

        在 (imo) 中更快更易于阅读。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2018-11-12
          • 2019-06-19
          • 2013-01-27
          • 2020-07-15
          • 1970-01-01
          • 2015-07-18
          • 2015-09-11
          • 1970-01-01
          相关资源
          最近更新 更多