Понимание неизменных структур данных

Я изучаю scala и как хороший студент я стараюсь соблюдать все правила, которые я нашел.

одно правило: неизменность!!!

поэтому я попытался закодировать все с неизменяемыми структурами данных и vals, и иногда это действительно сложно.

но сегодня я подумал про себя: единственное, что важно, это то, что объект/класс не должен иметь изменяемого состояния. Я не вынужден кодировать все методы в неизменяемом стиле, потому что эти методы не влияют на каждый другой.

Мой Вопрос: Я прав или есть какие-либо проблемы/недостатки, которые я не вижу?

EDIT:

пример кода для 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
}  

объяснение: это метод вычисления логарифмического правдоподобия однородной марковской модели переменного порядка. Метод apply состояния принимает все предыдущие символы и предстоящий символ и возвращает вероятность этого.

Как вы можете видеть: весь метод просто умножает некоторые вероятности, которые было бы намного проще использовать vars.

3 ответов


правила не непреложность, но референциальной прозрачности. Совершенно нормально использовать локально объявленные изменяемые переменные и массивы, потому что ни один из эффектов не наблюдаем для любых других частей общей программы.

принцип ссылочной прозрачности (RT) заключается в следующем:

выражение e is совершенно прозрачна если для всех программ p каждое вхождение e на p можно заменить с результатом оценки e, не влияя на наблюдаемый результат p.

обратите внимание, что если e создает и мутирует некоторое локальное состояние, оно не нарушает RT, так как никто не может наблюдать это.

тем не менее, я очень сомневаюсь, что ваша реализация более проста с vars.


случай для функционального программирования один из быть кратким в вашем коде и привносить более математический подход. Это может уменьшить вероятность ошибок и сделать ваш код меньше и более читаемым. Что касается того, легче или нет, это требует, чтобы вы думали о своих проблемах по-другому. Но как только вы привыкнете думать с функциональными шаблонами, вероятно, что функциональный станет легче, чем более императивный стиль.

Это действительно трудно быть идеально функциональный и имеют нулевое изменяемое состояние, но очень полезно иметь минимальный изменяемое состояние. Нужно помнить, что все необходимо сделать в равновесии и не до крайности. Уменьшая количество изменяемого состояния, вы в конечном итоге затрудняете написание кода с непреднамеренными последствиями. Общий шаблон должен иметь изменяемую переменную, значение которой является неизменяемым. Таким образом, identity (именованная переменная ) и value (неизменяемый объект, переменная может быть назначенным ) являются отдельными.

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 зацикливается на значении acc в то время, когда мы начали foreach. Любые мутации в 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 (которое является списком ) и не беспокойтесь о синхронизации, потому что список неизменяем aka все члены объекта списка являются окончательными. Также из-за неизменности мы знаем, что никакой другой актер не может изменить базовую структуру данных, которая была отправлена и, таким образом никакой другой актер не может изменить изменчивое состояние этого актера.


С Apocalisp уже указано материал, который я собирался процитировать его, я буду обсуждать код. Вы говорите, что это просто умножение материала, но я не вижу этого-он ссылается по крайней мере на три важных метода, определенных снаружи: order, states и M.log. Я могу сделать вывод, что order это Int и states возвращает функцию, которая принимает List[T] и a 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.log и states... Я бьюсь об заклад, я мог бы превратить это map на foldLeft и покончить с этими двумя методами. И я подозреваю, что метод вернулся states может взять весь срез вместо двух параметров.

по-прежнему... неплохо, правда?