読者です 読者をやめる 読者になる 読者になる

kmizuの日記

プログラミングや形式言語に関係のあることを書いたり書かなかったり。

「ここがヘンだよScala言語」についてのまとめ

ここがヘンだよScala言語の記事に突っ込みどころが多かったので、色々書いたのですが、ちょっとコメントの書き方が挑発的過ぎたので、こちらのエントリでまとめなおします。

  • if式の返り値


val v1 = if (1 < 2) {"a"}
println(v1)

val v2 = if (1 < 2) {"a"} else {"b"}
println(v2)

v1はUnit、v2は"a"。else節が無い場合、「最後に評価された式を、返り値とする」というルールから外れる。

2.8.1、2.9.0等、現行の版では、v1、v2ともに"a"が返る。ルール通りの動きで問題はない。

取り消し線と訂正がありますが、それについては本題とは関係無いので省きます。重要なのは、Scalaには「最後に評価された式を、返り値とする」というルールは最初から存在しないのに対して、そのようなルールが存在すると勘違いされていたということです。

たとえば、while式の返り値型は本体の式の返り値型に関わらずUnitです。これは何故かと言うと、while式の本体が一回も実行されない可能性があるため、他に返すべき妥当な返り値が存在しないためです。

一方、ブロック式{e1, e2, ..., en}について、ブロックの返り値はenの評価値である、というルールは存在します。その辺りをid:kaminami さんは勘違いされていたのでしょう。

  • 除外インポート
import scala.collection.mutable.{_, Map => _, Set => _}

サンプルはMap、Set以外の全クラスをインポート。ここで、"_"にワイルドカードとしての役割と、消去の役割をもたせている。こんなのよく通したな。

これは半分正しくて、半分間違っています。importの"_"においてワイルドカードの役割を持たせている、というのは正しいのですが、除外importにおける"_"は、単に"_"にリネームしているだけであって、「消去の役割を持たせている」わけではないのです。では、何故それで除外importができるかというと、"_"はScalaにおいて識別子ではなく、"_"という名前でアクセスするのが不可能なためです。

  • typeと別名インポート

typeがあるなら別名インポートはいらないのでは?

type宣言はクラス/オブジェクトのメンバであって、使用するには一手間要りますし、冒頭に宣言することもできません。たとえばJavaのライブラリとScalaのライブラリの名前衝突があったときに、別名インポートではなくtype宣言を使うのは無駄にコストがかかるだけです。

  • forのネスト
for(i <- 0 to 10; j <- 0 to 10; k <- 0 to 10) {
  print(i + ":" + j + ":"+ k)
}

ループが一つなのか、三つなのか、ぱっと見で分からない。必要だったのか?

必要です。いや、forが単にJavaの拡張for文程度のものなら必要は無かったのですが、forは実際はもっと汎用的な構文なので、無いと困ったことになります。

また、これはオフトピックですがループをネストするより、上記の書き方の方が見やすいと個人的には感じます。一般的に、深いネストはコードを読みにくくする、というのが自分の考えです。

  • for内包表記中のifとif式
for(n <- List(1,2,3,4,5) if n % 2 == 1) yield print(n + " ") // OK

val = if n % 2 == 1 // コンパイルエラー
}

内包表記中のifと、if式は違う。

確かに、for式中のifとif式は違います。しかし、これは別に一貫性が欠如しているわけではなく、for式はそれ全体で一つの制御構文(サブ言語)であって、その中のifもfor式の一部なので、外のif式と同じである事を期待するのが誤りなのです。

  • forとforEach

クロージャとforEachメソッドをサポートするなら、for式は必要ないのでは?

yieldは好みじゃない。特例だから。

forEachforeach、というのはさておき、実際問題、for式は必要です。

まず、for式の一般形について説明しましよう。for式はyieldがある場合、無い場合の二形式あり、

前者は

for(x1 <- g1; x2 <-g2; x3 <- g3) yield

後者は

for(x1 <- g1; x2 <-g2; x3 <- g3) 式

のように書きます(ifは省略しています)。後者のケースは、foreachのネストで代用しても構わないのですが、前者の形式が無いと色々と面倒です。

前者の形式は

