【问题标题】:A grasp of immutable datastructures掌握不可变数据结构
【发布时间】:2011-12-01 18:07:24
【问题描述】:

我正在学习 scala,作为一名优秀的学生,我努力遵守我发现的所有规则。

一个规则是:不变性!!!

所以我尝试用不可变的数据结构和 val 对所有内容进行编码,有时这真的很难。

但今天我想:唯一重要的是对象/类不应该有可变状态。我不必强制以不可变的方式编写所有方法,因为这些方法不会相互影响。

我的问题:我是正确的还是我没有看到任何问题/缺点

编辑:

艾西瓦娅的代码示例:

def logLikelihood(seq: Iterator[T]): Double = {
  val sequence = seq.toList
  val stateSequence = (0 to order).toList.padTo(sequence.length,order)
  val seqPos = sequence.zipWithIndex

  def probOfSymbAtPos(symb: T, pos: Int) : Double = {
    val state = states(stateSequence(pos))
    M.log(state( seqPos.map( _._1 ).slice(0, pos).takeRight(order), symb))
  }

  val probs = seqPos.map( i => probOfSymbAtPos(i._1,i._2) )

  probs.sum
}  

解释:这是一种计算变阶齐次马尔可夫模型的对数似然的方法。 state 的 apply 方法获取所有先前的符号和即将到来的符号,并返回这样做的概率。

如您所见:整个方法只是将一些概率相乘,使用 var 会更容易。

【问题讨论】:

  • 一个数据结构怎么可能没有状态呢?
  • 选择的答案是错误的。请参阅答案下的我的cmets。引用透明度甚至不是可交换的。
  • 请允许我说一下 Shelby Moore III 的最后一条评论正在激烈讨论中。

标签: scala data-structures functional-programming immutability


【解决方案1】:

规则并不是真正的不变性,而是参照透明。使用本地声明的可变变量和数组是完全可以的,因为整个程序的任何其他部分都无法观察到任何效果。

引用透明(RT)的原理是这样的:

表达式e引用透明如果对于所有程序pp 中的每个e 都可以替换为评估e 的结果,而不会影响p 的可观察结果。

请注意,如果 e 创建并改变了某些本地状态,它不会违反 RT,因为没有人可以观察到这种情况发生。

也就是说,我非常怀疑您的实现是否使用 vars 更简单。

【讨论】:

  • 是的。您正在寻找的是一些原则,当遵循这些原则时,会给您带来一些好处。您具有“不变性”,这非常接近,但限制性太强。 “参照透明”是更普遍的原则,我相信这是您正在寻找的原则。
  • 另请注意,您要寻找的原则不是适度。 IE。 “一点”可变状态的想法是可以的。
  • @Apocalisp 我同意你所说的原则,但我觉得它并不完全准确。使用内部突变仍然意味着您违反了在本地实现中的引用透明度。如果本地范围足够大且足够复杂(但通常不应该如此),这可能是一个问题。只是通过引用透明的接口进行编程,您可以获得大部分好处。您似乎在暗示引用透明度是一个只能应用于接口的属性,这是不正确的。
  • @Apocalisp 啊,那我们同意。我读到您的帖子建议如果您追求“无处不在的引用透明性”而不是“无处不在的不变性”,那么您自然会得到纯接口和可能不纯的本地实现。尽管我相信一个在其接口上具有引用透明性但在内部使用可变性的函数可能会在您具有并发性时失败。如果这是正确的,那么并非所有使用“局部可变性”的方式都是真正不可观察的。
  • @Ben:如果并发会影响结果,那么这个表达式最强调不是引用透明的。
【解决方案2】:

函数式编程的情况之一是在代码中保持简洁并引入更数学的方法。它可以减少出现错误的可能性,并使您的代码更小且更具可读性。至于更容易与否,它确实需要您以不同的方式思考您的问题。但是,一旦您习惯了使用函数式模式进行思考,那么函数式可能会比命令式风格更容易。

完美的功能和零可变状态确实很难,但拥有最小可变状态非常有益。要记住的是,所有事情都需要平衡而不是极端。通过减少可变状态的数量,您最终会使编写具有意想不到后果的代码变得更加困难。一个常见的模式是有一个可变变量,其值是不可变的。这样身份(命名变量)和值(可以分配变量的不可变对象)是分开的。

