More concise "Either"?

Is there a more concise way to write the following, with less repetition? I.e. an interface for the common members of existing instances of two different classes.

FormParser.ParaContext and FormParser.SepContext are third-party classes (not interfaces), so I can extend them, but beyond that my options are limited, I think?

I was kinda hoping I wouldn’t have to enumerate the common members of a sealed class/interface. Or that I could delegate to a class. Or add an extension inner class.

sealed interface Either {
  class ParaContext(val ctx: FormParser.ParaContext) : Either {
    override fun ranges() = ctx.ranges()
    override fun rest() = ctx.rest()
    override fun SUBTOTAL() = ctx.SUBTOTAL()
    override fun instructions() = ctx.instructions()
    override val amount = ctx.amount
  }

  class SepContext(val ctx: FormParser.SepContext) : Either {
    override fun ranges() = ctx.ranges()
    override fun rest() = ctx.rest()
    override fun SUBTOTAL() = ctx.SUBTOTAL()
    override fun instructions() = ctx.instructions()
    override val amount = ctx.amount
  }

  fun ranges(): FormParser.RangesContext?
  fun rest(): FormParser.RestContext?
  fun SUBTOTAL(): TerminalNode?
  fun instructions(): FormParser.InstructionsContext
  val amount: Token?
}

Ideally, I think I’d like the following to work:

sealed interface Either {
  class ParaContext(ctx: FormParser.ParaContext) : FormParser.ParaContext by ctx, Either
  class SepContext(ctx: FormParser.SepContext) : FormParser.SepContext by ctx, Either
}

