【问题标题】:Scala equivalent to Haskell's where-clauses?Scala 相当于 Haskell 的 where 子句?
【发布时间】:2010-11-20 00:54:25
【问题描述】:

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

编辑:

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

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

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

【问题讨论】:

  • 什么是 Haskell where 子句?
  • 这是一个关键字,允许您定义使用where 关键字的函数的本地“变量”(因为没有更好的术语)。
  • 编辑了我的答案,讨论了函数使用后的范围和定义。可以做到(...ish),但其中一些并不漂亮。
  • 根据您的评论编辑了我的答案。正如我所说,由于副作用,在使用它们的表达式之后定义函数有点尴尬,尤其是当这些表达式必须就地求值时!
  • 有人可以使用宏实现<smth> where <smth> 构造; Haskell do 符号也可以这样实现——for-comprehensions 对于一元代码来说有点难看......

标签: scala haskell language-features where-clause


【解决方案1】:

在 Hakell 中,where 子句包含函数的局部定义。 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 的回答以获取此类示例以及有关该主题的一些详细说明。

EDIT 2:添加了关于lazyvars 和vals 的讨论。

懒惰的`var`和`val`

Edward Kmett 的回答正确地指出了 Haskell 的 where 子句具有惰性和纯洁性。你可以在 Scala 中使用lazy 变量做一些非常相似的事情。这些仅在需要时实例化。考虑以下示例:

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

另外请注意,每个块必须以带有定义的表达式 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)
}

如果vals 是lazy 或者它们是defs,您给出的示例确实有效。

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

【讨论】:

  • 删除了我的例子。去吧。
  • 这真的很奇怪。您的示例有效,但请尝试以下示例: def notGood():Int = { val a = b val b = 1 a } 我得到的错误是“前向引用扩展了值 a 的定义”。也不适用于“def b = 1”。
  • 是的,现在我正在编写 Haskell,我真的习惯了使用 where 以及它释放了多少空间——它可以让你在不深入了解细节的情况下跟踪名称函数的主体。局部值是ok,但它们仍然会分散注意力,因为它们需要函数的主体之前出现。
【解决方案2】:

类似,是的。我不会像Flaviu那样详细介绍,但我会给出一个来自维基百科的例子。

哈斯克尔:

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 做它所做的事情有多种原因,尽管我喜欢隐式的模式匹配定义。 :-)

【讨论】:

  • 尽管我不了解 Haskell,但对我而言,Haskell 的代码看起来更具描述性和简洁性。可能会多出 30%。
  • @ses 另一个有趣的符号是 Haskell 中运算符优先级的 “语法糖” - what's $ in Haskell
【解决方案3】:

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

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

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

不幸的是,Scala 没有这些属性,因此无法提供与 Haskell 的 where 子句完全等效的功能。

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

【讨论】:

    【解决方案4】:

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

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

    本着标准化 where -&gt; 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
    }
    

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

    【讨论】:

    • 像这样的宏会很棒!实际上,将 EXPN2 放在 EXPN1 之前就足够了。
    猜你喜欢
    • 2012-11-11
    • 1970-01-01
    • 2019-03-22
    • 2012-05-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-02-11
    相关资源
    最近更新 更多