代码之家  ›  专栏  ›  技术社区  ›  Kim Stebel

scala相当于haskell的where子句?

  •  16
  • Kim Stebel  · 技术社区  · 15 年前

    是否可以使用类似于scala中的where子句的内容?也许有什么我没想到的把戏?

    编辑:

    谢谢你的回答,非常感谢。总而言之: 本地var、val和def可以用来实现几乎相同的功能。对于Lazy Evaluation,可以使用Lazy Val(带有隐式缓存)或函数定义。确保功能的纯度留给程序员。

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

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

    4 回复  |  直到 11 年前
        1
  •  24
  •   Community T.Woody    7 年前

    在Hakell语言中,where子句保存函数的局部定义。scala没有显式的where子句,但是可以通过在本地 var , val def .

    本地'var'和'val`

    斯卡拉:

    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”

    在斯卡拉

    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 S和 瓦尔 S.

    懒惰的'var'和'val`

    Edward Kmett 他的回答正确地指出了哈斯克尔的where子句具有惰性和纯洁性。在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 从未实例化(因此此代码从未进入无限循环)和的实例化顺序 LaZY-1 LaZy2 取决于函数的参数。例如,当你打电话 foo(1,2) 你会得到 LaZY-1 之前已实例化 LaZy2 当你打电话的时候 foo(2,1) 你会得到相反的结果。尝试在scala解释器中输出代码,然后查看打印输出!(我不会把它放在这里,因为这个答案已经很长了)。

    如果不使用参数函数而使用惰性变量,则可以获得类似的结果。在上面的示例中,可以将 lazy val 用一个 DEF 并取得了类似的成果。不同的是,惰性变量被缓存(也就是只计算一次),但是 DEF 每次调用时都对其进行评估。

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

    地方定义的范围

    本地定义具有它们声明的块的范围,如预期的那样(大多数情况下,在 稀有的 它们可以逃离块的情况,比如在for循环中使用中间流变量绑定时)。因此当地 var , 瓦尔 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 在里面 内网() 隐藏 酒吧 在封闭范围中定义。此外,功能 fun 是其封闭块的局部,因此不能以其他方式使用。方法 doesNotCompile() …不编译。有趣的是,这两者都是 println() 来自 smthDifferent() 方法打印 inner scope .因此,是的,您可以在方法内部使用定义之后再进行定义!不过,我不建议这样做,因为我认为这是不好的做法(至少在我看来)。在类文件中,可以根据需要排列方法定义,但我将保留 DEF 在函数内部使用。和 瓦尔 S和 var S…好。。。我觉得用完后放起来很难。

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

    {
    // some logic
    
    // some defs
    
    // some other logic, returning the result
    }    
    

    正如我之前所说,不能以 // some defs . 这就是scala和haskell的细微差别。

    编辑4 :详细说明了使用后的定义,提示 Kim 的评论。

    使用后定义“东西”

    在一种有副作用的语言中实现这一点很困难。在一个纯粹的无副作用的世界里,秩序并不重要(方法不依赖于任何副作用)。但是,由于scala允许副作用,定义函数的位置 物质。此外,当您定义 瓦尔 var ,必须在适当的位置对右侧进行计算,以便实例化 瓦尔 . 请考虑以下示例:

    // 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)
    }
    

    但是,如果 瓦尔 S是 懒惰的 或者它们是 DEF S.

    def foo(): Int = {
      println(1)
      lazy val a = { println("a"); b }
      println(2)
      lazy val b = { println("b"); 1 }
      println(3)
      a + a
    }
    

    此示例还很好地显示了工作时的缓存(尝试更改 惰性缬氨酸 DEF 看看会发生什么。)

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

    -- Flaviu Cipcigan

        2
  •  5
  •   Community T.Woody    7 年前

    类似的,是的。我不会细说,因为 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
    

    斯卡拉没有 words 或者,让我很懊恼的是,虽然很容易定义:

    implicit toWords(s: String) = new AnyRef { def words = s.split("\\s") }
    

    现在,haskell的定义更加紧凑,原因有很多:

    • 它有一个更强大的类型推断,所以除了 需要声明自己。scala不能这样做,因为有意识的设计决策是用类模型面向对象的。

    • 它有一个隐式模式匹配定义,而在scala中,您必须声明函数,然后声明模式匹配。

    • 就其简洁性而言,它对咖喱的处理只是简单的优于斯卡拉。这是关于类模型和运算符符号的各种决定的结果,在这些决定中,处理货币被认为不那么重要。

    • haskell对列表进行了特殊处理,使列表的语法更加简洁。在scala中,列表像其他类一样被处理,相反,要确保 任何 类可以像scala中的list一样紧凑。

    所以,scala做它所做的有各种各样的原因,尽管我喜欢一个隐式模式匹配定义。-)

        3
  •  4
  •   Edward Kmett    15 年前

    你可以使用 var val 提供局部变量,但这与Haskell的不同 where 条款在两个相当重要的方面:懒惰和纯洁。

    哈斯克尔 哪里 子句很有用,因为惰性和纯粹性使编译器只能实例化实际使用的WHERE子句中的变量。

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

    不幸的是,scala没有这些属性,因此不能提供与haskell的完全等效的 哪里 条款。

    你需要手动计算出 var S和 瓦尔 在使用它们的语句之前使用并放入它们,就像ml一样 let 声明。

        4
  •  3
  •   flazz    11 年前

    哈斯克尔 绑定 名称的值 let where 表达 . 我很确定 哪里 表达式可以在计算或代码生成之前标准化为let表达式(不考虑计算顺序)。

    斯卡拉 编码 绑定与 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
    }
    

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