Scala相当于Haskell的where-clauses?

时间:2009-08-16 13:45:03

标签: scala haskell language-features where-clause

是否可以在Scala中使用类似where-clauses的内容?也许有一些我没想到的技巧?

编辑:

感谢您的所有答案,我们非常感谢。总结一下: 本地变量,vals和defs可以用来实现几乎相同的东西。对于惰性求值,可以使用lazy val(带隐式缓存)或函数定义。确保功能纯度留给程序员。

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

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

4 个答案:

答案 0 :(得分:24)

在Hakell中,子句对函数保持局部定义。 Scala没有明确的where子句,但是通过使用本地varvaldef可以实现相同的功能。

本地`var`和`val`

在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

本地`def`

在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 varval s的讨论。

懒惰`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
}

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

如果使用无参数函数而不是惰性变量,则可以获得类似的结果。在上面的示例中,您可以使用lazy val替换每个def并获得类似的结果。区别在于惰性变量被缓存(也就是仅被评估一次),但每次调用时都会评估def

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

本地定义的范围

本地定义具有声明它们的块的范围,正如预期的那样(大多数情况下,在罕见的情况下它们可以逃脱块,就像使用中流变量绑定一样)在for循环中)。因此,可以使用本地varvaldef来限制表达式的范围。请看以下示例:

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()都按预期行事。 barinnerFun()的定义隐藏了封闭范围中定义的bar。此外,函数fun是其封闭块的本地,因此不能使用它。方法doesNotCompile() ...无法编译。值得注意的是,来自println()方法的两个smthDifferent()调用都打印inner scope。因此,是的,你可以在方法中使用它们之后放置定义!我不建议,因为我认为这是不好的做法(至少在我看来)。在类文件中,您可以根据需要排列方法定义,但我会在使用之前将所有def保留在函数内。并且valvar s ......好吧......我发现在使用它们之后放置它们很尴尬。

另请注意,每个块必须以带有定义的表达式 not 结束,因此您不能在块的末尾包含所有定义。我可能会把所有定义放在一个块的开头,然后写出我的所有逻辑,在该块的结尾产生一个结果。这样做似乎更自然,而不是:

{
// some logic

// some defs

// some other logic, returning the result
}    

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

编辑4 :在Kim的评论提示下,详细阐述了使用它们后的内容。

使用后定义'东西'

在具有副作用的语言中实现这是一件棘手的事情。在纯粹的无副作用世界中,顺序并不重要(方法不会取决于任何副作用)。但是,由于Scala允许副作用,因此定义函数的位置很重要。此外,当您定义valvar时,必须对右侧进行评估,以便实例化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)
}

如果vallazydef,则您提供的示例可以正常工作。

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)

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

Haskell的where子句很有用,因为懒惰和纯度使编译器只能实例化实际使用的where子句中的变量。

这意味着您可以编写一个很长的本地定义,并在其下面放置where子句,并且不需要考虑效果的顺序(因为纯度),并且如果每个代码都不需要考虑branch需要where子句中的所有定义,因为laziness允许where子句中未使用的术语像thunk一样存在,pure允许编译器在不使用时从结果代码中选择忽略。

遗憾的是,Scala既没有这些属性,也没有完全等同于Haskell的where子句。

您需要手动分解出您使用的varval,并将其放在使用它们的语句之前,就像ML let语句一样。

答案 3 :(得分:3)

Haskell 值绑定到包含letwhere 表达式的名称。我非常确定在评估或代码生成之前,任何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
}

我感觉到你的痛苦。如果我做一个有效的宏,我会回复你。