16

Scala で where 句に似たものを使用することは可能ですか? 私が思いつかなかったトリックがあるのではないでしょうか?

編集:

ご回答いただきありがとうございます。要約すると、ローカルの vars、vals、および defs を使用して、ほぼ同じことを達成できます。遅延評価の場合、遅延 val (暗黙的なキャッシュを使用) または関数定義を使用できます。機能的な純粋さを確保することは、プログラマーに任されています。

ここで 1 つだけ疑問が残ります: 値または関数の定義を、それらが使用されている式の後に置く方法はありますか? 時にはそれがより明確に見えることもあります。これは、クラスまたはオブジェクトのフィールド/メソッドで可能ですが、メソッド内では機能しないようです。

これまでの回答で言及されていないもう1つのこと。where 句は、その中で定義された式の範囲も制限します。Scalaでもそれを達成する方法を見つけていません。

4

4 に答える 4

25

Hakell では、where 句は関数に対するローカル定義を保持します。Scala には明示的な where 句はありませんが、 local varvalandを使用することで同じ機能を実現できます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 の例で構文エラーが発生した場合は修正してください :)。

より複雑な例は、同様の方法で実現できます (たとえば、両方の言語がサポートしているパターン マッチングを使用します)。ローカル関数は、他の関数とまったく同じ構文を持ちますが、そのスコープはそれらが含まれるブロックです。

編集:また、そのような例と主題に関するいくつかの詳細については、ダニエルの答えを参照してください。

EDIT 2lazy var : s とsに関する議論を追加しましたval

怠惰な「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上記の例では、everyを aに置き換えて、def同様の結果を得ることができます。違いは、遅延変数はキャッシュされます (つまり、一度だけ評価されます) が、adefは呼び出されるたびに評価されます。

編集 3:スコーピングに関する議論を追加しました。質問を参照してください。

ローカル定義の範囲

ローカル定義には、予想どおり、それらが宣言されているブロックのスコープがあります (ほとんどの場合、 for ループで中間ストリームの変数バインディングを使用する場合など、まれな状況でブロックをエスケープできます) 。したがって local varvalおよび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()動作します。barinの定義は、囲んでいるスコープの定義をinnerFun()隠します。barまた、関数funはそれを囲むブロックに対してローカルであるため、それ以外の方法では使用できません。メソッドdoesNotCompile()... はコンパイルされません。メソッド printprintln()からの両方の呼び出しに注意するのは興味深いことです。したがって、はい、メソッド内で使用された後に定義を配置できます! ただし、それは悪い習慣だと思うので、お勧めしません(少なくとも私の意見では)。クラス ファイルでは、メソッド定義を好きなように配置できますが、使用する前にすべての s を関数内に保持します。とs とs ... うーん... 使った後に置くのは面倒です。smthDifferent()inner scopedefvalvar

また、各ブロックは定義ではなく式で終了する必要があるため、ブロックの最後にすべての定義を含めることはできません。おそらく、すべての定義をブロックの先頭に配置し、そのブロックの最後に結果を生成するすべてのロジックを記述します。次の方法ではなく、その方が自然に思えます。

{
// some logic

// some defs

// some other logic, returning the result
}    

前に言ったように、ブロックを だけで終わらせることはできません// some defs。ここが、Scala が Haskell と少し異なるところです :)。

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

あなたが与える例は、valsが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
}

この例は、キャッシングが機能していることもよく示しています ( to を変更して、何が起こるか見てみましょlazy valdef:)

私はまだ、副作用のある世界では、定義を使用する前に定義することに固執する方がよいと考えています. このようにすると、ソース コードが読みやすくなります。

-- Flaviu Cipcigan

于 2009-08-16T13:59:17.690 に答える
5

似ています、はい。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

words簡単に定義できますが、Scala にはどちらもありません。

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

現在、Haskell の定義はさまざまな理由でよりコンパクトになっています。

  • より強力な型推論があるため、calcそれ自体の型以外に宣言する必要はありません。クラス モデルをオブジェクト指向にするという意識的な設計上の決定により、Scala はそれを行うことができません。

  • これには暗黙のパターン マッチング定義がありますが、Scala では、関数を宣言してからパターン マッチを宣言する必要があります。

  • そのカリー化の処理は、簡潔さに関する限り、Scala より明らかに優れています。これは、クラス モデルと演算子表記に関するさまざまな決定の結果であり、カリー化の処理はそれほど重要ではないと考えられていました。

  • Haskell はリストに対して特別な処理を行い、より簡潔な構文を使用できるようにしています。Scala では、リストは他のクラスと同じように扱われますが、その代わりに、あらゆるクラスが Scala で可能なリストと同じくらいコンパクトになるように努力が払われています。

したがって、Scala がそのようなことを行う理由はさまざまですが、暗黙的なパターン マッチングの定義が欲しいと思います。:-)

于 2009-08-16T14:36:46.133 に答える
4

varandを使用してローカル変数を提供することもできますが、これは、怠惰と純粋​​という 2 つのかなり重要な側面でvalHaskell の節とは異なります。where

Haskell のwhere句は、遅延性と純粋性により、コンパイラが実際に使用される where 句内の変数のみをインスタンス化できるため、便利です。

これは、大きな長いローカル定義を記述し、その下に句をドロップできることを意味します。where効果の順序を考慮する必要はなく (純粋であるため)、個々のコード ブランチですべての定義が必要かどうかを考慮する必要もありません。 where句、怠惰により、where句の未使用の用語がサンクとして存在できるため、コンパイラは使用されていないときに結果のコードから除外することを選択できます。

where残念ながら、Scala にはこれらのプロパティのいずれも含まれていないため、Haskell の句と完全に同等のものを提供することはできません。

varML ステートメントと同様に、使用するとを手動で取り出して、valそれらを使用するステートメントの前に配置する必要がありますlet

于 2009-08-16T17:02:18.303 に答える
3

Haskellは、およびを使用して値を名前にバインドします。評価またはコード生成の前に、(評価順序に関係なく) 任意の式を let 式に標準化できると確信しています。letwhere where

Scalaは、スコープ内のステートメントでバインドをエンコードします。コンパイラは、その名前に割り当てられた値が変更されないことを保証します。これらは最初から最後まで順番に実行されるため、let のように見えます。これは、コードに読み取ってもらいたいこととは反対です。つまり、主なアイデアが最初に示され、それをサポートする詳細がその後に示されます。これが私たちの美的負担の原因です。val

標準化の精神で、Scala の where をエンコードする方法の 1 つは、EXPN1 が任意の有効な式であり、EXPN2 が展開するオブジェクト宣言内で有効な任意のものであるようなwhere -> letマクロを使用できる (私は試していませんが、仮説を立てるだけです) 。EXPN1 where { 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 に答える