割り当てられた値ではなく、ユニットに評価されるScala割り当ての動機は何ですか?


84

割り当てられた値ではなく、ユニットに評価されるScala割り当ての動機は何ですか?

I / Oプログラミングの一般的なパターンは、次のようなことです。

while ((bytesRead = in.read(buffer)) != -1) { ...

しかし、これはScalaでは不可能です...

bytesRead = in.read(buffer)

.. bytesReadの新しい値ではなく、Unitを返します。

関数型言語から除外するのは興味深いことのようです。なぜそうなったのかしら?


デビッド・ポラックは、マーティン・オーダスキー自身が彼の答えに残したコメントによってかなり支持された、いくつかの直接の情報を投稿しました。ポラックの答えを安全に受け入れることができると思います。
ダニエルC.ソブラル2010年

回答:


88

私は、割り当てが単位ではなく割り当てられた値を返すようにすることを提唱しました。マーティンと私はそれを行ったり来たりしましたが、彼の主張は、95%の時間で値をスタックに置くことはバイトコードの無駄であり、パフォーマンスに悪影響を与えるというものでした。


7
Scalaコンパイラが割り当ての値が実際に使用されているかどうかを確認できず、それに応じて効率的なバイトコードを生成できなかった理由はありますか?
マットR

43
セッターがいる場合はそれほど簡単ではありません。すべてのセッターは結果を返す必要があり、これを書くのは面倒です。次に、コンパイラーはそれを最適化する必要がありますが、これは呼び出し間で行うのは困難です。
マーティンオーダスキー2010年

1
あなたの議論は理にかなっていますが、javaとC#はそれに反対しています。生成されたバイトコードで何か変なことをしていると思いますが、Scalaでの割り当てがクラスファイルにコンパイルされ、Javaに逆コンパイルされるとどのようになりますか?
フオングエン

3
@PhươngNguyễn違いは統一アクセス原則です。C#/ Javaセッターでは(通常)を返しvoidます。Scalaでは、割り当てが行われた場合にfoo_=(v: Foo)戻るFoo必要があります。
アレクセイロマノフ2011

5
@Martin Odersky:次はどうですか:セッターは残りvoidUnit)、割り当てx = valuex.set(value);x.get(value);と同等に変換されます。コンパイラーは、最適化フェーズでget、値が使用されていない場合の-callsを排除します。これは、新しいメジャー(後方互換性がないため)Scalaリリースの歓迎すべき変更であり、ユーザーの苛立ちが少なくなる可能性があります。どう思いますか?
Eugen Labun 2012

20

私は実際の理由についての内部情報に精通していませんが、私の疑いは非常に単純です。Scalaは、プログラマーが当然のことながら内包表記を好むように、副作用のあるループを使いにくいものにします。

それは多くの方法でこれを行います。たとえばfor、変数を宣言して変更するループはありません。while条件のテストと同時にループの状態を(簡単に)変更することはできません。つまり、多くの場合、変更の直前と終了時に変更を繰り返す必要があります。whileブロック内で宣言された変数は、whileテスト条件からは見えないため、do { ... } while (...)あまり役に立ちません。等々。

回避策:

while ({bytesRead = in.read(buffer); bytesRead != -1}) { ... 

それが価値があるものは何でも。

別の説明として、おそらくマーティン・オーダスキーは、そのような使用法に由来するいくつかの非常に醜いバグに直面しなければならず、彼の言語からそれを非合法化することに決めました。

編集

デビッド・ポラックはいくつかの実際の事実で答えましたが、マーティン・オーダスキー自身が彼の答えにコメントし、ポラックが提起したパフォーマンス関連の問題の議論に信憑性を与えているという事実によって明確に裏付けられています。


3
したがって、おそらくforループバージョンは次のようになります。for (bytesRead <- in.read(buffer) if (bytesRead) != -1これはforeachwithFilter利用可能なものがないために機能しないことを除けば、すばらしいことです。
oxbow_lakes 2010年

12

これは、より「正式に正しい」型システムを持つScalaの一部として発生しました。正式に言えば、割り当ては純粋に副作用のあるステートメントであるため、を返す必要がありますUnit。これにはいくつかの良い結果があります。例えば:

class MyBean {
  private var internalState: String = _

  def state = internalState

  def state_=(state: String) = internalState = state
}

state_=メソッドの戻りUnit(セッターのために予想されるように)正確に割り当て返すためUnit

ストリームのコピーなどのCスタイルのパターンの場合、この特定の設計上の決定は少し面倒になる可能性があることに同意します。ただし、実際には一般的に比較的問題はなく、型システムの全体的な一貫性に大きく貢献します。


ありがとう、ダニエル。割り当てとセッターの両方が値を返すという一貫性があれば、私はそれを好むと思います!(できない理由はありません。)「純粋に副作用のある発言」のような概念のニュアンスをまだ理解していないのではないかと思います。
Graham Lea 2010年

2
@Graham:しかし、一貫性を守り、すべてのセッターがどんなに複雑であっても、設定した値を返すようにする必要があります。これは複雑な場合もあれば、間違っている場合もあると思います。(エラーが発生した場合に何を返しますか?null?–ではなく。
debilski 2010年

7

おそらくこれは、コマンドとクエリの分離によるものです原則によるものですか?

CQSは、副作用のあるオブジェクトメソッドとないオブジェクトメソッド(つまり、オブジェクトを変更するメソッド)を明確に区別するため、OOと関数型プログラミングスタイルの共通部分で人気があります。変数の割り当てにCQSを適用すると、通常よりもさらに進んでいますが、同じ考え方が当てはまります。

短いCQSが有用である理由の説明図:を有する仮想的ハイブリッドF /オブジェクト指向言語を検討Listメソッドを有するクラスはSortAppendFirst、およびLength。命令型OOスタイルでは、次のような関数を記述したい場合があります。

func foo(x):
    var list = new List(4, -2, 3, 1)
    list.Append(x)
    list.Sort()
    # list now holds a sorted, five-element list
    var smallest = list.First()
    return smallest + list.Length()

一方、より機能的なスタイルでは、次のように書く可能性が高くなります。

func bar(x):
    var list = new List(4, -2, 3, 1)
    var smallest = list.Append(x).Sort().First()
    # list still holds an unsorted, four-element list
    return smallest + list.Length()

これらは試みているようです同じことをしてが、明らかに2つのうちの一方が正しくなく、メソッドの動作について詳しく知らなければ、どちらかを判断することはできません。

ただし、CQSを使用するAppendSort、リストを変更する場合はユニットタイプを返す必要があるため、必要のないときに2番目のフォームを使用してバグを作成することはできません。したがって、副作用の存在もメソッドシグネチャに暗黙的に含まれます。


4

これは、プログラム/言語に副作用がないようにするためだと思います。

あなたが説明するのは、一般的に悪いことと考えられている副作用の意図的な使用です。


ふふ。副作用のないScala?:)また、val a = b = 1(のval前にある「魔法の」を想像してくださいb)対のようなケースを想像してくださいval a = 1; val b = 1;

これは、少なくともここで説明する意味では、副作用とは何の関係もありません。副作用(コンピューターサイエンス)
Feuermurmel 2018

4

割り当てをブール式として使用するのは最適なスタイルではありません。2つのことを同時に実行すると、エラーが発生することがよくあります。また、Scalasの制限により、「==」の代わりに「=」を誤って使用することは回避されます。


2
これがゴミの理由だと思います!OPが投稿したように、コードはまだコンパイルされて実行されます。それは、あなたが合理的に期待することを実行しないだけです。それはもう1つの落とし穴であり、1つ少ないものではありません!
oxbow_lakes 2010年

1
if(a = b)のようなものを書くと、コンパイルされません。したがって、少なくともこのエラーは回避できます。
デーモン2010年

1
OPは「==」の代わりに「=」を使用せず、両方を使用しました。彼は、割り当てが値を返すことを期待しています。この値は、たとえば、別の値(例では-1)と比較するために使用できます
IttayD 2010年

@deamon:aとbがブール値の場合、(少なくともJavaでは)コンパイルされます。if(a = true)を使用して、初心者がこのトラップに陥るのを見てきました。(a)の場合はより単純なものを好むもう1つの理由(より重要な名前を使用する場合はより明確になります!)。
PhiLho 2011年

2

ちなみに、Javaでも、最初のwhile-trickはばかげています。なぜこのような何かをしないのですか?

for(int bytesRead = in.read(buffer); bytesRead != -1; bytesRead = in.read(buffer)) {
   //do something 
}

確かに、割り当ては2回表示されますが、少なくともbytesReadはそれが属するスコープ内にあり、面白い割り当てのトリックで遊んでいません...


1
トリックはかなり一般的なものですが、通常、バッファーを読み取るすべてのアプリに表示されます。そしてそれは常にOPのバージョンのように見えます。
TWiStErRob 2015年

0

間接参照の参照型がある限り、これを回避することができます。ナイーブな実装では、任意のタイプに以下を使用できます。

case class Ref[T](var value: T) {
  def := (newval: => T)(pred: T => Boolean): Boolean = {
    this.value = newval
    pred(this.value)
  }
}

次に、後でref.value参照にアクセスするために使用する必要がある制約の下で、while述語を次のように記述できます。

val bytesRead = Ref(0) // maybe there is a way to get rid of this line

while ((bytesRead := in.read(buffer)) (_ != -1)) { // ...
  println(bytesRead.value)
}

またbytesRead、入力しなくても、より暗黙的な方法でチェックを行うことができます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.