var acc: List[Int] = Nil
// lots of complex stuff that adds values
acc ::= 1
acc ::= 2
acc ::= 3
// do loop current list
acc foreach { i => /* do stuff that mutates acc */ acc ::= i * 10 }
println( acc ) // List( 1, 2, 3, 10, 20, 30 )

在我们启动 foreach 时,foreach 正在循环 acc 的值。 acc 的任何突变都不会影响循环。这比 java 中列表可以在迭代中更改的典型迭代器安全得多。

还有一个并发问题。 不可变对象非常有用,因为 JSR-133 内存模型规范断言对象最终成员的初始化将在任何线程可以看到这些成员之前发生,句号!如果它们不是最终的,那么它们是“可变的”,并且不能保证正确初始化。

Actor 是放置可变状态的理想场所。表示数据的对象应该是不可变的。举个例子。

object MyActor extends Actor {
  var acc: List[Int] = Nil
  def act() {
    loop {
      react {
        case i: Int => acc ::= i
        case "what is your current value" => reply( acc )
        case _ => // ignore all other messages
      }
    }
  }
}

在这种情况下,我们可以发送 acc 的值(它是一个 List )并且不用担心同步,因为 List 是不可变的,即 List 对象的所有成员都是最终的。同样由于不可变性,我们知道没有其他参与者可以更改发送的底层数据结构,因此没有其他参与者可以更改此参与者的可变状态

【讨论】:

    【解决方案3】:

    由于Apocalisp 已经拥有mentioned 我要引用他的内容,我将讨论代码。你说它只是在相乘,但我没有看到——它至少引用了外部定义的三个重要方法:orderstatesM.log。我可以推断出order 是一个Int,而states 返回一个函数,该函数接受一个List[T] 和一个T 并返回Double

    还有一些奇怪的事情正在发生......

    def logLikelihood(seq: Iterator[T]): Double = {
      val sequence = seq.toList
    

    sequence 除了定义seqPos 外,从未使用过,那为什么要这样做呢?

      val stateSequence = (0 to order).toList.padTo(sequence.length,order)
      val seqPos = sequence.zipWithIndex
    
      def probOfSymbAtPos(symb: T, pos: Int) : Double = {
        val state = states(stateSequence(pos))
        M.log(state( seqPos.map( _._1 ).slice(0, pos).takeRight(order), symb))
    

    实际上,您可以在此处使用sequence 而不是seqPos.map( _._1 ),因为所做的只是撤消zipWithIndex。另外,slice(0, pos) 就是 take(pos)

      }
    
      val probs = seqPos.map( i => probOfSymbAtPos(i._1,i._2) )
    
      probs.sum
    }
    

    现在,鉴于缺少方法,很难断言应该如何真正以函数式风格编写。保持神秘的方法会产生:

    def logLikelihood(seq: Iterator[T]): Double = {
      import scala.collection.immutable.Queue
      case class State(index: Int, order: Int, slice: Queue[T], result: Double)
    
      seq.foldLeft(State(0, 0, Queue.empty, 0.0)) {
        case (State(index, ord, slice, result), symb) =>
          val state = states(order)
          val partial = M.log(state(slice, symb))
          val newSlice = slice enqueue symb
          State(index + 1, 
                if (ord == order) ord else ord + 1, 
                if (queue.size > order) newSlice.dequeue._2 else newSlice,
                result + partial)
      }.result
    }
    

    只有我怀疑state/M.log 的东西也可以成为State 的一部分。既然我已经这样写了,我注意到其他优化。你使用的滑动窗口当然让我想起了sliding

    seq.sliding(order).zipWithIndex.map { 
      case (slice, index) => M.log(states(index + order)(slice.init, slice.last))
    }.sum
    

    这只会从 orderth 元素开始,因此需要进行一些调整。不过也不算太难。所以我们再重写一遍:

    def logLikelihood(seq: Iterator[T]): Double = {
      val sequence = seq.toList
      val slices = (1 until order).map(sequence take) ::: sequence.sliding(order)
      slices.zipWithIndex.map { 
        case (slice, index) => M.log(states(index)(slice.init, slice.last))
      }.sum
    }
    

    我希望我能看到M.logstates...我敢打赌我可以把map 变成foldLeft 并取消这两种方法。而且我怀疑states 返回的方法可能会采用整个切片而不是两个参数。

    还是……还不错吧?

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-05-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-10-27
      • 2021-10-09
      • 2022-01-26
      • 1970-01-01
      相关资源
      最近更新 更多