Как смоделировать переменную «назначить один раз» в Scala?

Это дополнительный вопрос к моему предыдущему вопросу о переменной инициализации.

Предположим, мы имеем дело с этим контекстом:

object AppProperties {

   private var mgr: FileManager = _

   def init(config: Config) = {
     mgr = makeFileManager(config)
   }

}

Проблема с этим кодом в том, что любой другой метод в AppProperties может переназначить mgr. Есть ли способ лучше инкапсулировать mgr, чтобы он воспринимался как val для других методов? Я думал о чем-то подобном (вдохновленный этим ответ):

object AppProperties {

  private object mgr {
    private var isSet = false
    private var mgr: FileManager = _
    def apply() = if (!isSet) throw new IllegalStateException else mgr
    def apply(m: FileManager) {
      if (isSet) throw new IllegalStateException 
      else { isSet = true; mgr = m }
    }
  }

   def init(config: Config) = {
     mgr(makeFileManager(config))
   }

}

... но мне это кажется довольно тяжелым (и инициализация слишком напоминает мне C++ :-)). Любая другая идея?


person Jean-Philippe Pellet    schedule 09.12.2010    source источник


Ответы (8)


Вы можете сделать это с неявными, сделав неявные доступными только в методе, который должен иметь возможность переназначения. Просмотр значения не требует неявного, поэтому «переменная» видна другим методам:

sealed trait Access                                                                                                                                                                                            

trait Base {                                                                                                                                                                                                  

  object mgr {                                                                                                                                                                                                 
    private var i: Int = 0                                                                                                                                                                                     
    def apply() = i                                                                                                                                                                                            
    def :=(nv: Int)(implicit access: Access) = i = nv                                                                                                                                                          
  }                                                                                                                                                                                                            

  val init = {                                                                                                                                                                                                 
    implicit val access = new Access {}                                                                                                                                                                        

    () => {                                                                                                                                                                                                    
      mgr := 5                                                                                                                                                                                                 
    }                                                                                                                                                                                                          
  }                                                                                                                                                                                                            

}

object Main extends Base {

  def main(args: Array[String]) {                                                                                                                                                                              
    println(mgr())                                                                                                                                                                                             
    init()                                                                                                                                                                                                     
    println(mgr())                                                                                                                                                                                             
  }                                                                                                                                                                                                            

}
person axel22    schedule 09.12.2010
comment
Окончательное решение опубликовано здесь: stackoverflow.com/questions/4404024/ - person Jean-Philippe Pellet; 10.12.2010

Итак, вот мое предложение, непосредственно вдохновленное axel22, Рекс Керр и Debilski ответы:

class SetOnce[T] {
  private[this] var value: Option[T] = None
  def isSet = value.isDefined
  def ensureSet { if (value.isEmpty) throwISE("uninitialized value") }
  def apply() = { ensureSet; value.get }
  def :=(finalValue: T)(implicit credential: SetOnceCredential) {
    value = Some(finalValue)
  }
  def allowAssignment = {
    if (value.isDefined) throwISE("final value already set")
    else new SetOnceCredential
  }
  private def throwISE(msg: String) = throw new IllegalStateException(msg)

  @implicitNotFound(msg = "This value cannot be assigned without the proper credential token.")
  class SetOnceCredential private[SetOnce]
}

object SetOnce {
  implicit def unwrap[A](wrapped: SetOnce[A]): A = wrapped()
}

Мы получаем безопасность во время компиляции, поскольку := не вызывается случайно, поскольку нам нужен SetOnceCredential объекта, который возвращается только один раз. Тем не менее, переменная может быть переназначена при условии, что у вызывающего объекта есть исходные учетные данные. Это работает с AnyVals и AnyRefs. Неявное преобразование позволяет мне напрямую использовать имя переменной во многих случаях, и если это не сработает, я могу преобразовать его явно, добавив ().

Типичное использование будет следующим:

object AppProperties {

  private val mgr = new SetOnce[FileManager]
  private val mgr2 = new SetOnce[FileManager]

  val init /*(config: Config)*/ = {
    var inited = false

    (config: Config) => {
      if (inited)
        throw new IllegalStateException("AppProperties already initialized")

      implicit val mgrCredential = mgr.allowAssignment
      mgr := makeFileManager(config)
      mgr2 := makeFileManager(config) // does not compile

      inited = true
    }
  }

