Let's say you have this class.
class Conduit<U : Units>(
val flowRate: Measure<UnitsRatio<U, Time>>,
val inputs: List<Conduit<U>> = emptyList()
) {
fun test(): Measure<UnitsRatio<U, Time>> {
return inputs.sumOf { it.flowRate }
}
}
It models a conduit for transferring different physical values depending on U
. If you wanted to sum up the total flow rate of all the inputs, it seems reasonable to have a call like the one in the test()
function.
This is how I implemented the sumOf
function.
val SUnits: Units? = null
inline fun <T, reified U : Units> Collection<T>.sumOf(selector: (T) -> Measure<U>): Measure<U> {
var sum: Measure<U> = SUnits.zero()
for (element in this) {
sum = selector(element)
}
return sum
}
The problematic part starts here, where we need to get the zero starting value for our sum. Because the measurement unit is generic, the actual type needs to be checked in order to assign the correct unit. That is the purpose of the zero()
function defined below.
inline fun <reified U : Units> Units?.zero(): Measure<U> {
return when (typeOf<U>()) {
typeOf<Energy>() -> 0.0 * (joules as U)
typeOf<Volume>() -> 0.0 * (liters as U)
typeOf<Time>() -> 0.0 * (seconds as U)
typeOf<UnitsRatio<Energy, Time>>() -> 0.0 * (watts as U)
typeOf<UnitsRatio<Volume, Time>>() -> 0.0 * ((liters / seconds) as U)
else -> throw RuntimeException()
}
}
It checks the type of U
and assigns the appropriate unit for it. However, when I test the following code, the type doesn't get recognized and my RuntimeException gets thrown.
Conduit(
maxFlowRate = 1 * watts, // type: Measure<UnitsRatio<Energy, Time>>
inputs = listOf(
Conduit(maxFlowRate = 1 * watts),
Conduit(maxFlowRate = 1 * watts)
)
).test().also { println(it) }
I beleive this is due to type erasure which I have a hard time grasping. Whatever the case, I'm hoping there is a way to pass along that information so that the sum accumulator can be initialized.
The library used in this example is Measured.
CodePudding user response:
Why is your zero()
function an extension on a Units object, but doesn't use the receiver for anything? If your goal was to use the receiver as the same type as the Measure, your function could simply be:
fun <U: Units> U.zero(): Measure<U> = Measure(0.0, this)
No reification or type-checking necessary for that.
But I guess the reason you need runtime checking is to be able to produce a 0.0 value based only on a generic type, not from an instance of Units. So remove the receiver from your function:
inline fun <reified U : Units> zero(): Measure<U> {
return when (typeOf<U>()) {
//...
}
}
Your sumOf
function doesn't make sense because it has a U
Units type, but it tries to use whatever is stored in the SUnits
property, which might not be a match for U
as requested by the function caller. I think it should probably look like the following.
inline fun <T, reified U : Units> Iterable<T>.sumOf(selector: (T) -> Measure<U>): Measure<U> {
var sum: Measure<U> = zero<U>()
for (element in this) {
sum = selector(element)
}
return sum
}
or more simply:
inline fun <T, reified U : Units> Iterable<T>.sumOf(selector: (T) -> Measure<U>): Measure<U> =
fold(zero<U>()) { acc, value -> acc selector(value) }
If you can accept getting null
as your return value when the collection is empty, you don't need the zero function at all. You just need:
fun <T, U : Units> Iterable<T>.sumOfOrNull(selector: (T) -> Measure<U>): Measure<U>? =
map(selector).reduceOrNull { a, b -> a b }
CodePudding user response:
I don't know what you're trying to do with SUnits
, but it's definitely playing a role in breaking your code.
Start by making it inline fun <reified U : Units> zero(): Measure<U>
and just write var sum = zero<U>()
. Delete SUnits
entirely. I don't know if there are any other problems, but you definitely need to do that.