16

是否可以使用类似于 Scala 中的 where 子句的东西?也许有一些我没有想到的技巧?

编辑:

感谢您的所有回答,非常感谢他们。总结一下:局部的vars、vals和defs可以用来实现几乎一样的东西。对于惰性求值,可以使用惰性 val(带有隐式缓存)或函数定义。确保功能的纯度留给程序员。

现在只剩下一个问题:有没有一种方法可以将值或函数定义放在使用它们的表达式之后?有时这似乎更清楚。这对于类或对象的字段/方法是可能的,但它似乎不适用于方法。

到目前为止,答案中没有提到的另一件事。where 子句还限制了其中定义的表达式的范围。我也没有找到在 Scala 中实现这一目标的方法。

4

4 回答 4

25

在 Hakell 中,where 子句包含函数的局部定义。Scala 没有明确的 where 子句,但是可以通过使用 local 和 来实现相同varval功能def

本地`var`和`val`

在斯卡拉:

def foo(x: Int, y: Int): Int = {
  val a = x + y 
  var b = x * y
  a - b
}

在哈斯克尔:

foo :: Integer -> Integer -> Integer 
foo x y = a - b
        where 
          a = x + y
          b = x * y

本地`def`

在斯卡拉

def foo(x: Int, y: Int): Int = {
  def bar(x: Int) = x * x
  y + bar(x)
}

在哈斯克尔

foo :: Integer -> Integer -> Integer 
foo x y = y + bar x
         where 
           bar x = x * x

如果我在 Haskell 示例中犯了任何语法错误,请纠正我,因为我目前没有在这台计算机上安装 Haskell 编译器:)。

更复杂的示例可以通过类似的方式实现(例如使用两种语言都支持的模式匹配)。局部函数与任何其他函数具有完全相同的语法,只是它们的作用域是它们所在的块。

编辑:另请参阅Daniel对此类示例的回答以及有关该主题的一些详细说明。

编辑 2:添加了关于lazy vars 和vals 的讨论。

懒惰的`var`和`val`

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,lazy2lazy3都是惰性变量。lazy3永远不会被实例化(因此这段代码永远不会进入无限循环)并且实例化的顺序lazy1取决于lazy2函数的参数。例如,当您调用时,foo(1,2)您将lazy1在之前实例化,lazy2而当您调用时,foo(2,1)您将得到相反的结果。在 scala 解释器中尝试代码并查看打印输出!(我不会把它放在这里,因为这个答案已经很长了)。

如果您使用无参数函数而不是惰性变量,您可以获得类似的结果。在上面的示例中,您可以将 every 替换lazy val为 adef并获得类似的结果。不同之处在于惰性变量被缓存(也就是只评估一次),但def每次调用它时都会评估 a 。

编辑 3:添加了关于范围界定的讨论,请参阅问题。

本地定义的范围

正如预期的那样,局部定义具有声明它们的块的范围(嗯,大多数情况下,在极少数情况下,它们可以逃脱该块,例如在 for 循环中使用中流变量绑定时)。因此 localvarvaldef用于限制表达式的范围。举个例子:

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()预期运行。barin的定义innerFun()隐藏bar了封闭范围内的定义。此外,该函数fun对其封闭块是本地的,因此不能以其他方式使用。该方法doesNotCompile()... 无法编译。有趣的是,这两个println()调用都来自smthDifferent()方法 print inner scope。因此,是的,您可以将定义放在方法中使用后!不过我不推荐,因为我认为这是不好的做法(至少在我看来)。在类文件中,您可以随意安排方法定义,但我会def在使用之前将所有 s 保留在函数中。还有vals 和vars ……嗯……我觉得用完之后再放就很尴尬。

另请注意,每个块必须以表达式而不是定义结尾,因此您不能将所有定义都放在块的末尾。我可能会将所有定义放在块的开头,然后在该块的末尾编写所有产生结果的逻辑。这样看起来确实更自然,而不是:

{
// some logic

// some defs

// some other logic, returning the result
}    

正如我之前所说,你不能只用// some defs. 这是 Scala 与 Haskell 略有不同的地方:)。

编辑 4 :根据Kim的评论,在使用它们后详细说明了定义内容。

使用它们后定义“东西”

在具有副作用的语言中实现这是一件棘手的事情。在纯无副作用的世界中,顺序并不重要(方法不依赖于任何副作用)。但是,由于 Scala 允许副作用,定义函数的位置确实很重要。此外,当您定义valor时var,必须在适当的位置评估右侧以实例化 that 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如果s 是lazy或它们是s ,您给出的示例确实有效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 valtodef看看会发生什么:)

我仍然在一个有副作用的世界里,最好在使用它们之前坚持定义。这样更容易阅读源代码。

-- Flaviu Cipcigan

于 2009-08-16T13:59:17.690 回答
5

类似的,是的。我不会像Flaviu一样详细介绍,但我会给出一个来自 Wikipedia 的示例。

哈斯克尔:

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 中,您必须先声明函数,然后再声明模式匹配。

  • 就简洁性而言,它对柯里化的处理明显优于 Scala。这是关于类模型和运算符符号的各种决定的结果,其中处理柯里化被认为不那么重要。

  • Haskell 对列表进行了特殊处理,从而可以为它们提供更简洁的语法。在 Scala 中,列表被视为与任何其他类一样,正在努力确保任何类都可以像 Scala 中的 List 一样紧凑。

因此,Scala 做它所做的事情有多种原因,尽管我喜欢隐式的模式匹配定义。:-)

于 2009-08-16T14:36:46.133 回答
4

您可以使用varandval提供局部变量,但这where在两个相当重要的方面与 Haskell 的子句不同:惰性和纯度。

Haskell 的where子句很有用,因为惰性和纯度允许编译器仅实例化 where 子句中实际使用的变量。

这意味着您可以编写一个很长的本地定义,并where在其下方删除一个子句,并且不需要考虑效果顺序(因为纯度),也不需要考虑每个单独的代码分支是否需要where 子句,因为惰性允许 where 子句中未使用的术语像 thunk 一样存在,这种纯度允许编译器在不使用时选择从结果代码中删除。

不幸的是,Scala 没有这些属性,因此无法提供与 Haskellwhere子句完全等价的内容。

您需要手动分解出您使用的vars 和vals 并将它们放在使用它们的语句之前,就像 MLlet语句一样。

于 2009-08-16T17:02:18.303 回答
3

Haskell使用和表达式将值绑定到名称。我很确定在评估或代码生成之前,任何表达式都可以标准化为 let 表达式(无论评估顺序如何)。letwhere where

Scala使用作用域内的语句对绑定进行编码。编译器确保分配给该名称的值不会改变。这些似乎是 let-like,因为它们是按从前到后的顺序执行的。这与我们希望我们的代码阅读的内容相反:首先显示主要思想,然后表达支持细节。这就是我们审美负担的原因。val

本着标准化的精神,where -> let我们可以用宏(我没有尝试过,只是假设)来编码 Scala 中 where 的一种方式,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
}

我感觉到你的痛苦。如果我制作了一个工作宏,我会回复你。

于 2013-06-12T02:50:49.820 回答