6

This is a follow-up question to my previous initialization variable question.

Suppose we're dealing with this context:

object AppProperties {

   private var mgr: FileManager = _

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

}

The problem with this code is that any other method in AppProperties might reassign mgr. Is there a technique to better encapsulate mgr so that it feels like a val for the other methods? I've thought about something like this (inspired by this answer):

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))
   }

}

... but this feels rather heavyweight to me (and initialization reminds me too much of C++ :-)). Any other idea?

Community
  • 1
  • 1
Jean-Philippe Pellet
  • 59,296
  • 21
  • 173
  • 234

8 Answers8

8

You could do it with implicits, making the implicit available only in the method that is supposed to be able to reassign. Viewing the value doesn't require the implicit, so the "variable" is visible to other methods:

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())                                                                                                                                                                                             
  }                                                                                                                                                                                                            

}
axel22
  • 32,045
  • 9
  • 125
  • 137
4

OK, so here's my proposal, directly inspired by axel22's, Rex Kerr's, and Debilski's answers:

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()
}

We get compile-time safety that := is not called accidentally as we need the object's SetOnceCredential, which is returned only once. Still, the var can be reassigned, provided the caller has the original credential. This works with AnyVals and AnyRefs. The implicit conversion allows me to use the variable name directly in many circumstances, and if this doesn't work, I can explicitly convert it by appending ().

Typical usage would be as follows:

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
}

This doesn't yield a compile-time error if at some other point in the same file, I try getting another credential and reassigning the variable (as in calledAfterInit), but fails at run-time.

Community
  • 1
  • 1
Jean-Philippe Pellet
  • 59,296
  • 21
  • 173
  • 234
  • 1
    But as I see it now, you could always retrieve the `SetOne.allowMe` credential anywhere in your code (as compared to the `sealed` version) and assigning would work as long as it’s the first assignment. So basically, the implicit credential is useless now. Or am I missing something? – Debilski Dec 10 '10 at 10:40
  • Assignment would not work from anywhere in my code, as `mgr` is private, but you're right, this is a small downside. But is this really worse than axel22's version, where I could create another `Access` anywhere in the same file? (I *could* move `Base` to another file to get the 100% guarantee, but wouldn't this be overkill just for the initialization of one field? I'd rather keep it in the same file and get a better overview). Second, I prefer the easily reusable `SetOnce` as opposed to a more verbose solution that would require me to redeclare new access traits for each new set-once var... – Jean-Philippe Pellet Dec 10 '10 at 13:47
  • It’s not better or worse it’s just that the credential is useless in your case because you can call `SetOnce.allowMe` from anywhere in your code and thus get the credential variable. You don’t actually gain anything by that. – Debilski Dec 10 '10 at 14:14
2

I assume you don't need to do this efficiently with primitives, and for simplicity that you also don't need to store null (but you can of course modify the idea if these assumptions are false):

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
}

and just use this class wherever you need that functionality. (Maybe you would prefer apply() to get?)

If you really want it to look just like a variable (or method) access with no extra tricks, make the SetOnce private, and

private val unsetHolder = new SetOnce[String]
def unsetVar = unsetHolder.get
// Fill in unsetHolder somewhere private....
Rex Kerr
  • 166,841
  • 26
  • 322
  • 407
  • Any reason you’re defaulting to `null: A` and not to `None: Option[A]`? – Debilski Dec 10 '10 at 08:32
  • @Debilski - The `null` is not visible to the world, and has less computational overhead than `Option`. This is something that is low-level enough that it _could_ get heavily used, and it takes about the same amount of effort to write it more efficiently. But if one wanted to store `null` of course one would do something else (though I would pick a private boolean instead of `Option` again because of less overhead). – Rex Kerr Dec 10 '10 at 17:07
2

Not really the nicest way and not really what you asked for but it gives you some encapsulation of access:

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)
    }
  }
}
Debilski
  • 66,976
  • 12
  • 110
  • 133
  • +1 for the fact that this solution creates only one extra object even if I have several such “assign-once” variables. Reassigning _init.mgr is still possible, but definitely looks “wrong enough” in client code. – Jean-Philippe Pellet Dec 10 '10 at 08:15
  • Reassigning is not possible in client code because `_init` is visible only inside `AppProperties` and `def mgr` is unchangeable. – Debilski Dec 10 '10 at 08:21
  • Right, but for that, a simple `private var` would do. I was interested in preventing it in the rest of the implementation of `AppProperties`. I guess I shouldn't have written “client code.” – Jean-Philippe Pellet Dec 10 '10 at 09:01
2

Looking at JPP’s post I have made another variation:

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

This time, we are perfectly allowed to assign the var multiple times but we need to have the correct credential in scope. The credential is created and returned on first assign which is why we need to save it immediately to implicit val credential = mgr := new FileManager(config). If the credential is incorrect, it won’t work.

(Note that the implicit credential does not work if there are more credentials in scope because they’ll have the same type. It might be possible to work around this but I’m not sure at the moment.)

Community
  • 1
  • 1
Debilski
  • 66,976
  • 12
  • 110
  • 133
  • 1
    Wouldn't it be possible to move the `SetOnceCredential` class from the `SetOnce` object to the `SetOnce` class to avoid the multiple-credentials problem you mention? It seems to be that we could even get rid of the cached credential in `key`, then. I like the idea, but now the compile-time guarantee is weakened, as any assignment would compile (with the default null value) but fail at runtime. – Jean-Philippe Pellet Dec 10 '10 at 14:00
  • You’re right. Moving it inside the class fixes this of course. – Debilski Dec 10 '10 at 14:28
  • Very nice! The last thing that I don't like is that your last line, `mgr1 := new FileManager(new Config)`, although it will fail at run-time, still compiles, not indicating that it actually requires a credential. – Jean-Philippe Pellet Dec 10 '10 at 14:30
  • Yeah, that’s a tough one. I’m not sure we can fix that. One might think about a read-once variable for the credential and make the implicit for `:=` compulsory. But still, one would be able to access the read-once method for retrieving the credential and thus the type system would not complain, even though `null` is being returned instead of a credential. – Debilski Dec 10 '10 at 14:37
0

I was thinking something like:

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 can be set exactly once.

Michael Lorton
  • 43,060
  • 26
  • 103
  • 144
0

It’s not exactly the same thing but in many cases, the solution for this ‘set the variable once and then keep using it’ is simple subclassing with or without a special factory method.

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(...)
}
Debilski
  • 66,976
  • 12
  • 110
  • 133
0

You can always move that value to another object, initialize it only once and access it when needed.

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
    }
}
OscarRyz
  • 196,001
  • 113
  • 385
  • 569