Private type members in Scala aren't
I haven't posted in a while now, and one major reason is that I've been teaching a new course on Programming Languages. I've been using Scala, somewhat experimentally, and as a (perhaps misguided) way of forcing myself to learn some Scala.
In the last few lectures, I've been covering Scala's object system. I'm not an expert on Scala by any means, but I was a little surprised by the way "private" appears to work (or more accurately, not work) on type members of classes/objects.
In Scala, a class (or object, or trait) can have type members. In an abstract class or trait, these can be left undefined:
abstract class A {
type T
val c : T
def f(x:T): T
}
whereas in an object, the definition has to be provided:
object B extends A {
type T = Int
val c: T = 42
def f(x: T): T = x + 1
}
So far so good. As an object-oriented language, Scala provides keywords such as public and private to indicate whether a class/object member is visible to the outside world or only internally.
So, if we make c private, and try to access it, we get an error:
object C {
type T = Int
private val c: T = 42
def f(x: T): T = x + 1
}
scala> B.c
<console>:13: error: value c in object C cannot be accessed in object C
C.c
But, if we make T private, the fact that C.T = Int still seems to be visible to the rest of the program, i.e.:
object D {
private type T = Int
val c:T = 42
def f(x: T): T = x + 1
}
scala> D.c
res1: Int = 42
scala> D.f
res2: D.T => D.T = <function1>
scala> D.f(-42)
res6: Int = -41
If I were relying on private to hide the implementation details of T (e.g. to enforce an invariant that T's values are greater than or equal to 42) then I would be disappointed. Is this a bug? The Scala language reference manual doesn't seem to contain any examples using private type, so if private does do something to type members, it isn't clear what. If it has no effect, perhaps it should not be allowed, or generate a warning.
Labels: functional programming, Scala
3 Comments:
There's a bug here, but only in the displayed types, not in the "actual behavior".
(Bugtracker entry: https://issues.scala-lang.org/browse/SI-8812).
Since the bug description is extremely terse, you might want to read on.
Regarding information hiding, there are other ways to hide T's definition.
Because of private, `D.T` will not work outside of `D`, just like `D.c`:
```
scala> object D {
| private type T = Int
| val c:T = 42
| def f(x: T): T = x + 1
| }
defined object D
scala> val v: D.T = 42
:11: error: type T in object D cannot be accessed in object D
val v: D.T = 42
^
```
However, `T = Int` is a type alias, so it can be freely inlined inside D. Hence your object is equivalent to:
```
object D {
private type T = Int
val c:Int = 42
def f(x: Int): Int = x + 1
}
```
So all your usage example should actually typecheck, as they do. Arguably, a correct implementation would (internally) just normalize the types of the members. Under that model, it's clear that private type members can't achieve abstraction. However, type synonyms should usually not be expanded for readability.
It's certainly bad that `D.f`'s type is shown as `D.T => D.T` (that's what the bugtracker entry is about), and the internal logic of the compiler is... not well defined.
More in general, `expression.T` is dispatched *statically* based on the static type `U` of `expression`, if that type `U` defines `T` with equality (something which is not so intuitive — I only got that after reading Odersky et al.'s νObj paper at ECOOP 2003).
So if `D.T` weren't private, it could be freely inlined everywhere in the program.
For information hiding — you need to use abstract types, similarly
to what you'd need to do in ML (signatures become traits,
structures become classes/objects).
To achieve abstraction in your example, you could try this (compiles, otherwise untested):
```
trait D {
type T
val c: T
def f(x: T): T
}
// One instance. D1.T is still Int, though, but that doesn't affect
// clients accepting D.
object D1 extends D {
type T = Int
val c: T = 42
def f(x: T): T = x + 1
}
// If you want to hide your implementation more
object DCompanion {
val anInstance: D = new D {
type T = Double
val c: T = 42.0
def f(x: T): T = x + 1.0
}
private object AnotherInstance extends D {
type T = Double
val c: T = 43.0
def f(x: T): T = x + 2.0
}
val anotherInstance: D = AnotherInstance
}
class Client {
def f(d: D) = {
// Here d.T is fully abstract.
//1 : D //would give a compile error.
}
}
```
Some further general observations.
Generally, the whole Scala type system is best understood as a huge superset of the
ML module system, with first-class modules (objects),
mixin composition, and more, and every bit as complex.
Many features of ML modules can be encoded, and that was
one of the design goals. Look up the paper "Scalable Component Abstractions"
for details on how to achieve that; for a less researchy introduction,
look up the "cake pattern", which is the same thing (though most won't mention that).
In my experience with Scala, after reading the spec, I recommend a trip to the issue tracker and/or
asking on the scala mailing lists (scala-language, scala-internals). That's often a good next step after reading the spec. Among other reasons:
- the spec hasn't kept up with compiler changes
- it was always incomplete (type inference isn't actually specified, whatever the spec seems to suggest).
- the correct behavior itself is still under research. In this case, the bugtracker shows two Scala compiler hackers figuring out what the behavior should be. Generally, access modifiers on type members show up in no formal model of Scala.
A cooler example of behavior under research: you mention type members in objects. In fact, they can be left abstract — the compiler used to give an error in some cases but not in others. When I pointed out the inconsistency, that restriction which was there in some cases turned out to be pointless, and was fixed (https://github.com/scala/scala/pull/4024). In fact, undefined type members model existential types, and that's one crucial insight of the new variants of Scala's type system.
Thanks, Paolo, that's very helpful. Your first (D1) suggestion to use traits is exactly what I told the students on Tuesday :)
I had, naturally (and perhaps naively!), been assuming that "undefined" type members behave like existentially-quantified types (or type members of signatures in ML) - where I was surprised was the fact that "private type" was allowed, but didn't seem to have the anticipated (or any) effect. From your explanation, it sounds like the right way to think of this is: private just constrains whether we can refer to D.T from other scopes, not whether the abbreviation D.T=Int is visible to other scopes. If we explicitly say that D.T abbreviates ty, then the meaning of the program is invariant under replacement of D.T with ty anywhere (except that some references to D.T might be disallowed because of the private keyword).
I couldn't find anything about how "private" interacts with "type" in the language specification either.
Post a Comment
Subscribe to Post Comments [Atom]
<< Home