g1.flatMap{x1 => g2.flatMap{x2 => g3.map{x3 => 式}}}

のシンタックスシュガーですが、複雑なケースでこれをそのまま読むのは正直言ってつらいです。これ以上の詳細な説明はこのエントリの目的を超えるので、 for式 モナドなどでぐぐると良いのではないかと思います。

ちなみに、

yieldは好みじゃない。特例だから。

ですが、これが特例というのは意味がわかりませんでした。

  • タプルのインデックス

コレクションは0オリジン、タプルは1オリジン。

静的型付け関数型言語では、タプルの要素は1から始まるという伝統があります。Scalaもその伝統にならった、というのが歴史的経緯です。

ただし、タプルの要素名は別に添え字ではなく単なるフィールド名なので、コレクションの添え字(整数)と同じ土俵で比較するのはあまりフェアではないでしょう。

  • overrideの有無

通常のメソッドの上書きには、overrideキーワードが必要である。
抽象メソッドの上書きには、overrideキーワードが必要ない。

「override」は読んで字のごとく、上書きすることですから、上書きする元があることが前提です。通常のメソッドの上書きでは、元のメソッド定義があるので「override」が必要なのは自然ですが、抽象メソッドには実体がありませんから、そもそも上書きすべき元が存在しません。したがって、ルールとしては現在のScalaの仕様は一貫性があると言えます。また、抽象メソッドを定義し忘れた場合、コンパイラにはねられますから、実用上の問題も特に発生しません。

ただし、実用性の観点から言って、

  • 新規に定義したメソッド
  • overrride/実装したメソッド

の二者を簡単にわかる形で区別したいという事はあります。ただし、それは実用上の要求であって、仕様の一貫性とは異なる問題です。

わざわざクラスのオブジェクトとしての振る舞いをobjectに括りだしたのだから、インスタンス生成の責務はobjectに渡せば良かったのでは?

Scalaでは、Javaと異なり、全てのメソッド呼び出しにはレシーバオブジェクトが存在します。しかし、レシーバが必要無いメソッドというのも実際には存在します(数学関数など)。objectは、そのようなメソッドを定義するためにあります(と断言すると語弊があるのですが)。

さて、インスタンス生成の責務をobjectに渡すとなると、classのセマンティクスも現在とは異なるようになりますし、objectの仕様も複雑化します。さらに、Javaとの相互運用性でも問題が発生します。それでもあえてそのような選択をする必要があるかどうかと言えば、私は非常に懐疑的です。

class, caseクラス、object。場合によってnewを付けたり、外したり。

まず、基本的な原則を述べましょう。全てのclassにおいて、そのclassのインスタンス生成にはnewが必要です

objectは「インスタンス」を定義する構文であって、objectで定義された名前は、それ自体がインスタンスへの参照なので、newを付ける必要はありません(というかできません)。

caseが付けられたクラスAは、オブジェクトを生成するためのapplyメソッド(内部でnewを呼んでいる)が同名のobject Aに生成されるため、Aのオブジェクトを生成するためにはnewが必要ありません。つまり、caseを付けるとnewを付けないでいいように、言語仕様上特別扱いを受けるわけではないのです。

基本的には以上です。場合によってnewを付けたり外したり、という指向錯誤が必要だったのは、まず原則を理解されていなかったからでしょう。

基本コンストラクタと、thisを用いて定義するコンストラクタ。分ける必要があったのか?

さて、これは難しい問題です。基本コンストラクタ一つだけにしぼればシンプルになりますが、利便性はあきらかに低下しますし、Javaとの互換性の上でも問題が発生します。それでもあえて分ける必要があるのでしょうか。

また、基本コンストラクタ以外を使いたくなければ単に定義しなければいい(覚える必要はない)のではないでしょうか。

"ACBED".sortWith(_ > _) 

複数の引数を取る場合、異なるオブジェクトに同じ名前を付けることになり、順番に依存する。

プレースホルダ構文は、基本的には

"ACBED".sortWith(_ > _) 

"ACBED".sortWith((x, y) => x > y) 

