予防線
自分以外誰の役にも立つことを意図していない記事なので僕以外はブラウザバック推奨。
突っ込んでもらっても構いませんがリアクションは期待しないでください。
じゃあなんでネット上に公開しているのかって?こうするのがいつでもどこからでもアクセスできる一番便利な方法だからです。
文章も僕のエゴで満ちているので客観的な評価や表現は期待しないこと。毒も吐くよ。
あと極めて主観的に印象や感想を書きます。客観性とか期待すんな!
標準ライブラリのできが結構ひどい
コード的にどうではなく、入っているクラス・メソッドの選定基準が非常に疑問。
例えば多くの場合良く使うusingメソッドがない。
Optionなどの実装にはすでにずっと疑問が提示されているにもかかわらず互換性を理由にそのままとなっている。
これはscalazやcatなどでOptionなどの再実装がされる背景となっている。
コンパイラ自身のコードが結構ひどい
Scalaはコンパイラ自身のコードをscalaで書いているのだけど(こういったコンパイラ・インタープリタのコードをそれが処理する言語自体で書くことを自己記述と言うらしい)、コードが結構ひどい。
不要にvarを使ったり、フォーマットに一貫性が無かったり、インターフェイス定義がひどくて拡張性に難があったり、そのせいで同じようなコードを複数の場所で書いていたりする。
scalaクローン的なプロジェクトがすでに複数走っているので(dottyとかオーケストラ?)、卵が先か鶏が先かわらかないけど言語の未来が怪しく思える。
Scalaの古いバージョンベースでコンパイルされた(maven repositoryに公開された)ライブラリが新しいバージョンで使えない
Scalaじゃなくてjvm共通なのかもしれませんが、数年前に作られたライブラリがそのままでは最新環境で使えない。
自分でリコンパイルして公開するのも手間ですし、仮にそうしたとしても本家のライブラリが更新されるわけではないので
第2,第3の自分は本家githubページからまともそうなforkを探す作業をしないといけない。
これに対して僕が思いつく解決策は2つ。
1つはtravisのような全自動テスト・コンパイルシステムがより普及・導入しやすくなればライブラリ開発者もコンパイラバージョンアップにキャッチアップしやすくなる。もしくはこちらで勝手にメジャーバージョンアップに合わせてリコンパイルしてもいいかもしれない。
2つ目はスクリプト言語のようにコンパイラバージョンアップとライブラリを分離する。
ところでいまさらだけどライブラリはjarなのになぜscalaコンパイラバージョンに依存しているのだろう。
基本ライブラリの破壊的な変更と、同じライブラリのバージョン違いを同時にincludeできない、いわゆる昔のdll地獄みたいなことが原因なのだろうか?
同じライブラリの別バージョンを同時に利用できない
これはJavaからの制限だし、Scalaに限らない話ではある。
各言語この問題には苦労していてかなり頑張って強引に解決している言語があったりjava系のように同時には使えないということにしたりgolangのように一度公開されたインターフェイスは挙動を変えないルールにしてそもそも外部インターフェイスのバージョンという概念をなくしたりしている。
個人的には単純にネームスペースの中にバージョンを含めるだけで解決すると思うのだけどどうなんだろう。
case classは便利なようなイマイチフィットしない。
好きじゃない。まずインスタンス作るのにnewを書かないのが一番イヤだ。
それでもDSL的に書くときに便利なのはわかるのだけど、それ内部DSL定義するという非常にニッチな条件でしか便利でないのでそんなことのために言語機能としてこんな特化したクラス定義方法を追加しないでほしい。
twitterでも言及したけど、case classは自動でメソッドを定義しているだけのはず( http://www.ne.jp/asahi/hishidama/home/tech/scala/class.html#h_case_class )。
今ならscala macroでできるんじゃないの?と思うしできるなら定義して言語機能としての提供はやめればいいと思う。
Javaの負の遺産多すぎ
hashCodeとかequalsとかJavaの微妙な部分が引きづられている。
結局Scala書きながらもJavaのことを忘れられない。
C#からdllの関数呼び出すときのように、RubyからCコード呼び出すときのように、その言語外のことを考えるのはその言語以外で書かれたコードを呼び出す時だけにしてほしい。ScalaはScalaしか書いていない時でもJavaの知識が必要になる、それも常時(equalsのように)。
toStringもJavaの負の遺産。scalazで中途半端に修正しようとしているが、型としてのStringへの変換とそのオブジェクトの中身・値を表現するためのメソッドは分けるべき。
例外の型が既存のJavaなのも負の遺産。とはいえ例外機構はベストな解が未だ定まっていないらしくまだ最近の言語でも統一された方法が取られていないので、こうするべきとは提案しがたい。僕はJava本来のcatch強制例外は好きだしあの方向性は間違っていないと思っている。
型システムが微妙
全体的に暗黙的に変換される処理が多すぎる。
例えば
type SerialNumber = String
def fromSN(sn: SerialNumber) = { ... }
val str: String = "単なる文字列ですよ"
fromSN(str)
これの4行目が通ってしまうのがイヤ。
関数の利用者に対して引数がSerialNumberであることを明示的に知らせたいし、そこへ無自覚にString放り込むとコンパイルエラーを出してほしいのに勝手に変換してしまう。
これとは微妙に違うけど似た話としてimplicit変換がある。
implicit変換は確かに便利だ。だけどデフォルトで勝手に変換されるケースが多すぎる(たしか何もimportしなくてもデフォルトでimport扱いになる領域で大量にimplicit変換が定義されている。)
型変換はもっと自覚的にしたいし、デフォルトの型変換を無効化する方法もほしい。できるかもしれないけどもっと簡単に、選択的にできるようにしたい。
一部の式syntaxが不満
for文、特にyield絡んだ時のfor文は最悪。forの結果を使うかどうかなんてコンパイラがコンパイル時に判断すればいい。
全部yield付いているものとして結果を使わないならループ毎の最終評価を破棄する感じ。
あと基本的な見た目としてforやifの条件式にカッコ()を使うのは今ではダサい。
終端記号;をなくすのになぜそこのカッコをなくさないのか。
コードフォーマッタが貧弱、言語標準フォーマットの整備が未熟、自動フォーマットが未活用
go fmtのように何も考えずに勝手にフォーマットしてほしい。
まあただこれがしづらいのはScalaのSyntaxが一部無駄に複雑であることも原因であると思う。
Actor周り面倒くさい
特に素直に終了してくれないことが多い。
thisはダサい、selfがいい
thisは主体となるものがあってそれが別のobject - thisを指しているイメージがある。
自分自身を参照する場合はselfのほうが自然。
syntaxに一貫性がない
caseがつくと便利なメソッド群が自動で定義される。それはいい。
ただ、caseがつくとつかないでコンストラクタ引数の可視範囲が変わるのはいただけない。
もちろん実際にはgetter/setterを定義しているのだけど、使う方からしたら実質的にcase付く点かないで挙動がかわってしまっている。
こんなのだったら最初からコンストラクタ引数はpublic valにするべきだった。
これに限らずScalaのSyntaxには利用者からは一貫していないように見えるSyntaxが多い。
Syntaxを一貫させようという意識が薄いように感じる。
この時は駄目!みたいな特殊なNGケース多すぎ
98%の使用者は気づいていないしはまらないけどたまたま不幸な使い方をした2%が原因を調べようとしても全然わからない、そんなNGケースが結構ある。
僕が覚えているのは下の2つ。
- http://d.hatena.ne.jp/xuwei/20140607/1402128646 のEvalの対象としてrootネームスペースのクラスは指定できない。これはコード追いかけていないので原因は不明だが数時間はまった。
- package名として
package
という単語は使えない。基本的にscalaの変数名などは``さえ使えばtypeだろうがifだろうが使えるが、package名としてpackage
は使えない。なぜかというと内部的に`package`パッケージへ対して特殊な処理をしているから。これはScalaコードを追いかけて調べた - superで基底クラスのメソッドにはアクセスできるのにvalやvarはダメ。おそらくJVMのバイトコードへの翻訳の仕方に問題がある?(これもJava、というかJVMの負の遺産っぽい)
すぐ上で書いたように一貫性を重視していないのでこういう残り2%でのバグ・不具合が非常に多い。
コンパイルが遅い
これは特にいろんな場所で言われていることでもある。
原因としては構文の複雑性とJVMベースであることも関係していると思う。
オブジェクトとコンパニオンオブジェクトがもにょい
もにょい
implicitを探すスコープ範囲がわかりづらい、意味不明。
playやscalazのReader/Writerをよくコンパニオンオブジェクトのスコープで定義すると思うのだけど、それはそのスコープが勝手に
implicitを探すときに調べてくれるから。でもわかりづらすぎる。1年以上書き続けたあとに知ったわ。
基底クラスのコンパニオンオブジェクトの中は調べてくれなかったりするのが上で書いたもにょい点の一つ。
可変長引数周りの一貫性が足りない
上で書いた一貫性の欠如絡みだけど、Scalaの可変長引数は使えば使うほど欠点が目立つようなところがある。
可変長引数の型は厳密にはSeq[T]ではないらしく、varとして定義してSeq[T]型を代入しようとしても失敗する。
他にも
def func(is: Int*)
def func(is: Seq[Int])
この2つを同時に定義できない。使い方は全く別なのに内部実装都合でこれらが両立できないのはいかがなものか。
(可変長の方をバイトコードへ翻訳するときにSeq[Int]とは別の型にすれば普通に通るはずなのだけど、Javaからの呼び出しなどを考慮した結果?こうなっている気がする)
名前渡し(call by name)の周りに一貫性が足りない
名前渡しと引数なしラムダ関数は本来同じもののはずがなぜか名前渡しという別名になっている。
同じでいいのでは?
:(コロン)がダサい
def f(v: Int) = ...
コロンが2つの意味でダサい。
- 不要であること(本当にそうであるかは未検証)。パーサが頑張れば型定義の前に:は不要に見える。
- 記号がダサい。{}などもそうだけど、プログラミング中のアルファベット以外の記号は可読性を損なう。|>や+|+みたいなよくscalazで定義されている述語としての記号は別にいいけど、スコープ定義や引数定義程度で()やら:やら{}持ち出すのはダサい。
あと当然ながら同様の理由でスコープを定める{}もダサい。Python式にindentにしてほしい。
自動リファクタリング、Lintなど機械的支援が貧弱
そこらにリソースを割く人がいないのもひとつの理由だろうけど、最大の原因はやはり機械的に解釈しづらい複雑な構文にあると思う。
例えばgoは構文的複雑性・柔軟性を排除する代わりに多面的な範囲で自動化の利益を得ている(go fmt, 言語組み込みのLint機能など)
やっぱり今時の言語はフォーマッタ、Lint内蔵が普通では?
アンダースコアに複数の意味をもたせているのがイマイチ
プレースホルダの他にimport時のワイルドカード代わりとしても使っている。
一見理にかなっているようだが、_の背後にある思想・考え方にブレが生じており結果的に_をよくわからないものにしている。
Syntax上の余計な遊び、いらない
defはvalでも上書きできるとか、overrideはつけなくてもいいケースがあるとか、
そういう非決定的かつどうでもいいsyntax上の遊びはいらない。
overrideは毎回つけるでいいし、defはvalで上書きできるというのはread onlyのvarがvalで上書きできる的なニュアンスを感じる。
それ同軸で扱うべき話を2軸使ってない?
Seq, List, Iterable、どれを使うか公式ライブラリでも混乱がある
まずメソッドごとに帰ってくる型が違う。使い分ける必要性があるかというとないに見える。
その結果、異なる型が帰ってくるごとに型の変換が必要になる。良くない。
変数・関数のデフォルトの修飾子がpublicなのに関数内で定義されたそれらが実質的にprivateなのに違和感
関数内に持ってくるとprivate[this]修飾がいらなくなるというのは関数もスコープの一つだと考えるなら一貫性がない。
関数を引数として取れる箇所でその関数の引数にimplicitつけられるのが違和感
それ便利だけど、その修飾は関数内でどう扱うかを決める修飾なので、混乱する。
implicitが内向き外向き両方の意味になってしまっている。
関数の引数にimplicit引数があると、関数を呼ぶ側としては明示的に渡す必要がなくなる。
また、同時に関数の内部でその引数を利用するときも明示的に渡さずとも動作する。
ただこれ、外側ではimplicitで渡すけど関数内部ではすべて明示的に書きたいケースがある。そういったときにそう書くのは結構面倒(できなくはない。別スコープ切って別の変数に入れてその別スコープ内で参照すればよい)。
どうすればよいかを考えると、基本的に修飾子は基本的に外側からどう見えるか、どうアクセスできるようにするかを定義するものなので
内部では自動的にimplicitにならないようにして、内部でもimplicitとして扱いたいならimplicit valへその都度代入する形にすればどうかと思う。
無名implicit変数という機能がほしい
implicit変数(引数)は明示的に変数を指定する必要がない。
そのため、変数名を一度も参照しないケースが多い。
だから参照されない変数として無名変数(_が適当?)として名前を与えず使えるようにしてほしい。
コンストラクタの引数は自動でprivate[this] valになることがわかりづらい
明示的につけないといけない仕様にしていいんでは。
あとclass内で宣言したvalなどはデフォルトでpublicになるのにコンストラクタ引数はprivate[this]って一貫してなくない?
private[this]がだるい
privateより圧倒的にprivate[this]のほうが使用頻度が高いのだから、privateはなくすないしprivate[this]の方をより書きやすくするべき。
ラムダ関数を定義するときに引数が複数だとパターンマッチが必要でだるい
_ を複数回使えないようなケースがある(foldleftの第2引数のケースなど)。
パッケージオブジェクト内のprivate classとprivate[パッケージ名] classが等価じゃない
文字通り。意味合いからすると等価では?
コンストラクタが一般的な関数として扱えない
例えば引数にIntを取ってClassAのオブジェクトを返す関数を引数として取る関数があるとする。
その場合、ClassAのコンストラクタにIntを取るものがあったとしてもコンストラクタは直接渡せない。
new ClassA(_) みたいな書き方をすれば行けるが、これは関数を渡しているのではなくラムダ関数を定義して渡している。
迂遠。
Main関数のシグネチャが微妙
一般的なScalaのエントリポイント(main関数)の書き方は以下。
def main(args: Array[String]): Unit = {
println("hello")
}
2つ問題がある。一つは引数の型が可変型であることで、これによりargs変数内の各要素を上書きすることができる。
今のScalaの型システム上であればSeq
型が好ましそう。
2つめは返り値の型がUnit型であること。ここもexit statusをきちんと意識して数値を返したい。
総じて、Javaとの互換性を優先する余り本来あるべきシグネチャになっていない。
好きな点
好きじゃない点だけ上げても微妙なので好きな点もあげる。
- オブジェクト指向 + 関数型 の方向性は圧倒的に正しかった。多くの後続言語でも同様になっていることからも自明
- Javaの資産(ライブラリだけじゃなく環境的な意味でも)が使える。僕Javaも好きなのでそこから流用できるのは良い。
- 静的片付けシステムBANZAI! 世界は静的片付けに収束すると確信している。
他の言語
KotlinとかswiftとかKlassicとかがこれらの不満を解決してくれないかなあ。
Rubyについて
これらの不満、特にSyntax周りはRubyっぽくしろと書いてあるように見える。
実際そのとおりでそう思っている理由は今まで書いてきた言語の中で一番Syntaxが心地よかったのはRubyだったから。
ただ、Rubyは動的型付け言語であるという1点において僕とは相容れない。