Calling extension method from a given opaque type

I defined an Optional capability like this:

import scala.util.boundary
import scala.util.boundary.*

opaque type Optional = Label[None.type]
object Optional:
  def apply[T](f: Optional ?=> T): Option[T] = boundary(Some(f))
  def none(using Optional): Nothing = break(None)

  extension [T] (x: Option[T])
    def getOrBreak(using opt: Optional): T = x.getOrElse(none)

And I would like to call the getOrBreak extension in the Optional context, without an import:

Optional:
  xs.map(x => x.getOrBreak)

That does not work because getOrBreak is not a member of the Optional type. Is there a trick to make it work somehow?

Or, would it require to change the rules of Translation of Calls to Extension Methods? What would be such rule?

If the extension is in the same scope as the opaque type, then both are made available by
import lib.* which I think is not terrible.

The following wraps the extensions in a contextual arg:

import scala.util.boundary, boundary.*

object lib:
  opaque type Optional = Label[None.type]
  object Optional:
    def apply[A](body: (Optional, Ext) ?=> A): Option[A] = boundary(Some(body))
    def none(using Optional): Nothing = break(None)

  class Ext:
    extension [A](x: Option[A]) def getOrBreak(using Optional): A = x.getOrElse(Optional.none)
  given Ext()

@main def test = println:
  import lib.Optional
  val xs = List(Option(42))
  Optional:
    xs.map(_.getOrBreak)

I also tried the trick of passing a Conversion, but it doesn’t save an import because of the feature import.

This given Ext() does not work for me, I still need the import lib.given.

But you are right that if I put the extension at the same level as Optional, I get everything I need with a single import lib.*.

Still I think it would be nice to have it working with a single import lib.Optional, as it would be the case for a class:

class Optional(using Label[None.type]):
  extension [A](x: Option[A]) def getOrBreak A = x.getOrElse(Optional.none)

opaque type are ideal to create capabilities on top of Label, except for this inconvenience that we cannot have extension methods without an import, or a wildcard.

I think I managed to do exactly what you’re after:

 import scala.util.boundary
 import scala.util.boundary.*
 
 opaque type Optional = Label[None.type]
 object Optional {
   def apply[T](f: (Ext.type, Optional) ?=> T): Option[T] = boundary(Some(f(using Ext)))
   def none(using Optional): Nothing = break(None)
 }
 
 object Ext {
   extension [A](opt: Option[A]) {
     def getOrBreak(using Optional): A = opt.getOrElse(Optional.none)
   }
 }
 
 object test {
   val value = Optional {
     Option(1).getOrBreak
   }
 }

the only difference to the previous version (besides Ext being a singleton) is providing Ext directly in Optional.apply: boundary(Some(f(using Ext)))

But I’m also pretty sure that the OG version with class Ext also works? At least after taking a very brief look :thinking:

Showing that it works:

➜  snips scala-cli run --server=false -S 3.7.2 -Vprint:typer option-ext.scala
[[syntax trees at end of                     typer]] // /home/amarki/snips/option-ext.scala
package <empty> {
  import scala.util.boundary
  import scala.util.boundary.*
  final lazy module val lib: lib = new lib()
  final module opaque class lib() extends Object() { this: lib.type =>
    opaque type Optional = scala.util.boundary.Label[None.type]
    final lazy module val Optional: lib.Optional = new lib.Optional()
    final module class Optional() extends Object() { this: lib.Optional.type =>
      def apply[A >: Nothing <: Any](body: (lib.Optional, lib.Ext) ?=> A):
        Option[A] =
        scala.util.boundary.apply[Option[A]]((using
          contextual$1: scala.util.boundary.Label[Option[A]]) =>
          Some.apply[A](body.apply(contextual$1, lib.given_Ext)))
      def none(using x$1: lib.Optional): Nothing =
        scala.util.boundary.break[None.type](None)(x$1)
    }
    class Ext() extends Object() {
      extension [A >: Nothing <: Any](x: Option[A])(using x$2: lib.Optional)
        def getOrBreak: A = x.getOrElse[A](lib.Optional.none(x$2))
    }
    final lazy module given val given_Ext: lib.given_Ext = new lib.given_Ext()
    final module class given_Ext() extends lib.Ext() {
      this: lib.given_Ext.type =>}
  }
  final lazy module val option-ext$package: option-ext$package =
    new option-ext$package()
  final module class option-ext$package() extends Object() {
    this: option-ext$package.type =>
    @main def test: Unit =
      println(
        {
          import lib.Optional
          val xs: List[Option[Int]] =
            List.apply[Option[Int]]([Option.apply[Int](42) : Option[Int]]*)
          lib.Optional.apply[List[Int]]((using contextual$2: lib.Optional,
            contextual$3: lib.Ext) =>
            xs.map[Int]((_$1: Option[Int]) =>
              contextual$3.getOrBreak[Int](_$1)(contextual$2))
          )
        }
      )
  }
  final class test() extends Object() {
    <static> def main(args: Array[String]): Unit =
      try test catch
        {
          case error @ _:scala.util.CommandLineParser.ParseError =>
            scala.util.CommandLineParser.showError(error)
        }
  }
}

Some(List(42))

Oh yes it works, it is only that I missed the Ext context param in def apply[A](body: (Optional, Ext) ?=> A): Option[A].

That’s a nice trick indeed and it does what I want.

But then it makes it harder to pass this capability around, as I have to declare two parameters. Still not really satisfying.

You could try wrapping a Label instead of aliasing it?

import scala.util.boundary
import scala.util.boundary.*

class Optional private (using private val label: Label[None.type]):
  extension [T] (x: Option[T])
    def getOrBreak(using opt: Optional): T = x.getOrElse(Optional.none)
  
object Optional:
  def apply[T](f: Optional ?=> T): Option[T] = boundary(Some(f(using new Optional)))
  def none(using optional: Optional): Nothing =
    break(None)(using optional.label)


val xs = List(Option(1), Option(2), None)

Optional {
  xs.map(x => x.getOrBreak)
}
1 Like