に置き換える単純なシンタックスシュガーです(構文的には面倒な話があるのですが、とりあえず置いておきます)。ここで、複数の_はそれぞれ別の名前を表しているので、同じ名前を付ける、というのは適当な表現ではありません。

順番に依存する、というのはおそらく_が複数あった場合に、_の出現順が無名関数の仮引数の順番に影響を与える、という事かと思われますが、そのような問題が発生する場合にプレースホルダ構文を使う事自体が誤りです。プレースホルダ構文はあくまで短い無名関数を簡単に書くためのシンタックスシュガーに過ぎませんので、濫用は控えた方がいいでしょう。

  • Mutatorの定義
class Foo {
  var bar: Int = _
  
  def bar_=(value: Int) {
    this.doSomething
    this.bar = value
  }
}


特別なメソッドの書き方。

おっしゃる通り、これは特別なメソッドの書き方です。しかし、これが無ければ、


foo.bar = 100

で、bar_=が呼び出される、というような振る舞いが実現できません。

Bertrand Meyerの統一アクセス原則では、内部実装がフィールドであろうが、メソッドで計算された値であろうが、外(ユーザ)からは同じようにアクセスできるべき、とされています。これは、ユーザにとっては実装がフィールドであろうがメソッドであろうがどっちでもいいからです。これはアクセッサについての話ですが、代入についても話は同じです。というわけで、これは特別扱いではあっても必然性があるものです。

ちなみに、Rubyでも同じようにsetterメソッドのためには特別なメソッド名を使う必要があります。

  • 似ているキーワード

None、Nothing、Null、null、Nil。一つ一つ意味と用法を覚える。

これは意図がよくわかりませんでした。Twitterでの話では、Scalaでは特別扱いが多いので、多くの事を覚えなければいけないので難しい、というのが主眼だったかと思います。しかし、上記の5つの内4つについては、いずれも異なる名前で定義されたクラス/オブジェクトであり、特別扱い(キーワード)なのはnullだけです。

異なるクラスが異なる意味を持つのはどんな言語でも当然のことであり、それぞれ意味と用法を覚えなければいけないのは当たり前の話ではないでしょうか。

  • mutableなSet,Mapと、immutableなSet,Map

性質の異なるものを、わざわざ同名にする必要はあったのか?

性質が異なるので同名ではありません(異なるパッケージに定義されています=異なる完全修飾名を持ちます)。
単純名が同じなのが嫌だということになると、immutable.ImmutableMapとかimmutable.ImmutableSetとかいう名前が大量にできることになりますが、それは大変冗長なので、もしそうだったら嫌だなあと思います。

ひょっとすると、名前の衝突を心配されているのかもしれませんが、その場合は、

import scala.collection.mutable
val set = mutable.Set[Int](1, 2, 3)

のようにすればよいかと。あるいは、別名importを使っても良いでしょう。

  • コロンで右結合

通常の左結合の演算子が混じると面倒。

パズルプログラム以外で、そのような問題が発生したのを見たことがありません。

  • パラメータ境界

foo[A <: T]、bar[A >: T]、boo[A <% T]。このときコロンで右結合は関係あるんだろうか?

UML的なsuper-subの矢印の方向とも逆なので、Scala用の解釈として覚える。

コロンで右結合はこの際に関係ありません。コロンで右結合のルールはメソッド呼び出し時のルールです。また、<:>:はキーワードです。

A <: Bは、AはBのサブタイプである、を意味するもので、型システムの分野では一般的なものです。Scalaがあらたに作ったものではありません。

パーサーやらの都合は知らないが、foo[ A.isSubtypeOf[T] ]、bar[ A.isSupertypeOf[T] ]、とかのほうが読み下すには良いと思う。

「読み下すには」それの方がより悪いでしょう。何故なら、型パラメータの中で突然メソッド呼び出しのように見える何かがあらわれる、という奇妙な事になるからです。

型に関する記述は静的に解釈されますから、実行時に解釈されるメソッド呼び出しを想起する表記は避けるのが妥当です。

というわけで、静的型付け言語に慣れている人をユーザとして考えるなら、そのような構文の導入は百害あって一理無しといえます。

  • 暗黙の型変換

