Skip to content

Conversation

@corlaez
Copy link

@corlaez corlaez commented Feb 1, 2026

UPDATE: I am not so bullish about the claim that this advances DCI that much. I began embracing contextOf and it avoids naming the variable at all which it is a very ceremonious process. I do think it would be more intuitive and make it sit right in within the Kotlin language, but I am not so bullish about this.

Dear @serras

I would like to make the case that adding extra context functions (that also add arguments to the block lambda) is a good idea that would make this feature feel more natural in the Kotlin language and reduce the ceremony and noise associated with trying to create context scoped context-parameters.

Possible now, Proposal enables: Ceremony, noise and path of least resistance

fun main() {
    class ContextObject
    
    // Possible now
    val contextObject = ContextObject()
    context(contextObject) {
        // contextObject can be used
    }
    // !!! context Object has leaked outside its scope.

    // Possible now
    context(ContextObject()) {
        val contextObject = contextOf<ContextObject>()
        // contextObject can be used but not without noise and ceremony
    }
    // context object was scoped only to it's context but not without noise and ceremony


    // PROPOSAL enables: (order based resolution)
    context(ContextObject()) {
        contextObject ->
        // contextObject can be used
    }
    // context object was scoped only to it's context.

    // PROPOSAL enables (order based resolution with a clean type check):
    context(ContextObject()) {
        contextObject: ContextObject ->
        // contextObject can be used
    }
    // context object was scoped only to it's context.
}

Let's take in consideration that the path of least resistance available is to leak the context, at least if you want to use it inside the lambda directly (without calling another function that requires and names the context).

With the changes proposed, even in this example with a single object, the option that requires less characters (and therefore could be considered the path of least resistance) becomes the one doing contextObject ->. It is a clear winner with less character and ceremony and no function calls to contextOf. It feels and acts more integrated to the language.

And while the second option in terms of character count is actually the one that leaks the context, if you were trying to scope the context and already went for the path of least resistance I would argue that at that point it is only natural to add the type safety in the lambda argument. So in this way I would consider contextObject: ContextObject -> the next natural choice (despite higher char count to the leaky version).

Ok, that was a very simple example to explain the notions of noise and ceremony and the practices it encourages (leaking objects outside of the scope).

DCI example with 2 parameters and an algorithm with 2 sequential contexts

This is a DCI (Data Context Interaction) example in Kotlin:

/** DCI Context (with Infrastructure injected in the constructor) */
class CreateNewUser(private val userRepo: UserRepoEffect, private val crypto: CryptoEffect) {
   // DCI Roles ...

    fun execute(username: String, password: ByteArray): CreateUserResult {
        val hashedPassword = context(NewUsername(username), NewPassword(password)) {
         // newUserName, newPassword ->
            newUserName: NewUsername, newPassword: NewPassword ->
         // val newUserName: context<NewUsername>(); val newPassword: contextOf<NewPassword>();
            try {
                newUserName.validate().onFailureHalt { return it }
                newPassword.validate().onFailureHalt { return it }
                val byteArray = newPassword.`to secure password hash`()
                byteArray
            }
            finally { `wipe sensitive password data`() }
        }
        context(NewUsername(username), EncryptedPassword(hashedPassword)) {// uses the existing context call
            try { userRepo.`persist new user`() }
            finally { `wipe sensitive encripted password data`() }
        }
        return Success
    }
   // ...
}

Right now we are limited to the noisiest option. Even with two objects, it is starting to add a lot of clutter. One way to avoid the contextOf calls and that noise, is to create additional functions for each stage.

However, we have lost the big picture, the clarity. We can not longer see the complete algorithm across multiple contexts:

    fun execute(username: String, password: ByteArray): CreateUserResult {
       // Clean but, I have lost the global picture, I now have to hunt down different functions 
       // and piece the process together in my head, rather than having it directly in the code.
        val hashedPassword = this.`Interaction - validate and hash password`()
        this.`Interaction - persist user()
        return Success
    }

I know perhaps there is a strict budget of how many functions we want to generate, and we may be tempted to implement the minimum and let library authors write layers on top. However, I truly think that the ability to name the context arguments in the lambda feels very natural and something that should truly be supported at a language level.

What do you think?

PS: The proposal is to add and not to replace existing functions because that would force functions that do not need these lambda arguments to declare them adding a tiny bit of ceremony but to those who don't really need any and already don't have any.

If the existing context calls were replaced by the proposal ones then _, _ -> becomes necessary for as many objects as you inject:

        withRoles(NewUsername(username), EncryptedPassword(hashedPassword)) { _, _ ->
            try { userRepo.`persist new user`() }
            finally { `wipe sensitive encripted password data`() }
        }

Also I don't know if that is even technically possible since the new generated functions with arguments rely on the argument-less to do their job.

Add an extra context function with arguments.
Add singular plural variation
@corlaez corlaez changed the title Context proposal Context - Parameters: passing arguments proposal Feb 1, 2026
@corlaez corlaez changed the title Context - Parameters: passing arguments proposal Context - Parameters: lambda with arguments Feb 1, 2026
@corlaez
Copy link
Author

corlaez commented Feb 1, 2026

There is noise associated with val naming = contextOf<T>() but what I haven't really accounted for is the naming fatigue that this generates, even for the proposal of arguments I made. If you want to use it you must name it.

Taking in consideration contextOf is inlined and therefore free, suddently getting used to using it liberaly might actually result in more clarity. Sure there is still some ceremony involed, but it COMPLETELY removes having to name all this roles, that we already guarantee there is only one of each in this context.

My code example, reimplemented embracing contextOf:

/** DCI Context (with Infrastructure injected in the constructor) */
class CreateNewUser(private val userRepo: UserRepoEffect, private val crypto: CryptoEffect) {
   // DCI Roles ...

    fun execute(username: String, password: ByteArray): CreateUserResult {
        val passwordHash = context(NewUsername(username), NewPassword(password)) {
            try {
                contextOf<NewUsername>().validate().onFailureHalt { return it }
                contextOf<NewPassword>().validate().onFailureHalt { return it }
                contextOf<NewPassword>().`to secure password hash`()

            } finally {
                contextOf<NewPassword>().`wipe sensitive password data`()
            }
        }
        context(NewUsername(username), EncryptedPassword(passwordHash)) {
            try {
                userRepo.`persist new user`().let { return Success }
            } finally {
                contextOf<EncryptedPassword>().`wipe sensitive encripted password data`()
            }
        }
    }

I no longer think it is as critical to push DCI forward. I do think that the proposal may help make the construct feel part of the language and still eases the naming process if it is so desired.

I will leave it up to you to decide. Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant