是否可以在Scala中使用类似where-clauses的内容?也许有一些我没想到的技巧?
编辑:
感谢您的所有答案,我们非常感谢。总结一下: 本地变量,vals和defs可以用来实现几乎相同的东西。对于惰性求值,可以使用lazy val(带隐式缓存)或函数定义。确保功能纯度留给程序员。
现在只剩下一个问题:是否有一种方法可以在使用它们的表达式之后放置值或函数定义?有时这似乎更清晰。这可以使用类或对象的字段/方法,但它似乎不适用于方法。
到目前为止,答案中没有提到另一件事。 where-clause也限制了它们中定义的表达式的范围。我还没有找到在Scala中实现这一目标的方法。
答案 0 :(得分:24)
在Hakell中,子句对函数保持局部定义。 Scala没有明确的where子句,但是通过使用本地var
,val
和def
可以实现相同的功能。
在Scala中:
def foo(x: Int, y: Int): Int = {
val a = x + y
var b = x * y
a - b
}
在Haskell:
foo :: Integer -> Integer -> Integer
foo x y = a - b
where
a = x + y
b = x * y
在Scala中
def foo(x: Int, y: Int): Int = {
def bar(x: Int) = x * x
y + bar(x)
}
在Haskell
foo :: Integer -> Integer -> Integer
foo x y = y + bar x
where
bar x = x * x
如果我在Haskell示例中出现任何语法错误,请更正我,因为我目前没有在此计算机上安装Haskell编译器:)。
可以用类似的方式实现更复杂的示例(例如使用两种语言都支持的模式匹配)。本地函数与任何其他函数的语法完全相同,只是它们的作用域是它们所在的块。
编辑:另请参阅Daniel关于此类示例的回答以及对该主题的一些阐述。
编辑2 :添加了关于lazy
var
和val
s的讨论。
Edward Kmett的回答正确地指出,Haskell的where子句有懒惰和纯洁。您可以使用lazy
变量在Scala中执行非常类似的操作。这些仅在需要时实例化。请考虑以下示例:
def foo(x: Int, y: Int) = {
print("--- Line 1: ");
lazy val lazy1: Int = { print("-- lazy1 evaluated "); x^2}
println();
print("--- Line 2: ");
lazy val lazy2: Int = { print("-- lazy2 evaluated "); y^2}
println();
print("--- Line 3: ");
lazy val lazy3: Int = { print("-- lazy3 evaluated ")
while(true) {} // infinite loop!
x^2 + y^2 }
println();
print("--- Line 4 (if clause): ");
if (x < y) lazy1 + lazy2
else lazy2 + lazy1
}
此处lazy1
,lazy2
和lazy3
都是惰性变量。 lazy3
永远不会被实例化(因此这段代码永远不会进入无限循环),lazy1
和lazy2
的实例化顺序取决于函数的参数。例如,当您致电foo(1,2)
时,您会在lazy1
之前获得lazy2
实例化,当您致电foo(2,1)
时,您将获得相反的结果。尝试使用scala解释器中的代码并查看打印输出! (我不会把它放在这里,因为这个答案已经很长了。)
如果使用无参数函数而不是惰性变量,则可以获得类似的结果。在上面的示例中,您可以使用lazy val
替换每个def
并获得类似的结果。区别在于惰性变量被缓存(也就是仅被评估一次),但每次调用时都会评估def
。
编辑3:添加了关于范围界定的讨论,请参阅问题。
本地定义具有声明它们的块的范围,正如预期的那样(大多数情况下,在罕见的情况下它们可以逃脱块,就像使用中流变量绑定一样)在for循环中)。因此,可以使用本地var
,val
和def
来限制表达式的范围。请看以下示例:
object Obj {
def bar = "outer scope"
def innerFun() {
def bar = "inner scope"
println(bar) // prints inner scope
}
def outerFun() {
println(bar) // prints outer scope
}
def smthDifferent() {
println(bar) // prints inner scope ! :)
def bar = "inner scope"
println(bar) // prints inner scope
}
def doesNotCompile() {
{
def fun = "fun" // local to this block
42 // blocks must not end with a definition...
}
println(fun)
}
}
innerFun()
和outerFun()
都按预期行事。 bar
中innerFun()
的定义隐藏了封闭范围中定义的bar
。此外,函数fun
是其封闭块的本地,因此不能使用它。方法doesNotCompile()
...无法编译。值得注意的是,来自println()
方法的两个smthDifferent()
调用都打印inner scope
。因此,是的,你可以在方法中使用它们之后放置定义!我不建议,因为我认为这是不好的做法(至少在我看来)。在类文件中,您可以根据需要排列方法定义,但我会在使用之前将所有def
保留在函数内。并且val
和var
s ......好吧......我发现在使用它们之后放置它们很尴尬。
另请注意,每个块必须以带有定义的表达式 not 结束,因此您不能在块的末尾包含所有定义。我可能会把所有定义放在一个块的开头,然后写出我的所有逻辑,在该块的结尾产生一个结果。这样做似乎更自然,而不是:
{
// some logic
// some defs
// some other logic, returning the result
}
正如我之前所说,你不能只用// some defs
结束一个块。这是Scala与Haskell略有不同的地方:)。
编辑4 :在Kim的评论提示下,详细阐述了使用它们后的内容。
在具有副作用的语言中实现这是一件棘手的事情。在纯粹的无副作用世界中,顺序并不重要(方法不会取决于任何副作用)。但是,由于Scala允许副作用,因此定义函数的位置很重要。此外,当您定义val
或var
时,必须对右侧进行评估,以便实例化val
。请考虑以下示例:
// does not compile :)
def foo(x: Int) = {
// println *has* to execute now, but
// cannot call f(10) as the closure
// that you call has not been created yet!
// it's similar to calling a variable that is null
println(f(10))
var aVar = 1
// the closure has to be created here,
// as it cannot capture aVar otherwise
def f(i: Int) = i + aVar
aVar = aVar + 1
f(10)
}
如果val
为lazy
或def
,则您提供的示例可以正常工作。
def foo(): Int = {
println(1)
lazy val a = { println("a"); b }
println(2)
lazy val b = { println("b"); 1 }
println(3)
a + a
}
此示例也很好地展示了工作中的缓存(尝试将lazy val
更改为def
并查看会发生什么:)
我仍然处在一个有副作用的世界里,最好坚持使用之前的定义。以这种方式阅读源代码更容易。
-- Flaviu Cipcigan
答案 1 :(得分:5)
类似,是的。我不会像Flaviu那样详细介绍细节,但我会举一个维基百科的例子。
Haskell中:
calc :: String -> [Float]
calc = foldl f [] . words
where
f (x:y:zs) "+" = (y + x):zs
f (x:y:zs) "-" = (y - x):zs
f (x:y:zs) "*" = (y * x):zs
f (x:y:zs) "/" = (y / x):zs
f xs y = read y : xs
这些定义只是calc
的本地定义。所以,在Scala中,我们会这样做:
def calc(s: String): List[Float] = {
def f(s: List[Float], op: String) = (s, op) match {
case (x :: y :: zs, "+") => (y + x) :: zs
case (x :: y :: zs, "-") => (y - x) :: zs
case (x :: y :: zs, "*") => (y * x) :: zs
case (x :: y :: zs, "/") => (y / x) :: zs
case (xs, y) => read(y) :: xs
}
s.words.foldLeft(List[Float]())(f)
}
由于Scala没有等效的read
,您可以将其定义如下,以便运行此特定示例:
def read(s: String) = s.toFloat
Scala没有words
,这让我很懊恼,尽管很容易定义:
implicit toWords(s: String) = new AnyRef { def words = s.split("\\s") }
现在,由于各种原因,Haskell的定义更加紧凑:
它有一个更强大的类型推断,因此不需要声明calc
本身的类型。 Scala无法做到这一点,因为有意识的设计决定是使用类模型面向对象。
它有一个隐式模式匹配定义,而在Scala中,你必须声明该函数,然后声明模式匹配。
就简洁而言,它对currying的处理明显优于Scala。这是关于班级模型和操作符号的各种决定的结果,其中处理curry被认为不那么重要。
Haskell对列表有特殊处理,可以为它们提供更简洁的语法。在Scala中,列表被视为与任何其他类一样,相反,要确保任何类可以像Scala中的List一样紧凑。
因此,尽管我喜欢隐式模式匹配定义,但是为什么Scala会做它的功能有多种原因。 : - )
答案 2 :(得分:4)
您可以使用var
和val
来提供局部变量,但这与Haskell的where
子句在两个相当重要的方面不同:懒惰和纯洁。
Haskell的where
子句很有用,因为懒惰和纯度使编译器只能实例化实际使用的where子句中的变量。
这意味着您可以编写一个很长的本地定义,并在其下面放置where
子句,并且不需要考虑效果的顺序(因为纯度),并且如果每个代码都不需要考虑branch需要where子句中的所有定义,因为laziness允许where子句中未使用的术语像thunk一样存在,pure允许编译器在不使用时从结果代码中选择忽略。
where
子句。
您需要手动分解出您使用的var
和val
,并将其放在使用它们的语句之前,就像ML let
语句一样。
答案 3 :(得分:3)
Haskell 将值绑定到包含let
和where
表达式的名称。我非常确定在评估或代码生成之前,任何where
表达式都可以标准化为let表达式(无论评估顺序如何)。
Scala 编码与范围内val
语句的绑定。编译器确保分配给该名称的值不会更改。这些看起来很像,因为它们是按顺序执行的。这与我们希望我们的代码阅读的内容相反:首先显示的主要思想,以及之后表达的支持细节。这是我们审美负担的原因。
本着标准化where -> let
的精神,我们可以通过一种方式编码Scala中的哪个位置可能与宏(我没有尝试,只是假设)EXPN1 where { EXPN2 }
这样EXPN1是任何有效的表达式,EXPN2可以是扩展到的对象声明中的任何有效内容:
object $genObjectname { EXPN2 }
{ import $genObjectName._; EXPN1 }
使用示例:
sausageStuffer compose meatGrinder where {
val sausageStuffer = ... // you really don't want to know
val meatGrinder = ... // not that pretty
}
我感觉到你的痛苦。如果我做一个有效的宏,我会回复你。