2

I'm coming from Java to Scala. I want to create an Outer class that defines (1) an inner Inner case class and (2) a method that returns a list of Inner objects. Note that in Java, the type of the inner class would unambiguously be Outer.Inner.

I found that there are two ways to reference the type of the inner class: outerObject.Inner or Outer#Inner, as discussed here. Which should I use to return a list of the Inner object?

For example, I can implement my Scala code like this:

// Implementation 1
class Outer {
    case class Inner(id: String)
    def build(input: List[String]): List[Inner] = {
        var result = scala.collection.mutable.ListBuffer[Inner]()
        input.foreach { s =>
            val inner = Inner(s)
            result += inner
        }
        result.toList
    }
}

or like this:

// Implementation 2
class Outer {
    case class Inner(id: String)
    def build(input: List[String]): List[Outer#Inner] = {
        var result = scala.collection.mutable.ListBuffer[Outer#Inner]()
        input.foreach { s =>
            val inner = Inner(s)
            result += inner
        }
        result.toList
    }
}

where the only difference is that build() returns List[Inner] or List[Outer#Inner].

Suppose the caller of my code runs this:

val input: List[String] = List("A", "B", "C")
val outer = new Outer()
val output = outer.build(input)

Then for implementation 1, the caller sees that output is:

output: List[outer.Inner] = List(Inner(A), Inner(B), Inner(C))

while for implementation 2, the caller sees:

output: List[Outer#Inner] = List(Inner(A), Inner(B), Inner(C))

Which is the preferred (idiomatic) approach in Scala?

Related questions:

stackoverflowuser2010
  • 38,621
  • 48
  • 169
  • 217

1 Answers1

4

Welcome to Scala!

Firstly, there's a cleaner, more efficient way to create your implementations, which I've shown below.

// Implementation 1
class Outer {
  case class Inner(id: String)
  def build(input: List[String]): List[Inner] = input.map(s => Inner(s))
}

// Implementation 2
class Outer {
  case class Inner(id: String)
  def build(input: List[String]): List[Outer#Inner] = input.map(s => Inner(s))
}

The use of map means that you don't need any ugly var elements, and nor do you need to build a ListBuffer then convert that to a List.

As to which is more idiomatic, it depends upon what you're trying to do. The first form returns a path-dependent list of Inners. It can be restricted for use with the Outer instance that created it. In the second form, the type of Inner returned cannot be tied to a specific Outer instance, but can be used anywhere that any Inner instance is acceptable. So, the latter is more restrictive and the former less so. Consider the following:

// Implementation 1
class Outer {
  case class Inner(id: String)
  def build(input: List[String]): List[Inner] = input.map(s => Inner(s))
  def getId(inner: Inner): String = inner.id
}

// Implementation 2
class Outer {
  case class Inner(id: String)
  def build(input: List[String]): List[Outer#Inner] = input.map(s => Inner(s))
  def getId(inner: Outer#Inner): String = inner.id
}

Then try evaluating the following with each implementation:

val input: List[String] = List("A", "B", "C")
val outer = new Outer()
val output = outer.build(input)
val outer2 = new Outer()
outer2.getId(output.head)

With implementation 1, you'll get a type mismatch error on the last statement, but you will not with implementation 2.

UPDATE: I forgot to mention this (probably to avoid further confusion), but the path-dependent version is a sub-type of the general version. (That is this.Inner is a sub-type of Outer#Inner.) So you can pass a path-dependent version of Inner to a function requiring any Inner, but not the other way around.

That is, the following will work:

class Outer {
  case class Inner(id: String)
  def build(input: List[String]): List[Inner] = input.map(Inner)
  def getId(inner: Outer#Inner): String = inner.id
}

val input: List[String] = List("A", "B", "C")
val outer = new Outer()
val output = outer.build(input)
val outer2 = new Outer()
outer2.getId(output.head)

but this will not:

class Outer {
  case class Inner(id: String)
  def build(input: List[String]): List[Outer#Inner] = input.map(Inner)
  def getId(inner: Inner): String = inner.id
}

val input: List[String] = List("A", "B", "C")
val outer = new Outer()
val output = outer.build(input)
val outer2 = new Outer()
outer2.getId(output.head)

(Note that I also used an even terser form of the build method in each of these.)

So, to summarize...

Functions (belonging to Outer) that return new Inner instances should return the path-dependent form, since that is also a sub-type of the generic form, and provides maximum flexibility. (However, bear in mind that publicly accessible Inner class instances can be created from outside of the Outer instance's control, and can be of whatever type the caller requires.)

Of far greater significance, however, is the type signature that is used when accepting existing Inner instances:

  • If the operation (belonging to an Outer instance) has a strong coupling between the Inner instance and itself (and the Inner instance will typically maintain a reference to its Outer instance), such that it would be an error to supply an Inner belonging to a different Outer instance, then you should only accept the path-dependent form.

  • However, if the operation works for any Inner instance, including the case where the accepting method doesn't belong to Outer at all, then it should accept the generic form.

Mike Allen
  • 8,139
  • 2
  • 24
  • 46
  • Thank you. What is the advantage of Scala's *path-dependent list of inner classes*? What problem does that solve? – stackoverflowuser2010 Sep 26 '18 at 02:35
  • @stackoverflowuser2010 In this particular example, there's clearly no obvious benefit from using the _path dependent_ form. In fact, it probably causes more problems than it solves. However, each `Inner` is also linked to the `Outer` instance that it belongs to. Let's say that `Outer` has other data members that `Inner` is manipulating through that reference. If you pass an `Inner` created by one `Outer` instance to a different `Outer` instance, it is still accessing the original `Outer`'s members, which might be cause for confusion. – Mike Allen Sep 26 '18 at 02:44
  • @stackoverflowuser2010 See [this answer](https://stackoverflow.com/a/2694607/2593574) for a good example. – Mike Allen Sep 26 '18 at 02:44
  • @stackoverflowuser2010 There's also another way to declare _path-dependent_ types via the `type` statement, which does not necessarily rely on inner classes. See Chapter 20 (and 20.7 in particular) of _Programming in Scala_, Third Edition, by Odersky, Spoon & Venners. – Mike Allen Sep 26 '18 at 02:50
  • @stackoverflowuser2010 Apologies for all the extra comments! :-) Just wanted to let you know that I updated the answer to better explain when to use each type. In practice, it should be pretty easy to make the necessary distinctions. – Mike Allen Sep 26 '18 at 04:12