掌握不可变数据结构

时间:2011-12-01 18:07:24

标签: scala data-structures functional-programming immutability

我正在学习斯卡拉,作为一名好学生,我试图遵守我发现的所有规则。

一条规则是:IMMUTABILITY !!!

所以我尝试使用不可变数据结构和val来编写所有代码,有时候这很难。

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

我的问题:我是否正确或有任何问题/缺点我看不到

修改

aishwarya的代码示例:

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方法接受所有先前的符号和即将到来的符号,并返回这样做的概率。

正如您所看到的:整个方法只是增加了一些使用变量更容易的概率。

3 个答案:

答案 0 :(得分:47)

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

参考透明度(RT)的原则是:

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

请注意,如果{{1}}创建并更改某些本地状态,则不会违反RT,因为没有人能够观察到这种情况。

那就是说,我非常怀疑你的实施对vars来说更直接。

答案 1 :(得分:7)

函数式编程的案例之一是在您的代码中简洁并引入更多数学方法。它可以减少错误的可能性,使您的代码更小,更易读。至于更容易与否,它确实需要您以不同的方式思考您的问题。但是,一旦你习惯于使用功能模式进行思考,那么功能性变得更容易变得更加容易。

很难完全正常运行并且具有零可变状态,但非常有利于最小可变状态。要记住的是,所有都需要平衡而不是极端。通过减少可变状态的数量,您最终会更难以编写具有意外后果的代码。一个常见的模式是拥有一个可变变量,其值是不可变的。这样,标识(命名变量)和值(可以赋值变量的不可变对象)是分开的。

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内存模型规范,不可变对象很有用,它声明对象最终成员的初始化将在任何线程可以看到这些成员之前发生,期间!如果它们不是最终的那么它们是“可变的”并且不能保证正确的初始化。

演员是放置可变状态的理想场所。表示数据的对象应该是不可变的。以下面的例子为例。

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对象的所有成员都是final。同样由于不变性,我们知道没有其他演员可以改变发送的基础数据结构,因此没有其他演员可以改变这个演员的可变状态

答案 2 :(得分:3)

由于Apocalisp已经mentioned我要引用他的内容,我将讨论代码。你说它只是成倍增加,但我没有看到 - 它引用了至少三个在外面定义的重要方法:orderstatesM.log。我可以推断orderIntstates返回一个函数,该函数需要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返回的方法可以取整个切片而不是两个参数。

还是......不错,是吗?