Передача параметров функциям-мутаторам в Scala

Я играю в 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
}

Единственный вышеприведенный код, который не компилируется, — это var в def initList(), а поскольку data — это val, я не могу выполнять какие-либо изменения в нем внутри функции.

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

Я прочитал учебники и погуглил для этого, я предполагаю, что не могу найти много об этом, потому что обычно это не делается в Scala.


person Nic Foster    schedule 21.01.2012    source источник


Ответы (4)


Первое, что нужно понять, это то, что хотя 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
person Dan Simon    schedule 21.01.2012

Вы должны предпочесть функциональное/неизменяемое решение, предложенное в других ответах. Однако, когда вам действительно нужна эта возможность — чтобы передать значение по ссылке, вы можете использовать Изменяемые эталонные ячейки в стиле ML.

Вот как вы объявляете их:

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)
person missingfaktor    schedule 23.01.2012

Как насчет такого:

def initList(inData: List[Int]) : List[Int] = {
    var data = inData  // <----The one added line
    for ( i <- 0 to 1000 )
    {
        data = i :: data
    }
    data.reverse
}
person Destin    schedule 21.01.2012
comment
Это сработало, я не могу поверить, что решение проблемы состоит в том, чтобы сделать изменяемую копию неизменной копии изменяемой переменной. Есть ли способ сделать это без копии? Или это считается стандартной практикой для Scala? - person Nic Foster; 21.01.2012
comment
Ну, на самом деле это очень мало копирует; объявление «данных» как var было просто тем, что вы сказали, что ссылка, на которую указывают данные, может быть изменена. 'inData' в основном является val по умолчанию, поэтому он использует ту же ссылку, что и 'data' в течение своего срока службы (или, по крайней мере, ту же ссылку, которая была первоначально передана в функцию), а затем initList#data также принимает ту же самую ссылка изначально — это три вещи, которые одновременно имеют одну и ту же ссылку! — но затем она постоянно перезаписывается циклом for{}. - person Destin; 21.01.2012
comment
Итак... есть ли другой способ? Да... есть и другие способы, но я не думаю, что они более практичны, чем этот. Его стоимость очень мала, и он выполняет то, что вы хотите. - person Destin; 21.01.2012

Что может быть лучше кода

val data = 1000 to 0 by -1

Гораздо быстрее (имо) легче читать.

person Malvolio    schedule 21.01.2012