为什么字符串字面量符合Scala单例?

8

明智或不明智,我正在编写一个方法,我希望仅接受Scala单例,即通过“object”实现的对象,而不是类或特质的构造实例。它应该接受任何类型的Scala单例,因此"MySingleton.type"不行。

我遇到了非常奇怪的构造“scala.Singleton”,它没有在API文档中记录,但似乎能解决问题:

scala> def check( obj : Singleton ) = obj
check: (obj: Singleton)Singleton

scala> check( Predef )
res0: Singleton = scala.Predef$@4d3e9963

scala> check ( new java.lang.Object() )
<console>:9: error: type mismatch;
 found   : java.lang.Object
 required: Singleton
              check ( new java.lang.Object() )

scala> check( Map )
res3: Singleton = scala.collection.immutable.Map$@6808aa2d

scala> check( Map.empty[Any,Any] )
<console>:9: error: type mismatch;
 found   : scala.collection.immutable.Map[Any,Any]
 required: Singleton
              check( Map.empty[Any,Any] )

然而,令我感到费解的是(对我来说),字符串字面量被接受为单例,而显式构造的字符串则不被接受:
scala> check( "foo" )
res7: Singleton = foo

scala> check( new String("foo") )
<console>:9: error: type mismatch;
 found   : java.lang.String
 required: Singleton
              check( new String("foo") )

为什么字符串字面量符合Singleton?我是否误解了Singleton类型应该指定的内容?

我不确定,但我认为这是因为编译器默认会将它们进行内部化处理。 - Derek Wyatt
1
这是我第一次听说scala.Singleton,而且文档中也没有提到。我不会打赌它会做出任何特定的承诺。事实上,我不确定有什么硬性标准可以将单例对象与类区分开来。您能详细说明一下您尝试通过识别它们来实现什么吗? - 0__
我的动机相当琐碎:我想能够导出Java转发类的完全限定名称,以便我可以通过Java反射将单例方法用作静态方法。我不喜欢依赖于被修改的/带有美元符号的名称。因此,我编写了一个实用程序,将单例类名的美元符号去除,以便使用Scala提供给Java的名称。如果混淆更改了,我只需要更新我的方法即可。该方法仅适用于Scala单例,因此如果类型系统能够强制执行它,我希望在获取和解码类名之前要求单例。 - Steve Waldman
顺便提一下,Scala 2.10将提供一个反射API,可以在这方面提供帮助。 - retronym
你可能会感兴趣知道 check(1) 也是可以的。好像所有的原始值都被视为单例。check(Double.NegativeInfinity) 也可以工作。 - nairbv
3个回答

9
首先,什么是单例类型?如果您认为类型是一组值,则单例类型是一个只有一个元素的集合。
最常见的情况是顶级对象可以属于这样的集合。
scala> object X
defined module X

scala> X: X.type
res41: X.type = X$@131d1cb

scala> res41: Singleton
res42: Singleton = X$@131d1cb

更一般地说,一个稳定的值可以形成一个单例类型。
scala> object X { val y: String = "boo" }
defined module X

scala> X.y: X.y.type
res44: X.y.type = boo

scala> res44: Singleton
res45: Singleton = boo

如果y是一个defvar,它不再合格,因为值随时间可能会改变,所以编译器无法保证单例类型分类只有一个值。
scala> object X { def y: String = "boo" }
defined module X

scala> X.y: X.y.type
<console>:12: error: stable identifier required, but X.y found.
              X.y: X.y.type
                     ^

scala> object X { var y: String = "boo" }
defined module X

scala> X.y: X.y.type
<console>:12: error: stable identifier required, but X.y found.
              X.y: X.y.type
                     ^

还有一个限制:因为语言规范明确将它们限制在AnyRef上,所以AnyVal不能形成单例类型。

Paul Phillips一直在维护一个分支,可以让您表达字面值的单例类型。

val xs: Stream[0.type](0)
val ys: Stream[0.type](0, 1) // does not compile
val x = xs.head // inferred type is 0.type, we statically know that this can only be 0!

有趣,谢谢。我真的很喜欢“<literal>.type”类型的想法。但我仍然不明白scala.Singleton的目的。正如pst在上面指出的那样,类型应该描述引用对象的持久特征,而不仅仅是历史上的某些东西。很难看出一个类型保证某个时刻某个集合中单一元素被引用的意义所在。毕竟,通过在瞬态作用域中将var分配给val,可以轻松地将任何var“转换”为此类型。如果与val(“val f =(a:Int)=> 2 * a”)相符,为什么def会失败? - Steve Waldman
“Singleton”的“用户端”目的是什么?是否有任何涉及此“类型”的有用应用程序? - 0__
毕竟,通过在瞬态作用域中分配给val,将任何变量“转换”为此变量是微不足道的。当前变量值的“快照”是稳定的;这并不会破坏稳定性的概念。 - retronym

6
据我所知,在这种情况下,每个不可变的引用都可以被视为单例,不仅仅是字符串。例如,您可以调用check(5)val foo = List(1,2,3); check(foo)。但是,var bar = List(1,2,3); check(bar)将无法工作。
根据这种行为,我认为如果编译器确定引用永远不会改变(或在此上下文中是“最终”的),则该引用将被视为单例。

我不太理解 List 的问题,似乎应检查表达式的类型,而不是评估它的变量/字段..然而前者通过了,而后者失败了。在 Singleton 和类型检查方面是否存在其他编译器魔法?规则在哪里指定? - user166390
1
是的,真实而有趣(正如pst所建议的一种非常奇怪的类型系统行为)。任何val似乎都符合要求(包括引用可变类型的val),而所有的var都失败了(包括初始化为不可变字面量的var)。映射到Java原语和字符串的字面值似乎符合要求(包括null、true/false和Class字面值),更复杂的Scala-y字面值(例如符号、函数、XML字面值)则不符合要求。看起来无论单例类型检查什么,它都不是我想要的。虽然我希望我能更好地理解它。 - Steve Waldman

4
我认为最简单的线索来自Scala参考手册的第3章,第3.2.1节:

一个单例类型的形式为p.type,其中p是指向预期符合(§6.1)scala.AnyRef的值的路径。该类型表示由null和p所表示的值组成的集合。

稳定类型要么是单例类型,要么是声明为trait scala.Singleton子类型的类型。

稳定类型的概念很重要,这个trait使得可以声明那些原本不会被视为稳定类型的东西为稳定类型。


谢谢。我想我得阅读规范才能理解在这种情况下“稳定性”的含义以及为什么它很重要。再次,考虑到scala.Singleton的可观察行为,我觉得有点令人困惑。 - Steve Waldman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接