Hey guys, I'm trying to understand something in th...
# javascript
s
Hey guys, I'm trying to understand something in the Kotlin-React code (for my comprehension), and I'm kind of getting mad here, please help me understand this fracking magic!!! So, in
ChildrenBuilder
(https://github.com/JetBrains/kotlin-wrappers/blob/master/kotlin-react/src/main/kotlin/react/ChildrenBuilder.kt), the
ElementType<P>.invoke
function declares the generic bounds
where P : Props, P : ChildrenBuilder
. I can call this function inside a
ChildrenBuilder
with, for example,
ReactHTML.div.invoke {}
(here I put the
invoke
to be explicit, even though it is not required). Now,
ReactHTML.div
is an
IntrinsicType<HTMLAttributes<HTMLDivElement>>
, which is also an
ElementType<HTMLAttributes<HTMLDivElement>>
, so the resolution kind of makes sense... BUT I cannot find anywhere
HTMLAttributes
implements
ChildrenBuilder
, which should break the resolution of the
ElementType<P>.invoke
function. The
HTMLAttributes
interface implements
AriaAttributes
,
DOMAttributes
,
PropsWithRef
,
PropsWithChildren
,
PropsWithClassName
,
PropsWithStyle
, and
Props
. How the hell can the Kotlin Compiler agrees that the
where
clause of the
ElementType<P>.invoke
function is satisfied ?!?!? Furthermore, IntelliJ shows the
invoke
"this" hint as
ChildrenBuilder & HTMLAttributes<HTMLDivElement> & Props
, when, according to the
invoke
function signature, it should simply display
HTMLAttributes<HTMLDivElement>
. Where is the
ChildrenBuilder
coming from ? What black magic is that ?!? Oh, and when I ask IntelliJ to explicit the type argument to
invoke
, it can't! It puts
Any
, which clearly breaks the
where
clause. If I try to set myself the type argument with
div.invoke<HTMLAttributes<HTMLDivElement>>
, then IntelliJ tells me that "Type argument is not within its bounds", which I fully agree with! Help me, please, I'm losing it 🙃 !
😜 1
s
It is fully correct behavior, it is how contravariance works. Here is playground with explanation: https://pl.kotl.in/5sOezk7mu
ElementType
is contravariant by
P
generic because props is input
s
So, what is
P
?
s
P
is generic to represent props
Copy code
external interface ElementType<in P : Props>
s
Yeah, but in my example, what is it ? It can't be
HTMLAttributes<HTMLDivElement>
...
s
yea, sure
good example of
ElementType
is
FunctionComponent
,
FunctionComponent
is opaque alias to
(Props) -> ReactNode?
so as you may heard all functions are covariant by return type and contravariant by parameters
s
opaque alias ?
s
Yea, in Typescript
FunctionComponent
is declared approximately this way
Copy code
type FC<P> = (props: P) -> ReactNode
In Wrappers we hide the fact that
FunctionComponent
can be invoked, but the rest contract is the same
s
Let's get back to my question "What is `P`". As I said, it cannot be
HTMLAttributes<HTMLDivElement>
(since it does not implement
ChildrenBuilder
, and IDEA can't explicit the type argument to the
invoke
function, so what can it be ?
s
Ok, I didn't think that you meant some concrete
P
. Are you asking about this signature?
Copy code
operator fun <P> ElementType<P>.invoke(
        block: @ReactDsl P.() -> Unit,
    ) where P : Props,
            P : ChildrenBuilder
s
Yep
s
When you put there
this: IntrinsicType<HTMLAttributes<HTMLDivElement>>
your
P
is
HTMLAttributes<HTMLDivElement>
but because
ElementType
is contravariant by
P
, clause
where P : Props, P : ChildrenBuilder
it is not upper bound, it is lower bound, you can pass in
invoke
function any
ElementType
whose generic type is
Props & ChildrenBuilder
or more abstract, so you can pass there
ElementType<Props>
,
ElementType<ChildrenBuilder>
or
ElementType<HTMLAttributes<HTMLDivElement>>
BTW
Props & ChildrenBuilder
is subtype for
Props
and for
ChildrenBuilder
Lets discuss another example, do you have questions about how this code works: https://pl.kotl.in/hUmLsUw9e
Copy code
interface Parent
interface Child : Parent

typealias Task = (Child) -> Unit

fun doWork(task: Task) {
    task(object : Child {})
}

fun main() {
    val parentTask = { _: Parent -> }
    val childTask = { _: Child -> }
    
    doWork(childTask)
    doWork(parentTask)
}
s
@Sergei Grishchenko Sorry I signed off. Nope, the code looks perfectly fine
s
I have renamed every type to make analogy with wrappers
Copy code
interface Props
interface PropsAndChildrenBuilder : Props

typealias ElementType = (PropsAndChildrenBuilder) -> Unit

fun createElement(elementType: ElementType) {
    elementType(object : PropsAndChildrenBuilder {})
}

fun main() {
    val regularFC = { _: Props -> }
    val syntaticFC = { _: PropsAndChildrenBuilder -> }
    
    createElement(regularFC)
    createElement(syntaticFC)
}
It is simplification of course but I hope you can see what is mechanic behind the scene
so at the first sight it seems that
createElement
receives only strange
ElementType
with
PropsAndChildrenBuilder
arg, but in fact when you pass
ElementType<Props>
it is also fine for
createElement
The same happens when you pass
IntrinsicType<HTMLAttributes<HTMLDivElement>>
to
invoke
, it seems than
invoke
receives only something like that
IntrinsicType<HTMLAttributes<HTMLDivElement> & ChildrenBuilder>
but in fact
IntrinsicType<HTMLAttributes<HTMLDivElement>>
is also ok for
invoke
param
s
🤯
s
Sorry if my explanation was too complicated, I did my best 🙂
s
OK, I get it. It works because lambda parameters are contravariant.
1
No, that makes perfect sense with lambdas !
Thanks a lot! It really advanced my comprehension 🙂
s
I don want to be too boring but I want explain that not only lambdas can be contravarian, e.g.
(T) -> R
in Kotlin refers to internal type
interface Function1<in T, out R>
, so you can declare you own types with contravariance, so even
ElementType<in P: Props>
is not lambda type, it is contravarint by it's
P
generic, you can treat
ElementType
as "something abstract that receives props
P
as input"
👍 1