How to auto apply init block after applying a func...
# announcements
s
How to auto apply init block after applying a functional block ? Take this code for example :
Copy code
class Config(var degree: Double = 315.0) {
  init {
    require(degree > 180 && degree < 360)
    { "degree should between 180 and 360" }
  }
}
And if we initialize
Config
directly , it will check the value as expected :
Copy code
@Test
fun normalTest() {
  try {
    Config(90.0)
    fail()
  } catch (e: Exception) {
    assertTrue(true)
  }
}
But if there is a function accepting the
Config
as functional object :
Copy code
private fun calculate(block: Config.() -> Unit = {}) {
  val cfg = Config().apply(block)
  println("do something... with degree = ${cfg.degree}")
}

@Test
fun testCalculate() {
  try {
    calculate {
      degree = 90.0
    }
    fail("It SHOULD fail")
  } catch (e: Exception) {
    assertTrue(true)
  }
}
It will skip the
init
check block . How to solve this problem ? Thanks. I could only think of this solution :
Copy code
class Config(var degree: Double = 315.0) {
  init {
    check()
  }

  fun check() {
    require(degree > 180 && degree < 360)
    { "degree should between 180 and 360" }
  }
}
and …
Copy code
private fun calculate(block: Config.() -> Unit = {}) {
  val cfg = Config().apply(block).also { it.check() }
  println("do something... with degree = ${cfg.degree}")
}
But I think it is so ugly … Is there any better way ? Thanks.
a
You could use a value class for the
degree
property, in which you put the
require
in its
init
function like you did
r
init
is called when the object is constructed. When you call
Config().apply(block)
, the first thing that happens is
Config()
, with your default value for
degree
of
315.0
. So the
init
is called, and passes, because 315 is between 180 and 360. You then set
degree
to
90
- but you have no guard on changing
degree
. Do you really want
Config
to be mutable? If so, and you want to keep the invariant that
degree > 180 && degree < 360
, you need to add it to `degree`’s setter.
s
to @Ayfri: value class may not be an option , as there may be more properties in the
Config
class. to @Rob Elliot:
Config
needs to be mutable , so that client of the function may call
calculate
like this
Copy code
calculate {
  degree = 90.0
}
r
What’s wrong with
Config(degree = 90.0)
and make
degree
a
val
?
s
Because there may be nested config , And I want to achieve something like
ktor
's config (ex : https://ktor.io/docs/http-client-engines.html#java ) such as :
Copy code
calculate (obj1 , obj2 ) {
  degree = 90.0
  angle { 
    value = 60.0
  }
}
r
I think this meets your requirements:
Copy code
class Config(
   degree: Double = 315.0
) {
   var degree: Double = degree
      set(value) {
         require(value > 180 && value < 360)
         field = value
      }

   init {
      this.degree = degree
   }
}
s
to @Rob Elliot , wow , thanks , I think it works . but a little ... cumbersome ... Is there any proposal to enforce such init checking after applying a block ?
r
No. Init happens on construction.
apply
is an extension function you call after the object has been constructed. Basically you pass the constructed Config instance as an argument to
apply
, so it absolutely has to already have been constructed.
The only way you can run logic when you reassign a field is in a custom setter for that field.
s
Thank you very much 🙌
r
Config().apply { degree = 90 }
is exactly the same as:
Copy code
val config = Config()
config.degree = 90
a
Maybe they can overwrite the
apply
function ?
e
Copy code
class ConfigBuilder {
    var degree: Double = 315.0
}
class Config(val degree: Double)
fun Config(block: ConfigBuilder.() -> Unit): Config {
    val builder = ConfigBuilder()
    builder.block()
    return Config(builder.degrees)
}
then you can
Copy code
Config { degree = 90.0 }
and a check in Config.init will happen after the block runs
1
s
Thanks @ephemient: this is another beautiful solution. and it seems widely used in kotlin related libraries (ex : ktor) .