Search code examples
kotlindesign-patternsdelegateslazy-evaluation

How to express in Kotlin "assign value exactly once on the first call"?


Looking for a natural Kotlin way to let startTime be initialized only in a particular place and exactly once.

The following naive implementation have two problems:

  1. it is not thread safe
  2. it does not express the fact "the variable was or will be assigned exactly once in the lifetime of an Item instance"

class Item {
    var startTime: Instant?

    fun start(){
        if (startTime == null){
            startTime = Instant.now()
        }

        // do stuff
    }
}

I believe some kind of a delegate could be applicable here. In other words this code needs something similar to a lazy variable, but without initialization on first read, instead it happens only after explicit call of "touching" method. Maybe the Wrap calls could give an idea of possible implementation.

class Wrap<T>(
  supp: () -> T
){
   private var value: T? = null
   private val lock = ReentrantLock()
  
   fun get(){
     return value
   }

   fun touch(){
      lock.lock()

      try{
          if (value == null){
              value = supp()
          } else {
              throw IllegalStateExecption("Duplicate init")
          }
      } finally{
        lock.unlock()
      }
   }
}

Solution

  • How about combining AtomicReference.compareAndSet with a custom backing field?

    You can use a private setter and make sure that the only place the class sets the value is from the start() method.

    class Item(val value: Int) {
        private val _startTime = AtomicReference(Instant.EPOCH)
        var startTime: Instant?
            get() = _startTime.get().takeIf { it != Instant.EPOCH }
            private set(value) = check(_startTime.compareAndSet(Instant.EPOCH, value)) { "Duplicate set" }
    
        fun start() {
            startTime = Instant.now()
        }
    
        override fun toString() = "$value: $startTime"
    }
    
    fun main() = runBlocking {
        val item1 = Item(1)
        val item2 = Item(2)
        println(Instant.now())
        launch { println(item1); item1.start(); println(item1) }
        launch { println(item1) }
        delay(1000)
        println(item2)
        item2.start()
        println(item2)
        println(item2)
        item2.start()
    }
    

    Example output:

    2021-07-14T08:20:27.546821Z
    1: null
    1: 2021-07-14T08:20:27.607365Z
    1: 2021-07-14T08:20:27.607365Z
    2: null
    2: 2021-07-14T08:20:28.584114Z
    2: 2021-07-14T08:20:28.584114Z
    Exception in thread "main" java.lang.IllegalStateException: Duplicate set