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

Viewed 359

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

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

I think your Wrap class is a good starting point to implement this. I would definitely make it a property delegate and touch() could be much simplified:

fun touch() {
    synchronized(this) {
        check(value == null) { "Duplicate init" }
        value = supp()
    }
}

Then you can remove lock. But generally, this is a good approach.

If you would like to reuse lazy util from stdlib then you can do this by wrapping it with another object which does not read its value until asked:

class ManualLazy<T : Any>(private val lazy: Lazy<T>) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): T? {
        return if (lazy.isInitialized()) lazy.value else null
    }

    fun touch() {
        lazy.value
    }
}

class Item {
    private val _startTime = ManualLazy(lazy { Instant.now() })
    val startTime: Instant? by _startTime

    fun start(){
        _startTime.touch()
    }
}

Of course, depending on your needs you can implement it in a much different way, using a similar technique.

This may be considered exploiting or hacking lazy util. I agree and I think Wrap approach is a better one.

Related