Using single abstract methods for understandable scope injection

Intro

I see a few other posts on here relating to scope injection using implicits or other things to try and get a sort of scope injection like behavior. The part that confuses me is that scala already has a feature that I would expect to work for this, but it has 2 problems.

The feature I am talking about is single abstract methods, or SAM. Below I detail how it seems this feature should already work right out of the box, but does not for some reason.


Some Context

Below, you can see a trait which can be implemented using a SAM.

  import scala.collection.mutable.{ListBuffer => MList}
  
  trait DslFunction[P] {
    
    var value: P = _
    
    protected def run: Unit
    
    // Technically not thread safe, but this could be easily fixed
    def execute(value: P): P = {
      this.value = value
      run
      value
    }
    
  }

The point here is not to make some special trait for this feature, but to point out the fact that you can essentially already do this with any SAM.


The Implementation

Showing how you would set it up

  def a(f: DslA): A =
    f.execute(A())
  
  trait DslA extends DslFunction[A] {
    
    def b1(f: DslB1): B1 = {
      val res: B1 = f.execute(B1())
      value.b1.append(res)
      res
    }
  
    def b2(f: DslB2): B2 = {
      val res: B2 = f.execute(B2())
      value.b2.append(res)
      res
    }
  
  }
  
  trait DslB1 extends DslFunction[B1] {
    
    def c1(f: DslC1): C1 = {
      val res: C1 = f.execute(C1())
      value.c1.append(res)
      res
    }
    
  }
  trait DslB2 extends DslFunction[B2] {
  
    def c2(f: DslC2): C2 = {
      val res: C2 = f.execute(C2())
      value.c2.append(res)
      res
    }
  
  }
  
  trait DslC1 extends DslFunction[C1] {
    
    def add(i: Int): Unit =
      value.ints.append(i)
    
  }
  trait DslC2 extends DslFunction[C2] {
  
    def add(s: String): Unit =
      value.strings.append(s)
  
  }

Awful Syntax - Works

val res0: A = a(new DslA {
    override protected def run: Unit = {
  
      b1(new DslB1 {
        override protected def run: Unit = {
      
          c1(new DslC1 {
            override protected def run: Unit = {
              add(1)
              add(2)
              add(3)
            }
          })
      
        }
      })
  
      b2(new DslB2 {
        override protected def run: Unit = {
      
          c2(new DslC2 {
            override protected def run: Unit = {
              add("~ 1 ~")
              add("~ 2 ~")
              add("~ 3 ~")
            }
          })
      
        }
      })
      
    }
  })

Awesome Syntax - Doesn’t Work

  a {
  
    b1 {
    
      c1 {
        add(1)
        add(2)
        add(3)
      }
    
    }
  
    b2 {
    
      c2 {
        add("~ 1 ~")
        add("~ 2 ~")
        add("~ 3 ~")
      }
    
    }
    
  }

Whats Wrong?

