Playing with types. How can I make the default pr...
# getting-started
p
Playing with types. How can I make the default prepare to a lambda returning Unit, making the default type of U to Unit? https://pl.kotl.in/c1FbX3GNm
j
You can't, because
Rule<String, Int>(String::class, "strRule")
wouldn't be valid if
prepare
returned
Unit
What you can do instead is declare a
Rule
subclass that has only one type parameter (not
U
), and that provides a default
prepare
.
p
By default I need no additional info, that's why should the prepare lambda return Unit
So I cannot solve this without the need to explicitly state: I create a Rule without "prepare"?
j
You can also add a convenience factory function named
Rule
with a single type parameter and a default
prepare
(you don't actually need a new type):
Copy code
fun <T: Any> Rule(
    clazz: KClass<T>,
    name: String,
    prepare: T.() -> Unit = {},
    whenTrue: T.() -> Boolean = { true },
    thenDo: T.() -> Unit = {},
): Rule<T, Unit> = Rule(clazz, name, prepare, whenTrue, thenDo)
(Style-wise, it's generally accepted to have a capitalized name for a factory function)
👍 1
You can even make this function
inline
with
reified T
and you won't need the
clazz
parameter anymore.
🤯 1
p
Wow, and you still can use type parameter if you need to:
Copy code
val rule = Rule<String>("string rule", action = { trigger.trigger() })
j
Yes, the type parameter is a "more convenient" way to specify the class on the call site. It probably cannot be inferred from those lambdas in your case.
p
Hm, but this does not compiles:
Copy code
val rule = Rule<Int>("int rule", prepare = { this * 2 })
j
If you want to use
prepare
, it means you need to use the real constructor (or declare another factory function overload with 2 type parameters).
Using the real constructor means calling it like this:
Copy code
val rule = Rule(Int:: class, "int rule", prepare = { this * 2 })
Using another overload means adding this:
Copy code
inline fun <reified T : Any, U : Any> Rule(
    name: String,
    noinline prepare: T.() -> U,
    noinline condition: T.(U) -> Boolean = { true },
    noinline action: T.(U) -> Unit = {},
): Rule<T, U> = Rule<T, U>(T::class, name, prepare, condition, action)
And then creating the rule like this:
Copy code
val rule = Rule<Int, Int>("int rule", prepare = { this * 2 })
p
Thanks, amazing but almost too high for me. I let the IDE insert the nolinline word, but what does it do exactly?
j
Here we're using
inline
on this function for the sole purpose of getting a
reified T
(so we can grab its class without the need for the caller to pass it). However, the compiler doesn't know that it's "just for this". It believes you want to inline everything, including the lambda parameters. That's a problem for you, because you need to pass those lambdas around (and store them in your
Rule
class as properties), so they cannot be inlined in the call site. This is what
noinline
is for. You basically tell the compiler that you don't need/want to
inline
the lambda parameters of the function.
Concretely, from the perspective of the caller, this will forbid stuff that's only possible when a lambda is inlined. For instance, using non-local returns, calling
suspend
functions, etc. But in your case it's ok to restrict this, non-local returns wouldn't make sense anyway.
p
Can the compiler infer the U from the
this*2
expression? so type just:
Copy code
val rule = Rule<Int>("int rule", prepare = { this * 2 })
j
This syntax is not valid, but I believe in Kotlin 1.9.? some inference was added. You can try using the
_
placeholder for the second type argument:
Copy code
Rule<Int, _>("int rule", prepare = { this * 2 })
This didn't work for me on the playground, but it might be a playground bug
p
Yes, it works! 👍