Help understanding local class definitions

I’ve been using the following code for some time, but I really don’t understand how it works. There is a class NonLocalExit defined within the definition of def block. And there is also a catch { case NonLocalExit within the same function block.

What I don’t understand is how do recrusive calls to block distinguish between all the different classes of the same name. I.e., suppose that body also calls block. The the class named NonLocalExit within that block is supposedly different than the class named NonLocalExit in the outer call to block, right? Are these two different java classes? Are local class definitions supported by java?

If a piece of application code recursively calls block, say 500 times (supposing there is enough stack space), then how many classes named NonLocalExit are created? this must be done at run-time, as the compiler doesn’t know how many need to be created.

in short, I’m confused about how this works. And whether my confusion has lead to bugs in my code which are waiting to show themselves later.


  def block[A](body:(A=>Nothing)=>A):A = {
    // CL like block/return, the name of the return() function is provided
    //  by the caller.
    //  Usage:  block{ ret =>  ... ret(someValue) ...}

    // extending Exception with NoStackTrace prevents throwing the
    // exception from computing the stacktrace and storing the information.
    // We don't need a stacktrace because the purpose of this exception
    // is simply to perform a non-local exit.
    import scala.util.control.NoStackTrace

    class NonLocalExit(val data:A) extends Exception with NoStackTrace {}
    def ret(data:A):Nothing = {
      throw new NonLocalExit(data)
    }
    try{
      body(ret)
    }
    catch{
      case nonLocalExit: NonLocalExit => nonLocalExit.data
    }
  }
1 Like

I can’t comment this at the language spec/compiler implementation level, but to my understanding, barring specific compiler tricks, there will be one class at the bytecode level per class declaration at the source code level. However, at the source code level the compiler will enforce the perception that for inner class declarations, there will be one class per outer instance.

class Foo {
  class Bar // only one Foo$Bar.class

  def accept(b: Bar): Unit = ()

  def acceptAny(o: AnyRef): Unit =
    o match {
      case _: Bar => println("Bar")
      case _ => println("other")
    }
}

val f1 = new Foo
val f2 = new Foo

f1.accept(new f1.Bar) // ok
//f1.accept(new f2.Bar) // doesn't compile

f1.acceptAny(new f1.Bar) // "Bar"
f1.acceptAny(new f2.Bar) // "other"

[EDIT: I guess that the synthetic $outer reference is used to distinguish cases in the #acceptAny() dispatch, but I’m not sure.]

With method-local classes in recursive functions, there’s no “anchor” the compiler can use to tie a class reference to a specific recursion level, and pattern matching boils down to reflection in disguise, anyway, so method-local classes basically behave just like inner classes.

def rabbitHole(count: Int): Unit = {
  class FooExc extends RuntimeException
  try {
    if(count <= 0) throw new FooExc else rabbitHole(count - 1)
  }
  catch {
    case e: FooExc =>
      println(e.getClass)
      throw e
  }
}

rabbitHole(2)

Output:

class de.sangamon.scalausers.MemberClassId$FooExc$1
class de.sangamon.scalausers.MemberClassId$FooExc$1
class de.sangamon.scalausers.MemberClassId$FooExc$1
Exception in thread "main" de.sangamon.scalausers.MemberClassId$FooExc$1

There is only one class for a given full name, unless you are using an unconventional a custom classloader, which you don’t. Therefore, the runtime type is independent of which invocation of the method.

One might wonder whether the compile time type is different, but since there is no way to refer to a type related to a different invocation of the same method, you can only ever refer to the type in its current invocation, so there is only one local type.

that’s the opposite of what I thought when I wrote the code.
The inner most catch catches a throw of the outermost exception class. :frowning:

Now my question to self is why did my tests pass if my code is wrong? :angry:

def rabbitHole(count: Int, f:()=>Nothing): Unit = {
  class FooExc extends RuntimeException
  def throwIt():Nothing = {
    println(s"throwing when count=$count")
    throw new FooExc
  }
  try {
    if(count == 4) {
      f()
    } else if (count == 0)
      rabbitHole(count+1,throwIt)
    else
      rabbitHole(count+1,f)
  }
  catch {
    case e: FooExc =>
      println(s"catching at count=$count " + e.getClass )
  }
}

rabbitHole(0,()=>(throw new Exception))

this prints the following

throwing when count=0
catching at count=4 class $line29.$read$$iw$$iw$FooExc$1

I figured out the reason why my tests were passing with a wrong implementation of block. It’s because I forgot to ever write a test for block.

OK, I wrote a test case to show the old implementation fails. This was failing as the exception thrown by ret1, was being caught by the catch of ret2.

  assert(45 == block{ret1:T =>
    block{ret2:T =>
      ret1(45)
    }
    ret1(46)})

I’ve fixed the code to add what I hope is a unique ident to the the recursion level. My tests pass.
Nevertheless, I’d be really happy if someone could tell me whether (1) body in the following fixed code is really a unique ident, (2) whether the eq in the if-expression if nonLocalExit.ident eq body is really a pointer comparison of the function object.

I’m not 100% in Scala when I’m dealing with the function object and when I’m calling the function when I use a function name without parens.


  def block[A](body:(A=>Nothing)=>A):A = {
    // CL like block/return, the name of the return() function is provided
    //  by the caller.
    //  Usage:  block{ ret =>  ... ret(someValue) ...}

    // extending Exception with NoStackTrace prevents throwing the
    // exception from computing the stacktrace and storing the information.
    // We don't need a stacktrace because the purpose of this exception
    // is simply to perform a non-local exit.
    import scala.util.control.NoStackTrace

    class NonLocalExit(val data:A,val ident:(A=>Nothing)=>A) extends Exception with NoStackTrace {}

    def ret(data:A):Nothing = {
      throw new NonLocalExit(data,body)
    }
    try{
      body(ret)
    }
    catch{
      case nonLocalExit: NonLocalExit if nonLocalExit.ident eq body => nonLocalExit.data
    }
  }

Yes, they are. Here is a valid Java code https://www.ideone.com/xXr3ml :

/* package whatever; // don't place package name! */

import java.util.*;
import java.lang.*;
import java.io.*;

/* Name of the class has to be "Main" only if the class is public. */
class Ideone
{
    public static void main (String[] args) throws java.lang.Exception
    {
        class Greeter {
            void greet() {
                System.out.println(Arrays.toString(args));
            }
        }
        new Greeter().greet();
        new Ideone().foo("me, of course");
    }

    void foo(String who) {
        class Bar {
            void baz() {
                class Abc {
                    void xyz() {
                        System.out.println("foo(" + who + ").Bar.baz().Abc.xyz()");
                    }
                }
                new Abc().xyz();
            }
        }
        new Bar().baz();
    }
}

Result of execution:

[]
foo(me, of course).Bar.baz().Abc.xyz()

I recommend attaching debugger to your program and experimenting with it. You can also write some automated unit tests.

Don’t confuse equivalence of types with equivalence of instances. You can have two separate instances of the same type. https://scastie.scala-lang.org/Yq7Bx3ftQFazZqbJGz9JSw

def makeXyzInstance = {
  class Xyz
  new Xyz
}

def makeFnInstanceA(y: Int) = (x: Int) => x + y

def makeFnInstanceB = (x: Int) => x + 1

val xyz1 = makeXyzInstance
val xyz2 = makeXyzInstance
// false because we have different instances
xyz1 eq xyz2
xyz1 == xyz2
// true because we have the same type
xyz1.getClass eq xyz2.getClass
xyz1.getClass == xyz2.getClass

val fn1a = makeFnInstanceA(3)
val fn2a = makeFnInstanceA(3)
// false because we have different instances
fn1a eq fn2a
fn1a == fn2a
// true because we have the same type
fn1a.getClass eq fn2a.getClass
fn1a.getClass == fn2a.getClass


val fn1b = makeFnInstanceB
val fn2b = makeFnInstanceB
// weird? returns true. maybe Scala turns functions that aren't closures into singletons?
fn1b eq fn2b
fn1b == fn2b
// true because we have the same type
fn1b.getClass eq fn2b.getClass
fn1b.getClass == fn2b.getClass

If x and y are both reference types then x eq y compares the addresses, i.e. compares object identity.

Yes, I see now. But it is shocking that xyz1.getClass eq xyz2.getClass is true. I understand that it’s true, but in my opinion it is the opposite of what one would naturally assume. For example, if a function returns a local function, then from at the call site those functions are not eq. Local functions have local semantics, but local classes do not.

OK now I know, but it’s still strange. The impression is that it is an implementation trade-off/limitation because of the JVM, i.e. a leaky abstraction.


def f1(n:Int):Int=>Int = {
  def f2(m:Int):Int = n+m

  f2
}

f1(12) eq f1(12) ;; false

def f3(n:Int): Class[_ <: Any] = {
  class Xyz(m:Int)

  val x = new Xyz(n)
  x.getClass
}

f3(12) eq f3(12) ;; true
def f1(n: Int): Class[_ <: Int=>Int] = {
  def f2(m: Int): Int = n+m

  (f2 _).getClass
}

f1(1) eq f1(2) // true

Runtime classes are way less powerful than compile time types. If you’re going to use runtime reflection that’s something you have to live with. And unfortunately you also have to be aware that runtime type tests (x.isInstanceOf[Y] or case x: Y =>) work with the erased runtime class. Most of the time you’ll get a warning when you do a type test that is unsafe—like on a parameterized type—but not always.

Whoa there…that is very often not true:

scala 2.13.2> def foo = () => 3
def foo: () => Int

scala 2.13.2> foo eq foo
val res2: Boolean = true

even when there are parameters involved, you can still get true:

scala 2.13.2> def foo(z: Int) = (x: Int) => x * x
def foo(z: Int): Int => Int

scala 2.13.2> foo(3) eq foo(0)
val res8 : Boolean = true

A way to get false is to actually capture a binding:

scala 2.13.2> def foo(z: Int) = (x: Int) => x * z
def foo(z: Int): Int => Int

scala 2.13.2> foo(3) eq foo(0)
val res9: Boolean = false

The impression is that it is an implementation trade-off/limitation because of the JVM, i.e. a leaky abstraction

Disagree. This is good, not bad. These are highly desirable optimizations for the compiler to be allowed to do.

The same goes for your getClass example, for the same reason. You don’t want the compiler to be forced to allocate extra classes and extra instances unless the language semantics demand it. And the language semantics shouldn’t demand it without a very good reason, because if they don’t, the compiler is then free to generate tighter, faster code.

it is the opposite of what one would naturally assume

According to what principle or precedent would one assume that?

2 Likes

I must admit that the fact that the above example of def foo(z: Int) = (x: Int) => x * x is a good argument against my intuition. I guess I expect functions and classes to close over their environment, but for functions it appears to be only the case sometimes, and for classes it appears to be never.

In my Java example method xyz from class Abc closes over parameter who from method foo in class Ideone.