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 Inner
s. 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.