什么时候可以省略(省略)括号、点、大括号、=(函数)等的精确规则是什么?
例如,
(service.findAllPresentations.get.first.votes.size) must be equalTo(2).
服务是我的对象
def findAllPresentations:选项[列表[演示]]
投票返回列表[投票]
must 和 be 都是 specs 的函数
为什么我不能去:
(service findAllPresentations get first votes size) must be equalTo(2)
?
编译器错误是:
“Option[List[com.sharca.Presentation]] 类型的 RestServicesSpecTest.this.service.findAllPresentations 不带参数”
为什么它认为我正在尝试传递参数?为什么我必须为每个方法调用使用点?
为什么必须 (service.findAllPresentations get first votes size)
等于 equalTo(2) 导致:
“未找到:价值第一”
然而,(service.findAllPresentations.get.first.votes.size)
的“必须等于 2”必须等于 2,也就是说,方法链接工作正常吗? - 对象链链链参数。
我浏览了 Scala 的书和网站,并没有真正找到全面的解释。
事实上,正如 Rob H 在 Stack Overflow 问题 Which characters can I omit in Scala? 中解释的那样,省略 '.' 的唯一有效用例是是用于“操作数运算符操作数”样式的操作,而不是用于方法链接?
您似乎偶然发现了答案。无论如何,我会尽量说清楚。
使用前缀、中缀和后缀表示法时可以省略点——所谓的运算符表示法。使用运算符表示法时,如果传递给方法的参数少于两个,则可以省略括号。
现在,操作符符号是方法调用的符号,这意味着它不能在没有被调用的对象的情况下使用。
我将简要介绍这些符号。
字首:
只有 ~
、!
、+
和 -
可用于前缀表示法。这是您在编写 !flag
或 val liability = -debt
时使用的符号。
中缀:
这就是方法出现在对象和它的参数之间的符号。算术运算符都适合这里。
后缀(也是后缀):
当方法跟随一个对象并且不接收任何参数时,使用该表示法。例如,您可以写成 list tail
,这就是后缀表示法。
只要没有柯里化方法,您就可以毫无问题地链接中缀表示法调用。例如,我喜欢使用以下样式:
(list
filter (...)
map (...)
mkString ", "
)
这与以下内容相同:
list filter (...) map (...) mkString ", "
现在,如果 filter 和 map 采用单个参数,为什么我在这里使用括号?这是因为我将匿名函数传递给他们。我不能将匿名函数定义与中缀样式混合,因为我的匿名函数结尾需要一个边界。此外,匿名函数的参数定义可能被解释为中缀方法的最后一个参数。
您可以使用带有多个参数的中缀:
string substring (start, end) map (_ toInt) mkString ("<", ", ", ">")
Curried 函数很难与中缀表示法一起使用。折叠函数就是一个明显的例子:
(0 /: list) ((cnt, string) => cnt + string.size)
(list foldLeft 0) ((cnt, string) => cnt + string.size)
您需要在中缀调用之外使用括号。我不确定这里的确切规则。
现在,让我们谈谈后缀。后缀可能很难使用,因为除了表达式的结尾之外,它永远不能在任何地方使用。例如,您不能执行以下操作:
list tail map (...)
因为 tail 不会出现在表达式的末尾。你也不能这样做:
list tail length
您可以通过使用括号来标记表达式的结尾来使用中缀表示法:
(list tail) map (...)
(list tail) length
请注意,不鼓励使用后缀表示法,因为 it may be unsafe。
我希望这已经消除了所有的疑虑。如果没有,请发表评论,我会看看我能做些什么来改进它。
类定义:
val
或 var
可以从类参数中省略,这将使参数变为私有。
添加 var 或 val 将使其成为公共的(即生成方法访问器和修改器)。
如果类没有主体,则可以省略 {}
,即
class EmptyClass
类实例化:
如果编译器可以推断出通用参数,则可以省略它们。但是请注意,如果您的类型不匹配,则始终推断类型参数以使其匹配。因此,如果不指定类型,您可能无法得到您所期望的——也就是说,给定
class D[T](val x:T, val y:T);
这会给你一个类型错误(Int found, expected String)
var zz = new D[String]("Hi1", 1) // type error
虽然这很好用:
var z = new D("Hi1", 1)
== D{def x: Any; def y: Any}
因为类型参数 T 被推断为两者中最不常见的超类型 - Any。
函数定义:
如果函数返回 Unit(无),则可以删除 =
。
如果函数是单个语句,则可以删除函数体的 {}
,但前提是该语句返回一个值(您需要 =
符号),即
def returnAString = "Hi!"
但这不起作用:
def returnAString "Hi!" // Compile error - '=' expected but string literal found."
如果可以推断出函数的返回类型,则可以省略它(递归方法必须指定其返回类型)。
如果函数不接受任何参数,则可以删除 ()
,也就是说,
def endOfString {
return "myDog".substring(2,1)
}
按照惯例,它是为没有副作用的方法保留的——稍后会详细介绍。
在定义 pass by name 参数时,()
本身并没有真正被删除,但它实际上是一个在语义上完全不同的符号,即,
def myOp(passByNameString: => String)
说 myOp 接受一个按名称传递的参数,它产生一个字符串(也就是说,它可以是一个返回字符串的代码块)而不是函数参数,
def myOp(functionParam: () => String)
它说 myOp
接受一个具有零参数的函数并返回一个字符串。
(请注意,按名称传递的参数被编译成函数;它只是使语法更好。)
如果函数只接受一个参数,则可以在函数参数定义中删除 ()
,例如:
def myOp2(passByNameString:(Int) => String) { .. } // - You can drop the ()
def myOp2(passByNameString:Int => String) { .. }
但如果它需要多个参数,则必须包含 ():
def myOp2(passByNameString:(Int, String) => String) { .. }
声明:
可以删除 .
以使用运算符表示法,它只能用于中缀运算符(带参数的方法的运算符)。有关详细信息,请参阅 Daniel's answer。
.也可以删除后缀函数列表尾
() 可以去掉后缀运算符 list.tail
() 不能与定义为的方法一起使用:def aMethod = "hi!" // 方法定义中缺少 () aMethod // 有效 aMethod() // 调用方法时编译错误
因为按照惯例,这种表示法是为没有副作用的方法保留的,比如 List#tail(也就是说,没有副作用的函数的调用意味着该函数除了返回值之外没有可观察的效果)。
() 可以在传入单个参数时删除运算符符号
() 可能需要使用不在语句末尾的后缀运算符
() 可能需要指定嵌套语句、匿名函数的结尾或带有多个参数的运算符
调用带有函数的函数时,不能在内部函数定义中省略 (),例如:
def myOp3(paramFunc0:() => String) {
println(paramFunc0)
}
myOp3(() => "myop3") // Works
myOp3(=> "myop3") // Doesn't work
当调用一个采用名称参数的函数时,您不能将参数指定为无参数匿名函数。例如,给定:
def myOp2(passByNameString:Int => String) {
println(passByNameString)
}
您必须将其称为:
myOp("myop3")
或者
myOp({
val source = sourceProvider.source
val p = myObject.findNameFromSource(source)
p
})
但不是:
myOp(() => "myop3") // Doesn't work
IMO,过度使用删除返回类型可能对重用代码有害。只需查看规范,即可找到由于代码中缺乏明确信息而导致可读性降低的一个很好的例子。实际找出变量类型的间接级别数可能很疯狂。希望更好的工具可以避免这个问题并保持我们的代码简洁。
(好的,为了编写一个更完整、更简洁的答案(如果我遗漏了什么,或者有什么错误/不准确的地方,请发表评论),我已经添加到答案的开头。请注意这不是一种语言规范,所以我不想让它在学术上完全正确——更像是一张参考卡。)
一系列报价,让您深入了解各种情况......
就个人而言,我认为规范中会有更多内容。我敢肯定一定有,我只是没有在寻找合适的词......
但是有几个来源,我将它们收集在一起,但没有什么真正完整/全面/可理解/可以向我解释上述问题......:
“如果方法体有多个表达式,则必须用花括号 {...} 将其括起来。如果方法体只有一个表达式,则可以省略大括号。”
从 chapter 2, "Type Less, Do More", of Programming Scala:
“上部方法的主体出现在等号'='之后。为什么是等号?为什么不只是花括号{...},就像在Java中一样?因为分号,函数返回类型,方法参数列表,甚至花括号有时省略, 使用等号可以防止几种可能的解析歧义. 使用等号也提醒我们, 在 Scala 中, 即使函数也是值, 这与 Scala 对函数式编程的支持一致, 在第 8 章, 函数式编程中进行了更详细的描述斯卡拉。”
从 chapter 1, "Zero to Sixty: Introducing Scala", of Programming Scala:
“一个没有参数的函数可以在没有括号的情况下声明,在这种情况下它必须在没有括号的情况下调用。这为统一访问原则提供了支持,使得调用者不知道符号是变量还是没有括号的函数参数。如果返回一个值(即返回类型不是Unit),则函数体前面加“=”,但是当类型为Unit时,返回类型和“=”可以省略(即看起来像一个过程而不是函数)。不需要在主体周围使用大括号(如果主体是单个表达式);更准确地说,函数的主体只是一个表达式,任何具有多个部分的表达式都必须用大括号括起来(有一部分的表达式可以选择用大括号括起来)。” “可以在没有点和括号的情况下调用具有零个或一个参数的函数。但是任何表达式都可以在其周围有括号,因此您可以省略点并仍然使用括号。而且由于您可以在任何可以使用括号的地方使用大括号,您可以省略点并放入大括号,可以包含多个语句。没有参数的函数可以不带括号调用。例如,String 上的 length() 函数可以调用为“abc”.length 而不是“abc”。 length(). 如果函数是一个不带括号定义的 Scala 函数,那么该函数必须在不带括号的情况下调用。按照惯例,不带参数的函数有副作用,例如 println,用括号调用;没有副作用的函数是不带括号调用。”
来自博文Scala Syntax Primer:
“过程定义是一个函数定义,其中省略了结果类型和等号;它的定义表达式必须是一个块。例如,def f (ps) {stats} 等价于 def f (ps): Unit = {stats }. 示例 4.6.3 下面是一个名为 write 的过程的声明和定义:
trait Writer {
def write(str: String)
}
object Terminal extends Writer {
def write(str: String) { System.out.println(str) }
}
上面的代码隐式补全为以下代码:
trait Writer {
def write(str: String): Unit
}
object Terminal extends Writer {
def write(str: String): Unit = { System.out.println(str) }
}"
从语言规范:
“对于只接受单个参数的方法,Scala 允许开发人员将 . 替换为空格并省略括号,从而启用我们的插入运算符示例中显示的运算符语法。这种语法在 Scala API 的其他地方使用,例如作为构造 Range 实例:
val firstTen:Range = 0 to 9
在这里,to(Int) 是一个在类中声明的普通方法(实际上这里有一些更隐式的类型转换,但你明白了)。
来自 Scala for Java Refugees Part 6: Getting Over Java:
“现在,当你尝试“m 0”时,Scala 将它作为一元运算符丢弃,理由是它不是一个有效的运算符(~、!、- 和 +)。它发现“m”是一个有效的对象——它是一个函数,而不是方法,所有函数都是对象。由于“0”不是有效的 Scala 标识符,因此它既不能是中缀也不能是后缀运算符。因此,Scala 抱怨它期望“;”——这将分隔两个(几乎)有效的表达式:“m”和“0”。如果你插入它,那么它会抱怨 m 需要一个参数,或者,如果失败,一个“_”将它变成一个部分应用的函数。” “我相信运算符语法样式仅在您在左侧有一个显式对象时才有效。该语法旨在让您以自然的方式表达“操作数运算符操作数”样式的操作。”
Which characters can I omit in Scala?
但也让我感到困惑的是这句话:
“需要有一个对象来接收方法调用。例如,您不能执行“println “Hello World!””,因为 println 需要一个对象接收者。你可以做“Console println “Hello World!””,满足需求。”
因为据我所知,有一个对象可以接电话……
我发现遵循这个经验法则更容易:在表达式中,空间在方法和参数之间交替。在您的示例中,(service.findAllPresentations.get.first.votes.size) must be equalTo(2)
解析为 (service.findAllPresentations.get.first.votes.size).must(be)(equalTo(2))
。请注意,2 周围的括号比空格具有更高的关联性。点还具有更高的关联性,因此 (service.findAllPresentations.get.first.votes.size) must be.equalTo(2)
将解析为 (service.findAllPresentations.get.first.votes.size).must(be.equalTo(2))
。
service findAllPresentations get first votes size must be equalTo 2
解析为 service.findAllPresentations(get).first(votes).size(must).be(equalTo).2
。
实际上,在二读时,也许这是关键:
Scala 允许开发人员使用仅采用单个参数的方法来替换 .带空格并省略括号
如博文中所述:http://www.codecommit.com/blog/scala/scala-for-java-refugees-part-6。
所以也许这实际上是一个非常严格的“语法糖”,它只适用于你有效调用方法的情况下,对象上需要一个参数。例如
1 + 2
1.+(2)
没有别的了。
这将解释我在问题中的示例。
但正如我所说,如果有人能指出语言规范中指定的确切位置,将不胜感激。
好的,一些好人(来自#scala 的 paulp_)指出了该信息在语言规范中的位置:
6.12.3:运算符的优先级和关联性决定了表达式部分的分组如下。如果表达式中有多个中缀操作,则具有较高优先级的运算符比具有较低优先级的运算符绑定得更紧密。如果有连续的中缀操作 e0 op1 e1 op2 。 . .opn en 与运算符 op1, . . . , opn 的优先级相同,则所有这些运算符必须具有相同的结合性。如果所有运算符都是左结合的,则序列被解释为 (. . . (e0 op1 e1) op2 . . .) opn en。否则,如果所有运算符都是右关联的,则序列被解释为 e0 op1 (e1 op2 (. . .opn en) . . .)。后缀运算符的优先级始终低于中缀运算符。例如,e1 op1 e2 op2 总是等价于 (e1 op1 e2) op2。左结合运算符的右手操作数可以由括在括号中的几个参数组成,例如 e op (e1, . . . ,en)。然后这个表达式被解释为 e.op(e1, . . . ,en)。左关联二元运算 e1 op e2 被解释为 e1.op(e2)。如果 op 是右关联的,则相同的操作被解释为 { val x=e1; e2.op(x) },其中 x 是一个新名称。
嗯 - 对我来说,它与我所看到的不相符,或者我只是不明白;)
没有。您可能会收到有关该功能是否有副作用的建议。这是假的。更正是在 Scala 允许的合理范围内不使用副作用。如果它不能,那么所有的赌注都被取消了。所有赌注。使用括号是集合“all”的一个元素,是多余的。一旦所有赌注都关闭,它就不会提供任何价值。
这个建议本质上是对失败的 effect system 的尝试(不要混淆:它没有其他效果系统有用)。
尽量不要产生副作用。在那之后,接受所有的赌注。隐藏效果系统的事实上的句法符号可以而且确实只会造成伤害。