Hello, guys. I saw someone from 47deg who used `wi...
# arrow
r
Hello, guys. I saw someone from 47deg who used
with(x,y){...}
syntax for better context receivers experience. And that was custom function. How can I implement that multiple
with()
?
s
r
Thank you, Simon. That was you! 😄
t
That's interesting. What is the purpose of
TypePlacedHolder
as a param of the block function?
s
By-pass a compiler bug 🤣 I assume that the compiler goes down a different code branch when the lambda has non-empty parameters, and the bug is not present in that code branch. (Can also be IntellIJ Kotlin plugin, I cannot remember for sure). Anyhow, without that placeholder I think it placed one of the
context(A, B)
into the first parameter of the lambda. So
context(A, B) () -> C
resulted in
context(A) (B) -> C
but it's been some time so I don't remember the details anymore.
I guess that it’s the same that the one shared by Simon
s
Yes, but only arity-2 & 3.
t
Copy code
@OptIn(ExperimentalContracts::class)
@Suppress("SUBTYPING_BETWEEN_CONTEXT_RECEIVERS")
inline fun <A, B, R> with(a: A, b: B, f: context(A, B) () -> R): R {
    contract {
        callsInPlace(f, InvocationKind.EXACTLY_ONCE)
    }
    return f(a, b)
}

context (Int, String)
fun foo(){}

fun bar() = with(5, ""){
    foo()
}
this seems to work for me on
1.8.0
. But I did very limited testing, so I might just not have triggered the issue.
y
@than_ I came up with TypePlacedHolder as a workaround for that compiler bug long ago, and AFAIK I was the first one to share it as a workaround. TL;DR at the bottom Basically, it seemed like the compiler is completely unaware of type parameters of a lambda that are only mentioned in its contexts, and so the real issue was that A and B would sort of get erased and so the compiler wouldn't recognise that when you call the
with
function with a lambda that that lambda has those A&B contexts. My hunch that it was type parameter related came from the fact that if you used concrete types (say for instance
Int
or
List<String>
) the lambda's contexts would be recognised appropriately. I noticed, however, after messing around a bit, that if I had passed all the context parameters also as regular parameters, the context parameters would also get recognised. In other words, calling the function
inline fun <A> foo(lambda: context(A) (A) -> Unit)
with a lambda would result in the context of type A being recognised and usable inside the lambda. That meant that a multi-with function could be defined as
inline fun <A, B, R> with(a: A, b: B, f: context(A, B) (A, B) -> R): R
Upon seeing that, though, it felt a little ugly that all those arguments would need to be passed to the function twice. The magical thing is, it turned out that you only had to mention the last type parameter that was used amongst the context parameters. My guess is that mentioning that last type parameter triggered a compiler path where it saved the information for all the previous type parameters mentioned within the context parameters. Interestingly, if I only mentioned A as a normal parameter, then A would be recognised as a context, but B wouldn't. Weird compiler magic¬ Putting that all together, that meant that a multi-with could just be
inline fun <A, B, R> with(a: A, b: B, f: context(A, B) (B) -> R): R
The finishing touch was to use a trick I came up with a while ago, which is `TypeWrapper`s. Basically, a `TypeWrapper`(or
TypePlacedHolder
as Simon calls it) is an interface that has a type parameter that you want the compiler to move around as information. Using a type wrapper here just felt neater since you only need to pass a singleton and thus a user will see that the singleton can be safely ignored. Side note: TypeWrappers are quite versatile in that they just expose the type-inference that the compiler has and thus allow you to access and play with types and pass them around while relying on type-inference to do the heavy lifting. For instance, long ago I made a few utilities that allowed you to be able to partially specify type arguments when calling a function (which is now obsolete because we have the underscore operator that does the same thing). A use case for those utilities was creating intersection types, since you can't explicitly specify an intersection type, but you can call a method that has
A, B, C
as type parameters
where C : A, C : B
and it has a normal parameter of type
TypeWrapper<A, B>
thus allowing you to specify your A and B and C would be automatically inferred to
A&B
. TL;DR: compiler seemed to forget about type parameters that are used in a lambda's context, and so by mentioning the last such type parameter somewhere in its regular parameters, the compiler would retain the information about those type parameters and thus the contexts would be recognised inside the lambda. The bug is now fixed thankfully though!