Shorthand for repeated also-apply operation

Hello,

I’m working on a metamodel transformation library written in Kotlin. There is a mapping with a bunch of specific built-in datatypes. The map is built on-line with a companion object.

// ...
       var UInt16Type = EcoreUtil.copy(library.UIntCodec).apply {
            // uint<16>
            this.modifiers.add(factory.createAssignment().also {
                it.value = factory.createIntLiteral().apply {this.value = 16}
                it.assignee = this.type.modifiers[0]
            })
        }
// ...

The code above is duplicated multiple types with different parameters (different codec name, different assignment value); in Rust, I’d use a macro instead of copying the code over and over again. I was wondering if there is perhaps some kind of shorthand for such repeated also-apply calls. Or perhaps some type of metaprogramming that roughly works out like Rust macros.

Thanks in advance.

Kotlin has a somewhat similar idea of using higher-order inline functions (i.e functions that take functions, just like apply)
So it depends exactly on which parts you want to be parameters. Making some assumptions, you can do this:

inline fun MyCodecInterface.toType(config: MyCopiedCodecInterface.(AssignmentType) -> Unit) = EcoreUtil.copy(this).apply {
  this.modifiers.add(factory.createAssignment().also(config)
}
// Usage
var UInt16Type = library.UIntCodec.toType {
  it.value = factory.createIntLiteral().apply { value = 16 }
  it.assignee = this.type.modifiers[0]
}
1 Like

How about a simple DSL function, since lambdas can inlined making them similar to Rust macros.

val UInt16Type = createType(someCodec) {
    addModifier(someAssignment) {
        value = createIntLiteral { value = 16 }
        // ...
    }
}

You could make a function that’s used like this or something similar depending on what you want. The inline createType takes a codec and a lambda, then maybe passes them to code similar to what you had.

inline fun <TCodec : Codec> createType(
    baseCodec: TCodec,
    build: CreateTypeScope<TCodec>.() -> Unit
) {
    return EcoreUtil.copy(baseCodec).apply { codec ->
        build(CreateTypeScope(codec))
    }
}

value class CreateTypeScope<TCodec : Codec>(
    private val codec: TCodec
) {
    inline fun <TModifier : Modifier> addModifier(
        modifier: TModifier,
        block: TModifier.() -> Unit = {} // Optional if you want
    ) {
        // ...
    }
}

And you don’t even need the custom scope class if you don’t want. The copied codec could be the scope too, and addModifier could be an extension on codecs for example.

I’m typing this on my phone so this is more of a rough example, but using higher order functions like this can be a nice way to abstract away boilerplate like this, and is pretty common in Kotlin especially in libraries. It’s also pretty rare for me to see factory/builder classes in code, with builder functions being a more commonly used approach. (e.g. Kotlin’s buildString vs. Java’s StringBuilder)

2 Likes

Okay, looks like my best bet is to factor out some of the code into an inline lambda and use that as a higher-level function. Thanks for both of your suggestions.

1 Like

One more question. The input parameter for config has Unit as its return type because it returns nothing (Unit = void)… or does it return itself (as in, { … } returns a bunch of instructions which is the body of the function), which is a Unit?

Unit is void yes, or more accurately Unit is just a singleton.
The type MyCopiedCodecInterface.(AssignmentType) -> Unit means a function that takes MyCopiedCodecInterface as a receiver (accessible as this) and takes AssignmentType as a parameter, and returns nothing of value Unit. Note that Kotlin also has Nothing, which is a value that can’t exist. It’s used in functions like `fun forever(block: () → Unit): Nothing { while(true) { block() } }