fun evalLine(ctx: Either) {
  val instructions = ctx.instructions()
  [...]

But I’d settle for a suggestion that works and is less repetitious than what I have :smile:

If your goal is to take a 3rd party class hierarchy and make a sealed class hierarchy (there’s some 3rd part base context or interface), then yeah.

If those 3rd part classes don’t have any common ancestor defining those common methods, then I don’t think there is a more concise way.

In case there is a 3rd party shared ancestor, consider defining the val in Either and narrowing the type in the implementations.

sealed interface Either {
  abstract val ctx: FormParser.BaseContext
  class ParaContext(override val ctx: FormParser.ParaContext) : Either
  class SepContext(override val ctx: FormParser.SepContext) : Either
}

This lets you access ctx as FormParser.BaseContext if just dealing with Either but if dealing with Either.ParaContext (even if safe casted) then ctx will be of type FormParser.ParaContext.

There are variations too. You could define methods in Either that depend on FormParser.BaseContext so you don’t need to access ctx property when using Either. You could also hide ctx property if you use a sealed class instead and make ctx protected and abstract in Either.

2 Likes

That’s my goal: effectively to create FormParser.BaseContext, or take a third-party class (un-)hierarchy and introduce a common ancestor/interface/trait, so I can run the same evalLine() logic on either instance.

Thanks for confirming that anything more concise would (currently) require a common ancestor, however I’d then use that ancestor directly if it existed:

fun evalLine(ctx: FormParser.BaseContext) {
  val instructions = ctx.instructions()
  [...]

I sealed the interface in the example only hoping I wouldn’t need to enumerate the common members.

Philosophically I’m dealing with two different issues I think:

  1. I was surprised there isn’t a concise way to express the common members of a set of types, i.e. “union type”.

    I found the following quote compelling on the one hand and principled at the expense of convenience on the other:

    Sounds like this does come up in practice but has already been thoroughly debated.

  2. When third-party types aren’t interfaces, there isn’t a concise way to extend an existing (un-)hierarchy with a type for the (explicit) common members (only interfaces can be delegated to).

    Maybe a use case for KT-21955 delegating to a class?

    Adding already-implemented types directly (without delegation/wrapper, i.e. “extension types”) has also been thoroughly discussed.

Thanks again for your help!

1 Like

Just FYI, your use-case will be idiomatically implementable with multiple receivers, since you can simply require a FormParserContextScope<T> that defines all of your ranges, rest, SUBTOTAL, etc functions as T.ranges() T.rest() T.instructions() and so on, and then you simply do

@with<FormParserContextScope<T> 
fun <T> evalLine(ctx: T) {
    val instructions = ctx.instructions()
}

and then simply you define your FormParserContextScope implementation for each FormParser.XXContext that you want to support. You can also optionally make FormParserContextScope a sealed interface if you’re certain that you only need implementations for those 2 Contexts and no other ones.

According to the Kotlin Online Event Q&A, multiple receivers won’t be stable until the new Frontend is stable, but Roman Elizarov did confirm that a public prototype will be coming out this year, which should be quite interesting to play with!

Thanks for the suggestion! Is this what you mean or am I missing something?

sealed interface FormParserContextScope<T> {
  object paraContext : FormParserContextScope<FormParser.ParaContext> {
    override fun FormParser.ParaContext.ranges() = ranges()
    override fun FormParser.ParaContext.rest() = rest()
    override fun FormParser.ParaContext.SUBTOTAL() = SUBTOTAL()
    override fun FormParser.ParaContext.instructions() = instructions()
    //override val FormParser.ParaContext.amount = amount
  }

  object sepContext : FormParserContextScope<FormParser.SepContext> {
    override fun FormParser.SepContext.ranges() = ranges()
    override fun FormParser.SepContext.rest() = rest()
    override fun FormParser.SepContext.SUBTOTAL() = SUBTOTAL()
    override fun FormParser.SepContext.instructions() = instructions()
    //override val FormParser.SepContext.amount = amount
  }

  fun T.ranges(): FormParser.RangesContext?
  fun T.rest(): FormParser.RestContext?
  fun T.SUBTOTAL(): TerminalNode?
  fun T.instructions(): FormParser.InstructionsContext
  //val T.amount: Token?
}

fun <T> evalLine(ctx: T, either: FormParserContextScope<T>) {
  val instructions = with(either) { ctx.instructions() }
  [...]
}

fun main() {
  val ctx = FormParser.ParaContext()
  evalLine(ctx, FormParserContextScope.paraContext)
}
1 Like

yeahhhhhh basically that but with the new Multiple receivers feature that is planned for Kotlin, which then will turn your code into this instead (Note that this is the syntax that was publicly discussed but it is subject to change):

// Same exact deal here
sealed interface FormParserContextScope<T> {
  object paraContext : FormParserContextScope<FormParser.ParaContext> {
    override fun FormParser.ParaContext.ranges() = ranges()
    override fun FormParser.ParaContext.rest() = rest()
    override fun FormParser.ParaContext.SUBTOTAL() = SUBTOTAL()
    override fun FormParser.ParaContext.instructions() = instructions()
    //override val FormParser.ParaContext.amount = amount
  }

  object sepContext : FormParserContextScope<FormParser.SepContext> {
    override fun FormParser.SepContext.ranges() = ranges()
    override fun FormParser.SepContext.rest() = rest()
    override fun FormParser.SepContext.SUBTOTAL() = SUBTOTAL()
    override fun FormParser.SepContext.instructions() = instructions()
    //override val FormParser.SepContext.amount = amount
  }

  fun T.ranges(): FormParser.RangesContext?
  fun T.rest(): FormParser.RestContext?
  fun T.SUBTOTAL(): TerminalNode?
  fun T.instructions(): FormParser.InstructionsContext
  //val T.amount: Token?
}

@with<FormParserContextScope<T>>()
fun <T> evalLine(ctx: T) {
  // The scope instance is automatically a receiver, and so no need to do with(either){...} here
  val instructions = ctx.instructions()
  [...]
}

//I can't quite remember the syntax planned for this, but basically there's 2 options. Either this:
//Notice that this with is using a parameter and not a type, because it simply uses the passed-in instance as a receiver for the whole function
@with(FormParserContextScope.paraContext)
@with(FormParserContextScope.sepContext)
fun main() {
  val ctx = FormParser.ParaContext()
  //Automatically understands that it needs to use the paraContext scope
  evalLine(ctx)
}

// or this, more primitive style:
fun main() {
  with(FormParserContextScope.paraContext) {
    with(FormParserContextScope.sepContext) {
      val ctx = FormParser.ParaContext()
      //Automatically understands that it needs to use the paraContext scope
      evalLine(ctx)
    }
  }
}

but yeah the version of the code that you wrote is the one that works right now, but in the future with multiple receivers the whole @with thing will make it much neater and more idiomatic

1 Like

Nice! Thanks for helping me wrap my head around this feature.

I’m curious if multiple receivers will allow the following, by somehow resolving (para ?: sep).instructions() with the common ancestor of FormParserContextScope.para and FormParserContextScope.sep, i.e. FormParserContextScope<T>, or if it will continue to narrow para ?: sep to e.g. Any and therefore error: unresolved reference. None of the following candidates is applicable because of receiver type mismatch:

fun main() {
  val para = FormParser.ParaContext()
  val sep = FormParser.SepContext()
  val instructions =
    with(FormParserContextScope.para) {
      with(FormParserContextScope.sep) {
        (para ?: sep).instructions()
      }
    }
}
1 Like

Hmmmm, let me write a quick prototype with semantics that work today to see if it’ll work or not. Brb with an edit to this reply. However, my intuition is that it most likely would not work :frowning:

EDIT: Sadly it doesn’t work:

class TerminalNode
class FormParser {
    class ParaContext {    
		fun ranges(): FormParser.RangesContext? = TODO("ParaContext.ranges")
		fun rest(): FormParser.RestContext? = TODO("ParaContext.rest")
		fun SUBTOTAL(): TerminalNode? = TODO("ParaContext.SUBTOTAL")
		fun instructions(): FormParser.InstructionsContext = TODO("ParaContext.instructions")
    }
    class SepContext {    
		fun ranges(): FormParser.RangesContext? = TODO("SepContext.ranges")
		fun rest(): FormParser.RestContext? = TODO("SepContext.rest")
		fun SUBTOTAL(): TerminalNode? = TODO("SepContext.SUBTOTAL")
		fun instructions(): FormParser.InstructionsContext = TODO("SepContext.instructions")
    }
    class RangesContext
    class RestContext
    class InstructionsContext
}
sealed interface FormParserContextScope<T> {
  object paraContext : FormParserContextScope<FormParser.ParaContext> {
    override fun FormParser.ParaContext.ranges() = ranges()
    override fun FormParser.ParaContext.rest() = rest()
    override fun FormParser.ParaContext.SUBTOTAL() = SUBTOTAL()
    override fun FormParser.ParaContext.instructions() = instructions()
  }

  object sepContext : FormParserContextScope<FormParser.SepContext> {
    override fun FormParser.SepContext.ranges() = ranges()
    override fun FormParser.SepContext.rest() = rest()
    override fun FormParser.SepContext.SUBTOTAL() = SUBTOTAL()
    override fun FormParser.SepContext.instructions() = instructions()
  }

  fun T.ranges(): FormParser.RangesContext?
  fun T.rest(): FormParser.RestContext?
  fun T.SUBTOTAL(): TerminalNode?
  fun T.instructions(): FormParser.InstructionsContext
}

fun <T> FormParserContextScope<T>.evalLine(ctx: T) {
  val instructions = ctx.instructions() 
}

fun main() {
  val paractx: FormParser.ParaContext? = FormParser.ParaContext()
  val sepctx: FormParser.SepContext = FormParser.SepContext()
  
  FormParserContextScope.paraContext.evalLine(paractx!!)
  FormParserContextScope.sepContext.evalLine(sepctx)
  with(FormParserContextScope.paraContext){
  	with(FormParserContextScope.sepContext){
      evalLine(paractx!!)
      evalLine(sepctx)
      evalLine(paractx ?: sepctx)
  	} 
  }
}

I rewrote the example to only use the scope as a receiver because that uses the same receiver decision mechanism as multiple receivers. Sadly in this case paractx ?: sepctx results in type Any , and so it doesn’t work. You can still use this obviously: if(paractx != null) evalLine(paractx) else evalLine(sepctx)

1 Like

Awesome, thanks for this clear and thorough response! In that case I think I’ll stick with delegation/wrappers vs. extension interfaces, at least for now. Explicitly decorating/undecorating instances is verbose, but once decorated I think there are fewer gotchas.

KT-21955 delegating to a class would make this more convenient/less repetitive.

If someday union types are added, then presumably I could drop the explicit delegation/wrappers.

1 Like

With some tweaking, this works, and it’s acceptable I guess:

class TerminalNode
class FormParser {
    class ParaContext {    
		fun ranges(): FormParser.RangesContext? = TODO("ParaContext.ranges")
		fun rest(): FormParser.RestContext? = TODO("ParaContext.rest")
		fun SUBTOTAL(): TerminalNode? = TODO("ParaContext.SUBTOTAL")
		fun instructions(): FormParser.InstructionsContext = TODO("ParaContext.instructions")
    }
    class SepContext {    
		fun ranges(): FormParser.RangesContext? = TODO("SepContext.ranges")
		fun rest(): FormParser.RestContext? = TODO("SepContext.rest")
		fun SUBTOTAL(): TerminalNode? = TODO("SepContext.SUBTOTAL")
		fun instructions(): FormParser.InstructionsContext = TODO("SepContext.instructions")
    }
    class RangesContext
    class RestContext
    class InstructionsContext
}
sealed interface FormParserContextScope<T> {
  object paraContext : FormParserContextScope<FormParser.ParaContext> {
    override fun FormParser.ParaContext.ranges() = ranges()
    override fun FormParser.ParaContext.rest() = rest()
    override fun FormParser.ParaContext.SUBTOTAL() = SUBTOTAL()
    override fun FormParser.ParaContext.instructions() = instructions()
  }

  object sepContext : FormParserContextScope<FormParser.SepContext> {
    override fun FormParser.SepContext.ranges() = ranges()
    override fun FormParser.SepContext.rest() = rest()
    override fun FormParser.SepContext.SUBTOTAL() = SUBTOTAL()
    override fun FormParser.SepContext.instructions() = instructions()
  }

  fun T.ranges(): FormParser.RangesContext?
  fun T.rest(): FormParser.RestContext?
  fun T.SUBTOTAL(): TerminalNode?
  fun T.instructions(): FormParser.InstructionsContext
}

fun <T> FormParserContextScope<T>.evalLine(ctx: T) {
  val instructions = ctx.instructions() 
}
//sampleStart
fun main() {
  val paractx: FormParser.ParaContext? = FormParser.ParaContext() //Try changing this to null
  val sepctx: FormParser.SepContext = FormParser.SepContext()
  with(FormParserContextScope.paraContext){
  	with(FormParserContextScope.sepContext){
      paractx?.also(::evalLine) ?: sepctx.also(::evalLine)
  	} 
  }
}
//sampleEnd

With union types, I think paractx ?: sepctx would evaluate to a union of the 2. Then, you’d need a decorator @withFormParserContextScopeUnion<FormParser.ParaContext, FormParser.SepContext>() (i.e. with 2 type parameters ContextA and ContextB)that simply accepts the 2 other FormParserContextScope instances as receivers and adds a new object as a receiver like this:

object: FormParserContextScope< magic union syntax between ContextA and ContextB > {
    override fun < magic union syntax between ContextA and ContextB >.ranges() = if(this is ContextA) ranges()  else if (this is ContextB) ranges()
    // Automatically uses the correct implicit receiver based on the type
    // override other functions too...
}

and then simply when you call evalLine(paractx ?: sepctx) it automatically uses that union receiver. At least I hope that’s how it’ll work

1 Like

Er, if the following works:

val a = ContextA()
val instructions: FormParser.InstructionsContext = a.instructions()

And so does this:

val b = ContextB()
val instructions: FormParser.InstructionsContext = b.instructions()

Then you wouldn’t expect the following to work intrinsically, without FormParserContextScope?

val ab: magic union syntax between ContextA and ContextB = a ?: b
val instructions: FormParser.InstructionsContext = ab.instructions()

And if not, then isn’t magic union syntax between ContextA and ContextB just the common ancestor, same as today?

You’d expect error: unresolved reference. None of the following candidates is applicable because of receiver type mismatch, like today?

I guess denotable union types just means you can write, e.g. ContextA | ContextB vs. BaseContext. Except the former would preclude ContextC. Which would be most handy in cases like, e.g. Error | String. Sorry, thinking out loud.

In that case your (hypothetical) FormParserContextScope< magic union syntax between ContextA and ContextB > implementation makes sense, thanks!

It’s weird I find, having to write

when (this) {
  is ContextA: ranges()
  is ContextB: ranges()
}

but not

when (this) {
  is ContextA, is ContextB: ranges()
}

Subtle. I guess it makes sense if (without a common ancestor) ContextA::ranges()ContextB::ranges(). Will take some getting used to, but probably a rare case. Thanks again for all the help!

1 Like