Question about `context` (Context Receivers)

Hi! I hope you don’t mind, I was looking into your code in github :slight_smile:

I enjoy reading code and seeing how different people write things and learn from it. There’s one thing I can’t figure out:

What is context? I can’t find it in the Kotlin manual, I don’t see imports for it in your code… I’m curious :slight_smile:

I have a disorganized mess of programs at openrndr-template/src/main/kotlin at master · hamoid/openrndr-template · GitHub :slight_smile:

Cheers!

Hey Abe,

Thanks for getting in touch! Good that you ask, it’s actually quite a useful Kotlin feature when writing OPENRNDR sketches. I’m happy to share how I use it!

This feature was added in Kotlin 1.6.20 as a prototype so needs to be explicitly enabled in the build file

tasks.withType<KotlinCompile> {
    kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
}

It allows us to add another context to a function. In other words, it adds another this to the function. So, besides the implicit this receiver from the containing class, there is another this coming from the context we provided. Note that this. can be omitted in unambiguous cases, which is still the case for both.

For example, we can extract drawing logic outside of the OPENRNDR program block into another class. The following code makes drawer as well everything else from Program available in the draw() function.

class Figure(
    val shapeContour: ShapeContour
) {

    context(Program)
    fun draw() {
        drawer.strokeWeight = 2.0
        drawer.stroke = ColorRGBa.DARK_ORCHID
        drawer.contour(shapeContour)
    }

All we have to do then is to call the draw function from the scope where an instance of Program is available:

program {
    val figure = Figure(contour { ... })

    // We are in the context of Program in this block
    // The instance of Program will be passed to draw function as a context receiver
    figure.draw()
}

This example is a bit silly, we could as well just pass drawer to the figure.draw() function. It still helps to reduce the number of arguments which makes the code more readable.

Another way to use it is to combine it with Kotlin’s with function:

// In a separate class/file
class AnotherClass {
    context(Drawer)
    fun draw() {
        clear(ColorRGBa.BLACK)
        isolated {
            translate(bounds.center)
            circles(...)
        }
    }
}
------------------------------------
// In OPENRNDR application
program {
    with(drawer) {
        anotherClass.draw()
    }
}

Notice that in this example, we don’t need to write drawer. before all function calls because an instance of Drawer (as well as an instance of AnotherClass) are both available as this inside that function. This helps to reduce boilerplate code even further.

If there is a conflict (a function is called that exists in both context receivers), the local class has priority. We can also specify which receiver we want to use:

this@Drawer.contour(...)

You can find a lot more detail about it here:

or just search for “Kotlin Context Receivers”

I like to structure my code in classes, so I use this to extract drawing logic to different classes and files outside the program or extend blocks of OPENRNDR application without the need to pass the context through multiple layers as arguments.

I’m still at the very beginning of my journey with OPENRNDR, so the code you found is quite basic and not very well structured (these Faces were actually my very first OPENRNDR sketch! :grin:). But given OPENRNDR makes a great use of Kotlin DSL, the feature can come in handy in various situations.

Happy this helped you understand it a bit.
Let me know if you have any more questions or comments :slight_smile:

2 Likes

Oh wow! What a super detailed answer! :slight_smile: Thank you! I remember reading about context receivers but never actually tried them. Would it be worth it to make this thread public so others can learn from your great answer? Or do you prefer to keep it private? Cheers!

1 Like

Sure thing, I was thinking the same when I finished writing it!
Let me post it publicly :slight_smile: