Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to simulate an “assign-once” var in Scala?

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?

like image 357
Jean-Philippe Pellet Avatar asked Dec 09 '10 23:12

Jean-Philippe Pellet


3 Answers

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

}
like image 112
axel22 Avatar answered Sep 28 '22 07:09

axel22


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.

like image 36
Jean-Philippe Pellet Avatar answered Sep 28 '22 08:09

Jean-Philippe Pellet


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....
like image 26
Rex Kerr Avatar answered Sep 28 '22 09:09

Rex Kerr