implicitにやるよりは、必要になった時点でtoString()のようなフレームワークで指定したconverterメソッドを呼び出す方が好み。言語機能に同じような役割の新要素を追加するよりマシ。

これは単純に意味がわかりませんでした。

  • notメソッド

unless文をサポートしないなら、Booleanにnotメソッドくらいは装備しておいて欲しい。

notメソッドはありませんが、!メソッドはあります:

val x = !(3 <= 4)
println(x)
scala>

ちなみに、unless相当はユーザ定義できますので、新たに文というか式として追加する必要は無いでしょう(標準ライブラリには欲しいですが)。

def unless[A](cond: Boolean)(body: => A) = {
  if(!cond) body
}

unless(3 < 2) {
  println("3 < 2 == false")
}

unless(2 < 3) {
  println("2 < 3 == false")
}
  • コンパニオンの参照
class A {
  def companion = A  // 名前を直に指定するのはカッコ悪い
  def foo = this.companion.defaultValue
}

object A {
  val defaultValue = 10
}

コンパニオンを取得するメソッドくらい装備しておいて欲しい。

ご要望としてはよくわかります。しかし、Twitter上では、Scalaは特別扱いが多くて覚える事が多いので難しい、という事をおっしゃっていたかと。上記の要望は特別扱いを増やすものですが、それでも構わないでしょうか。

  • unsignedがない

32ビットunsignedなら、4ByteをByteでとってLongに変換。低いレイヤやネットワーク関連で面倒な思いをしている人も多いことだろう。

unsignedが欲しいという気持ちはよくわかります。しかし、ここで、ScalaがJVMの上で動作する言語であり、Javaとの相互運用性を可能な限り高める(これは、Scalaの設計理念と反する事もあるのでケースばいケースですが)事を設計目的の一つとしている事を思い出していただければと思います。

具体的には、Javaのプリミティブ型は基本的にScalaのクラスに1対1マッピングされます。そのため、Scalaからプリミティブ型を引数に取るJavaのメソッドを呼び出す場合、boxingするような余計な処理は必要ありません。これは、大きな利点の一つです。

さて、仮にunsigned(Byte/Short/Int)を導入したとしましょう。Scala側ではUIntクラスとか(ダミーとして)作ればいいとして、これをどうやってJavaにマッピングするべきでしょうか。上で書かれている通り、longに変換すべきでしょうか。すると、新しくscala処理系側で、UInt >> longというマッピングを追加する必要があります。

ここでScala >> Javaの一方通行なら問題無いのですが、JavaからScalaへLongをUIntの値として渡そうとした場合に妙なことになります。Scala側のメソッドシグネチャとしてはUIntは単なるLongになっているでしょうから、JavaからLongの値をそのまま渡すことはできます。しかし、そうすると、

UInt(Scala) ==> long(Java)
long(Java)    ==> Long(Scala)

というマッピングが追加されることになってしまいます。これまでのScalaでは双方向にマッピングできていたのに、この拡張を追加したせいで、一貫性を損なってしまいます。一貫性などどうでもいいから利便性を優先して欲しい、という話は理解できます。しかし、一貫性を捨てるということは覚えることを増やすことにもつながります。果たしてそれで良いのでしょうか。

と、ここまでが個々の内容に対するツッコミです。以下は、id:kaminamiさんのコメントに関する全般的な疑問になります。

Twitter上では、id:kaminamiさんは、Scalaは特別扱いが多くて覚える事が多いので、普通の人には「難しすぎる」という趣旨の発言をされていたと記憶しています(後で該当するtweetを貼ります)。また、id:kaminamiさんご自身にとっても、覚える事が多くて難しいという発言をされていました。

しかし、今回の件では、特別扱いを増やすような要望をいくつか出されています。そのため、id:kaminamiさんの意図をつかみかねています。

私が考えられる可能性を列挙しますので、お答えいただければ幸いです。
-

  • 既にScalaは覚えることが多くて複雑なので、覚える事(特別扱い)をもっと増やしても構わない
  • 提案されている事が、言語仕様をより複雑にし、覚える事(特別扱い)を増やすという事を理解されていない
  • Scalaは覚える必要のある事が多くて難しいが、利便性を高めるためなら、もっと難しくなってもいい