Функциональная Очередь Из Программирования В Scala

Я прохожу программирование в Scala 2nd Edition Одерского, ложки и Веннерса, и этот пример бросил меня за цикл, так как он, казалось, шел против того, что я думал, было правдой о функциональном программировании и неизменности. В Примере (а ранее в книге В гл. 18), авторы утверждают, что операции над объектом все еще могут быть "чисто функциональными", даже если эти операции могут внутренне мутировать состояние объекта. Пример, который находится на стр. 442, Ch. 19, is это:

 class Queue[+T] private (
   private[this] var leading: List[T], 
   private[this] var trailing: List[T]
 ) {

   private def mirror() = 
     if (leading.isEmpty) {
       while (!trailing.isEmpty) {
         leading = trailing.head :: leading
         trailing = trailing.tail
       }
     }

   def head: T = { 
     mirror()
     leading.head 
   }

   def tail: Queue[T] = { 
     mirror()
     new Queue(leading.tail, trailing) 
   }

   def enqueue[U >: T](x: U) = 
     new Queue[U](leading, x :: trailing)
 }

приведенное обоснование заключается в том, что, пока побочные эффекты не видны клиентам, что-то вроде этого можно считать функциональным. Я думаю, что смогу это скрыть...Я имею в виду, строго говоря, это то, что определяет функцию. Но, по общему признанию (и я не слишком разбираюсь в том, что гарантирует модель памяти JVM), но нет ли потенциальных проблем с этим в этом коде?

например, если два потока выполняют операции в этой очереди, который выглядит так, чтобы начать:

Leading: Nil
Trailing: List(1,2,3,4)

возможно ли, что один поток может вызвать head (), добираясь до этой точки в mirror (), прежде чем быть descheduled:

private def mirror() = 
     if (leading.isEmpty) {
       while (!trailing.isEmpty) {
         leading = trailing.head :: leading
         > trailing = trailing.tail
       }
     }

в этот момент очередь выглядит так:

Leading: List(1)
Trailing: List(1,2,3,4)

и когда второй поток вызывает tail () в то время как первый не активен, это будет возвращено:

Leading: Nil
Trailing: List(1,2,3,4)

тогда как если бы вызов head() первого потока должен был закончиться, это было бы возвращено после последующего вызова tail() в этом списке:

Leading: List(2,3,4)
Trailing: Nil

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

1 ответов


вы правы: функциональный код может быть нефункциональным при использовании несколькими потоками, если он использует внутреннее состояние как часть своей реализации. Вы можете исправить это, синхронизируя каждый метод, который касается изменяемых переменных (т. е. с synchronized блока). К сожалению, это не идеально подходит для производительности, поэтому в потоковых приложениях часто предпочтительны функциональные реализации.