There seems to be 2 issues here:

  1. Scala doesnt seem to recognize that you are trying to do a SAM unless there are parameters.
    AKA, It seems to think I am doing something like: a(f: => Unit).
  2. Even if you circumvent this by doing something like
    def run(useless: Unit): Unit
    a { _ => ... }
    (which does get around problem #1, by the way, although it is an annoyance)
    You are still left with the fact that your single abstract METHOD does not act like a METHOD whatsoever.
    The first definition I found when searching programming method definition:
    A method in object-oriented programming is a procedure associated with a message and an object.
    Our little single abstract METHODs here dont seem to realize they are really part of a bigger picture.

As I have been informed, apparently the powers that be are opposed to implementing any sort of "scope injection" in scala, given that it is confusing, but I would argue that this is rather intuitive. If you are filling out one last method to complete the trait, you would expect that that method knows about the rest of the trait, would you not?


Im very interested to see what everyone else has to say about this.

1 Like

Lambdas work in Scala just as in Java, i.e. this within a lambda doesn’t refer to that lambda (so you can’t use whatever members that lambda’s class has), but it does refer to outer class.

Example https://scastie.scala-lang.org/BPJWHb46SYirV4eoL0hx2Q :

class OuterClass { outer =>
  def m() = {
    val checkThis = () => {
      this == outer
    }
    checkThis()
  }
}

println(s"this in lambda refers to outer class? ${new OuterClass().m()}")

Prints: this in lambda refers to outer class? true

There’s a somewhat ugly syntax for emulation of scope injection. If scope injection allows you to do that (where all identifiers are part of injected scope):

a {
  b {
    c(d, e)
    f()
  }
  g(h)
}

Then you can emulate that using this code in current Scala:

a { s =>
  import s._
  b { s =>
    import s._
    c(d, e)
    f()
  }
  g(h)
}

Also every time I see scope injection examples, they are ridden with side-effects. Is there a purely functional example that uses scope injection?

1 Like

My question is, is this possible to change?

I see these 2 things as very separate:

class Ex1 {
  def test(f: => Unit): Unit = f
}
// ...
val ex1: Ex1 = Ex1()
ex1.test {
  println(this == ex1) // true
}

and

trait MySAM {
  
  val info1: Int = ???
  val info2: String = ???

  def run: Unit

}
class Ex2 {
  def test(f: MySAM): Unit = f.run
}
// ...
val ex2: Ex2 = Ex2()
ex2.test { // Assuming that the whole 0 argument part works
  println(this == ex2) // currently: true, suggested: false
}

The point I am trying to make is that you currently have a mechanism in which to express an anonymous function where this is not modified. Then, you have another way of doing essentially the same exact thing, but with (what I see as) a rather obvious and intuitive separate understanding of what those 2 (seemingly different) things should do.

In java, (I am admittedly not up to date with newer versions, but I doubt this has changed), you only have 1 option. Lambdas are implemented using functional interfaces. In scala, if I do a simple (i1: Int, i2: Int) => i1 + i2, it is its own separate thing (Int, Int) => Int. Personally, the only time I have seen SAM’s have any sort of use on top of a regular function is to add helper methods such as map to make function composition easier.
The only possible downside I see is that you would lose this, but could easily get it back by having some class like class FunctionWrapper(f: (Int, Int) => Int) { def map(...): FunctionWrapper = ... }. Its not like any implementations using the SAM notation were benefiting from the fact they were part of a trait anyway.
I would assume it would be extremely trivial for anyone familiar with the compiler to make this change. Just modify the AST so that anywhere where an anonymous function was supplied in order to implement a SAM, you just wrap it up into the ugly syntax, and then it would behave in the manner I am suggesting. I would have a hard time believing any argument that this is not intuitive:
If you implement a SAM using the short-hand syntax, it is just syntactic sugar for doing it the other way, so this will actually refer to the trait you are implementing.

A function is a SAM itself. (A, B) => C is a syntactic sugar for Function2[A, B, C] (you can use them interchangeably). In Scala 2.12+ functions are treated just like any other SAM. Having some semantics for one category of SAMs and different semantics for other category of SAMs would be irregular and confusing, especially when you can do something like that: class Abc extends (Int => Int) with OtherSAMTypeWithPossiblyCompatibleAbstractMethodSignature.

Java had anonymous classes since Java 1.1 and that let you implement closures, although with a lot of boilerplate. You’ve already shown an example with such boliterplate, but in Scala. Java 8 adds short lambda syntax for SAM interfaces with lightweight implementation under the hood. The difference between Java’s anonymous classes and Java’s lambdas is the boilerplate involved and meaning of this - just as in case of Scala.

Ah, I did not realize that A => B was implemented in that same way. This definitely does poke a hole in my argument. That being said, I think it would be intuitive enough to have separate rules for something as specific as FuntionX[...], and everything else. Then, for your valid (although extremely edge-case example), I think you come up with a concrete rule that either if the SAM interface contains FunctionX, it will behave like FunctionX, or if the SAM interface contains OtherSAM, it will use the syntactic sugar rule.


@inlineSAM
trait OtherSAM {
  def f(i1: Int, i2: Int): Int
}

@inlineSAM(false)
trait Function2[A, B, C] {
  def f(a: A, b: B): C
}

I would imagine you could pull something like this off, while also allowing the user the specify which behavior they would like, and leave the default whichever people see fit.

It’s not extremely edge-case. Functions are in many places. For example all collections and companion objects of case classes implement some sort of function trait, e.g. Scastie - An interactive playground for Scala.

val mapFn: Int => String = Map.empty[Int, String]
val setFn: Double => Boolean = Set.empty[Double]
val seqFn: Int => String = Seq.empty[String]

case class SomeData(f1: Int, f2: String)
val factoryFn: (Int, String) => SomeData = SomeData

People are mixing in FunctionXXX traits to base traits or base classes to get widespread applicability (Functions are used everywhere) and function composition for free.

I hope you see now that the semantics aren’t broken, but they work as designed and they make some sense after all. What you’re proposing is yet another way to implement scope injection. I think annotations on base classes destroy local reasoning - instead of checking for method signatures to discover semantics I need to analyze class hierarchy. Kotlin’s syntax for function literals with receiver is better as it’s quick to discover. That’s only my opinion, though.

Hows this?

def test(@injectedScope f: MySam): Unit

Instead of putting the annotation on the class, you put it in the function signature itself, and then any calls to that function where a SAM interface is implemented using the short-hand syntax is expanded into the long-hand one, giving you this scope injection behavior.

It looks better, but:

  • I’m not the power that be that decides about Scala
  • there were already long discussions where I took part and gave opinions about various approaches
  • syntax bikeshedding is probably not the missing piece that powers that be expect (or is it? I forgot already)

I guess the bigger picture question is why are the "powers that be" so opposed to the concept, in general? Im not saying my suggestion is perfect, but based on the way you describe it, it sounds like you could present magic beans, and it would still get rejected.

1 Like

Did you see my proposal regarding scope injection?

It handles your use-case perfectly (if it were implemented). All you would need is to write something like:
def b1(import f: DslB1): B1 =

1 Like

I just gave it a look over, and was admittedly a little confused with what was going on there. Part of what I was trying to get at is that Im not really pushing for some full featured scope injection, all Im trying to say is that if you implement a SAM interface using the nice lambda function syntax (or whatever the proper terminology is), then the method actually behaves like it is a part of the trait it is implementing. No implicits involved.

Then @tarsa made a good point that FunctionX is implemented using the same exact syntax, so it would break everything. My solution to this was allowing the specific function to specify whether or not this sort of desugaring happens, through the use of an annotation on the parameter. Something like this:

def test(@scoped f: MySAM): Unit

While I personally have nothing against any complex implementations, the so called powers that be seem to, and I think that what I have proposed here is very simple, as it is essentially just a conditional desugaring.

@soronpo, my questions for you would be:

  • Would you see any benefit in having this simpler implementation, if you had to choose between this, or nothing at all?
  • You clearly came up with your solution for a reason. You ran into a problem you couldnt solve, or had to solve in way that was annoying to write. Could you solve whatever that problem is using my proposed solution?

Keep in mind you can have "local variables" "just appear" using the method I demonstrated in my example.
Some notably important people might consider this "black magicy", but I would argue that by simply looking at the signature of the function you are calling, you can see that it is being run in the context of a method inside a trait, and is perfectly simple to reason about, especially as there is already an analogous desugared way of writing it.

1 Like