Youssef Shoaib [MOD]
08/22/2021, 2:01 PMtransparent
modifier to it so that, whenever you have a value of that value class, it acts like if it is of the aforementioned type, but you may still access the functions that the value class itself provides. In a way, this is just a way to provide scoped extension functions, which I admit is practically solved by multiple receivers already, but there's some use-cases that don't quite fit that bill. Let me first just start with a simple example that is solved by context receivers:
transparent value class NumericalString private constructor(val underlying: String) {
constructor(value: Int): this(value.toString())
... etc for the rest of the number types
val doubled: String get() = "${underlying.toDouble() * 2}"
}
fun main() {
val fortyTwo = NumericalString(42)
val four = fortyTwo.doubled.lastLetter()
println(four + fortyTwo)
}
fun String.lastLetter() = last()
Re-writing this with context receivers is messy-ish, but it's kind of doable:
interface NumericalStringOperations {
val String.doubled: String
companion object Default: NumericalStringOperations {
override val String.doubled: String get() = this.toDoubleOrNull()?.let { "${it * 2}" } ?: this
}
}
fun main() {
with NumbericalStringOperations:
val fortyTwo = "42"
val four = fortyTwo.doubled.lastLetter()
println(four + fortyTwo)
}
fun String.lastLetter() = last()
The issue is, however, that first of all this is not typesafe, since it has no idea if a String is numerical or not. The second issue is a bit niche, which is that what if you want to shadow a function that already exists:
transparent value class NumericalString private constructor(val underlying: String) {
constructor(value: Int): this(value.toString())
... etc for the rest of the number types
val doubled: String get() = "${underlying.toDouble() * 2}"
// Kind of assuming that no boxing will really happen and that the compiler is smart enough to not force `other` to be boxed
// The point of this is that "42" and "42.0" should be considered equal
override inline fun equals(other: Any?) = if(other is NumericalString) other.underlying.toDouble() == underlying.toDouble() else underlying == other.underlying
fun last(): NumericalString = NumericalStirng(underlying.last())
}
fun main() {
val fortyTwo = NumericalString(42)
val four = fortyTwo.doubled.last()
val doubleFour = NumericalString(4.0)
println("$four == $doubleFour returned ${four == doubleFour}") // Should be true
}
In fact, this absolutely can't be done with context receivers (right now) because @HidesMembers
is limited in its usage to a handful of functions, and even if you do hack around the compiler to allow it to work everywhere (which I did try), it still doesn't resolve the shadowing correctly because call resolution with @HidesMembers
only takes into account normal static extensions and not member extensions. Even if @HidesMembers
was extended properly to handle this, I still believe that making it a first-class citizen in Kotlin gives it more validity, if you will.
So far, the case I've mentioned is basically just a refined type, but this feature can extend beyond that. For example, let's take a detour into functional programming with a simple Either<A,B>
perhaps like this:Fudge
08/23/2021, 7:56 PMwith:
syntax?Youssef Shoaib [MOD]
08/23/2021, 8:05 PMIt is possible to gradually turninto a hard keyword to support even more concise syntax for bringing annonymous receivers into the scope in a class:with
class Service {
with createServiceContext() // Introduce anonymous scope property
}
And, potentially, the same for the local scope (expanding on Algebraic effects and coeffects example):
fun helloToConsole() {
with Emit { msg -> println(msg) }
hello()
}