  def calledAfterInit {
    mgr2 := makeFileManager(config) // does not compile
    implicit val mgrCredential = mgr.allowAssignment // throws exception
    mgr := makeFileManager(config) // never reached
}

Это не приводит к ошибке времени компиляции, если в какой-то другой точке того же файла я пытаюсь получить другие учетные данные и переназначить переменную (как в calledAfterInit), но терпит неудачу во время выполнения.

person Jean-Philippe Pellet    schedule 10.12.2010
comment
Но, насколько я понимаю сейчас, вы всегда можете получить учетные данные SetOne.allowMe в любом месте вашего кода (по сравнению с версией sealed), и присваивание будет работать, пока это первое присваивание. Таким образом, неявные учетные данные теперь бесполезны. Или я что-то упускаю? - person Debilski; 10.12.2010
comment
Присваивание не будет работать из любого места в моем коде, так как mgr является закрытым, но вы правы, это небольшой недостаток. Но действительно ли это хуже, чем версия axel22, где я мог создать еще один Access в любом месте того же файла? (Я мог переместить Base в другой файл, чтобы получить 100% гарантию, но не будет ли это излишним только для инициализации одного поля? Я бы предпочел оставить его в том же файле и получить обзор лучше). Во-вторых, я предпочитаю легко переиспользуемый SetOnce, а не более подробное решение, которое потребовало бы от меня повторного объявления новых трейтов доступа для каждой новой однократно устанавливаемой переменной... - person Jean-Philippe Pellet; 10.12.2010
comment
Это не лучше и не хуже, просто учетные данные бесполезны в вашем случае, потому что вы можете вызвать SetOnce.allowMe из любого места вашего кода и, таким образом, получить переменную учетных данных. На самом деле вы ничего этим не выигрываете. - person Debilski; 10.12.2010
comment
Хорошо, теперь я изменил свое предложение: теперь SetOnceCredential является внутренним классом SetOnce. Вызов allowAssignment (бывший allowMe) теперь возможен только в рамках mgr. - person Jean-Philippe Pellet; 10.12.2010
comment
Возможно, вам следует показать пример ситуации, когда неправильные/отсутствующие учетные данные фактически препятствуют назначению, и их невозможно получить. :) - person Debilski; 10.12.2010
comment
Я добавил пример, в котором :=` без надлежащих учетных данных в области видимости не компилируется. Все еще можно попытаться получить учетные данные, но это выдает исключение... Как вы писали, на данный момент это кажется трудно исправить. - person Jean-Philippe Pellet; 10.12.2010
comment
Я только что узнал о @implicitNotFound и добавил его :-) - person Jean-Philippe Pellet; 10.12.2010

Я предполагаю, что вам не нужно делать это эффективно с примитивами, и для простоты вам также не нужно хранить null (но вы, конечно, можете изменить идею, если эти предположения ложны):

class SetOnce[A >: Null <: AnyRef] {
  private[this] var _a = null: A
  def set(a: A) { if (_a eq null) _a = a else throw new IllegalStateException }
  def get = if (_a eq null) throw new IllegalStateException else _a
}

и просто используйте этот класс везде, где вам нужна эта функциональность. (Может быть, вы бы предпочли apply() get?)

Если вы действительно хотите, чтобы это выглядело как доступ к переменной (или методу) без каких-либо дополнительных трюков, сделайте SetOnce закрытым и

private val unsetHolder = new SetOnce[String]
def unsetVar = unsetHolder.get
// Fill in unsetHolder somewhere private....
person Rex Kerr    schedule 09.12.2010
comment
Хороший. Я буду использовать комбинацию вашего ответа и неявного ответа axel22 для назначения. - person Jean-Philippe Pellet; 10.12.2010
comment
По какой причине вы по умолчанию используете null: A, а не None: Option[A]? - person Debilski; 10.12.2010
comment
Я разместил свое окончательное решение здесь: stackoverflow.com/questions/4404024/ - person Jean-Philippe Pellet; 10.12.2010
comment
@Debilski - null не виден миру и требует меньше вычислительных ресурсов, чем Option. Это нечто достаточно низкоуровневое, чтобы можно активно использоваться, и требуется примерно столько же усилий, чтобы написать его более эффективно. Но если бы кто-то хотел сохранить null, конечно, он бы сделал что-то еще (хотя я бы снова выбрал частное логическое значение вместо Option из-за меньших накладных расходов). - person Rex Kerr; 10.12.2010

Не самый лучший способ и не совсем то, о чем вы просили, но он дает вам некоторую инкапсуляцию доступа:

object AppProperties {
  def mgr = _init.mgr
  def init(config: Config) = _init.apply(config)

  private object _init {
    var mgr: FileManager = _
    def apply(config: Config) = {   
      mgr = makeFileMaker(config)
    }
  }
}
person Debilski    schedule 09.12.2010
comment
+1 за то, что это решение создает только один дополнительный объект, даже если у меня есть несколько таких переменных «назначить один раз». Переназначение _init.mgr по-прежнему возможно, но определенно выглядит «достаточно неправильно» в клиентском коде. - person Jean-Philippe Pellet; 10.12.2010
comment
Переназначение невозможно в клиентском коде, потому что _init виден только внутри AppProperties, а def mgr нельзя изменить. - person Debilski; 10.12.2010
comment
Верно, но для этого подойдет и простая private var. Я был заинтересован в том, чтобы предотвратить это в остальной части реализации AppProperties. Думаю, мне не стоило писать «клиентский код». - person Jean-Philippe Pellet; 10.12.2010

Глядя на сообщение JPP, у меня есть сделал еще один вариант:

class SetOnce[T] {
  private[this] var value: Option[T] = None
  private[this] var key: Option[SetOnceCredential] = None
  def isSet = value.isDefined
  def ensureSet { if (value.isEmpty) throwISE("precondition violated: uninitialized value") }
  def apply() = value getOrElse throwISE("uninitialized value")

  def :=(finalValue: T)(implicit credential: SetOnceCredential = null): SetOnceCredential = {
    if (key != Option(credential)) throwISE("Wrong credential")
    else key = Some(new SetOnceCredential)

    value = Some(finalValue)
    key get
  }
  private def throwISE(msg: String) = throw new IllegalStateException(msg)

  class SetOnceCredential private[SetOnce]
}

private val mgr1 = new SetOnce[FileManager]
private val mgr2 = new SetOnce[FileManager]

val init /*(config: Config)*/ = {
    var inited = false

    (config: Config) => {
      if (inited)
        throw new IllegalStateException("AppProperties already initialized")


      implicit val credential1 = mgr1 := new FileManager(config)
      mgr1 := new FileManager(config) // works

      implicit val credential2 = mgr2 := new FileManager(config) // We get a new credential for this one
      mgr2 := new FileManager(config) // works

      inited = true
    }
}

init(new Config)
mgr1 := new FileManager(new Config) // forbidden

На этот раз нам вполне разрешено назначать переменную несколько раз, но нам нужно иметь правильные учетные данные в области видимости. Учетные данные создаются и возвращаются при первом назначении, поэтому нам нужно немедленно сохранить их в implicit val credential = mgr := new FileManager(config). Если учетные данные неверны, он не будет работать.

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

person Debilski    schedule 10.12.2010
comment
Нельзя ли переместить класс SetOnceCredential из объекта SetOnce в класс SetOnce, чтобы избежать упомянутой вами проблемы множественных учетных данных? Кажется, тогда мы могли бы даже избавиться от кэшированных учетных данных в key. Мне нравится эта идея, но теперь гарантия времени компиляции ослаблена, так как любое присваивание будет компилироваться (с нулевым значением по умолчанию), но завершаться ошибкой во время выполнения. - person Jean-Philippe Pellet; 10.12.2010
comment
Ты прав. Перемещение его внутри класса, конечно же, исправляет это. - person Debilski; 10.12.2010
comment
Очень хорошо! Последнее, что мне не нравится, это то, что ваша последняя строка, mgr1 := new FileManager(new Config), хотя и завершится ошибкой во время выполнения, все равно компилируется, не указывая на то, что она действительно требует учетных данных. - person Jean-Philippe Pellet; 10.12.2010
comment
Да, это тяжело. Я не уверен, что мы сможем это исправить. Можно подумать о переменной для однократного чтения для учетных данных и сделать неявную переменную для := обязательной. Но, тем не менее, можно было бы получить доступ к методу однократного чтения для получения учетных данных, и, таким образом, система типов не будет жаловаться, даже если вместо учетных данных возвращается null. - person Debilski; 10.12.2010

Я думал что-то вроде:

object AppProperties {                                        
  var p : Int => Unit = { v : Int => p = { _ => throw new IllegalStateException } ; hiddenx = v  }
  def x_=(v : Int) = p(v)
  def x = hiddenx                                                     
  private var hiddenx = 0                                             
}

X можно установить ровно один раз.

person Malvolio    schedule 09.12.2010
comment
Спасибо, но едва ли лучше, чем мой первоначальный фрагмент кода, так как другие методы в AppProperties могут по-прежнему переназначать hiddenx. - person Jean-Philippe Pellet; 10.12.2010

Это не совсем то же самое, но во многих случаях решение для «установить переменную один раз, а затем продолжать ее использовать» — это простое создание подклассов со специальным фабричным методом или без него.

abstract class AppPropertyBase {
  def mgr: FileManager
}

//.. somewhere else, early in the initialisation
// but of course the assigning scope is no different from the accessing scope

val AppProperties = new AppPropertyBase {
  def mgr = makeFileMaker(...)
}
person Debilski    schedule 10.12.2010

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

object FileManager { 

    private var fileManager : String = null
    def makeManager(initialValue : String ) : String  = { 
        if( fileManager  == null ) { 
            fileManager  = initialValue;
        }
        return fileManager  
    }
    def manager() : String  = fileManager 
}

object AppProperties { 

    def init( config : String ) { 
        val y = FileManager.makeManager( config )
        // do something with ... 
    }

    def other()  { 
        FileManager.makeManager( "x" )
        FileManager.makeManager( "y" )
        val y =  FileManager.manager()
        // use initilized y
        print( y )
        // the manager can't be modified
    }
}
object Main { 
    def main( args : Array[String] ) {

        AppProperties.init("Hello")
        AppProperties.other
    }
}
person OscarRyz    schedule 10.12.2010