Scalaを使ってからKotlinに触ってます。使っているのは主にWebサービス開発ですが、Kotlin、あんまり好きじゃないです。Android開発でなら何かKotlinでしか得られないサポートがあると思うのですが、そういうのが無い限りKotlinを使うメリットってあまり無いんじゃないかと私は思ってます。よくKotlinは矯正されたScalaだから分かりやすいだとか、モナドが嫌いだからKotlinのほうが良いとか言う人がいますが、そういうのを超越してKotlinってダメだと思います。
ただ、私は
- Kotlinの言語仕様とかあんまり詳しいところまでちゃんと見てない
- Kotlinの歴史的背景とか詳しく知らない
- Android開発もやったことがない
- Webサービス開発で使っての個人的印象でダメだと言ってる
というのはエクスキューズで入れときます。あくまでも私が思ったことを列挙してるだけで、「だからKotlinはダメ」と言いたいわけじゃないです。ダメって言ってるけど。あくまでもダメと思ってるのは私個人の意見として、ということです。
何か私の知らない領域でKotlinにはこんなメリットがあるんだよ!だから使うんだよ!とか、何かの事情があってKotlinを使わなければならないというのならば別にそれは良いと思います。私はKotlinはクズだから使うなと啓蒙したいわけではなくて、心の底からKotlinじゃなくてScalaが好きだ!!と言いたいだけです。だからジャスト俺の感想記事。
では気に入らない所を列挙していきます。
whenが使いにくい
whenのそれぞれのケースで新しい名前をつけることが出来ません。上手く説明できないので例示します。
Scalaなら、
1 2 3 4 5 | val statusString = getData(fileId) match { case None = > "no such data" case Some(x) if x.isEmptyData = > "empty data" case _ = > "data exists" } |
というふうに書けますが、Kotlinだと
1 2 3 4 5 6 | val data = getData(fileId) val statusString = when(data) { null -> "no such data" data.isEmpty() -> "empty data" else -> "data exists" } |
になるのが嫌。何が嫌かというとvalが一個増えるのが嫌。Scalaのxのようにそれぞれのケースでローカル変数を定義できないので、そのメソッドを呼んで分岐を判定させたいときは必ずwhenの外で変数を一つ定義してあげなくてはならない。
個人的に変数が増えるのって嫌だ。変数は極力少なくしたい。変数が増えるほど人間の頭の中で考えなければならない要素が増えるから嫌。valが使えるので「こいつは宣言した以降変わってないんだな」という保証はできるんだけど、それでもそこに「何が入っていたか」は宣言したところを見ないとわからない。上記くらいのコードだったらすぐわかるけど、もう少し複雑になってくると可読性は落ちてくる。
Scalaはcaseで宣言した変数のスコープはそのcase内だけになるので、あっちこっち見る必要がない。何に対してmatchをかけてるのか、どのcaseかだけ見れば何をやっているのか読み解ける。
それに、マッチしたあとに元々の名前を使わなければならないっていうのが足かせになるケースもあると思うんですよ。
Scalaだと、
1 2 3 4 5 | val maybeUser = tryToGetUser(session) // ここではまだユーザーが取れたかどうか分からない val userName = maybeUser match { case None = > "invalid session!!!" case Some(user) = > user.UserName // ここではもうユーザーが取れたことがはっきりしてる } |
というふうに書けますが、Kotlinだと名前を置き換えれないので
1 2 3 4 5 | val user = tryToGetUser(session) val userName = when(user) { null = > "invalid session!!!" else = > user.UserName } |
と書くしか出来ません。上記の例だとuserとwhenの間に何もないのでまだマシですが、もしこの間になにか処理が入ってきたらどうでしょう。userがnullかどうかわからないまま処理するって気持ち悪くないですか?nullなのか値があるのかはっきりさせてから処理を進めるか、もしくは逆に絶対ぬるぽが出ない状態で処理を進めてからnullの場合と値がある場合とで分けるという書き方のほうが良くないですか?
いや、たぶんKotlinは後述するようにnull safetyのための演算子があるのでそれを毎回使うのがデフォなんだよ、コンパイルエラーも出るから良いでしょ、ってことなんだとは思うんですが、それがあんまり使い勝手良くないと思っていて、だから早めにearly returnみたいな感じでnullな時の制御を分けておきたいので上記のようなことをしてる。
郷に入っては郷に従えってことなんかな。
whenの一つのケースは単一ステートメントを書く
言語仕様をちゃんと見てないから変なことを言ってるかも知れないけど、どうもwhenの右の->に続くものは単一のステートメントでなくてはならないみたいだ。だから複数行コードを書かなきゃならないときはブロックにする必要がある。
Scalaなら、
1 2 3 4 5 6 | val statusString = getData(fileId) match { case None = > logger.warn( "access to illegal data" ) "no such data" case _ = > "data exists" } |
というふうに書けますが、Kotlinだと
1 2 3 4 5 6 7 8 | val data = getData(fileId) val statusString = when(data) { null -> { logger.warn( "access to illegal data" ) "no such data" } else -> "data exists" } |
になる。ステータス文字列を取り出すコードの中にロギングを含めるのは良いのか、とかいうツッコミはやめてね。例だから。
whenやmatchのモチベーションって「もっとマシなswitch」だと思うのですが、CでもJavaでもC#でもswitchってブロックじゃなきゃいけないなんて制約なかったじゃないですか?みんなそれで慣れてるじゃないですか?それをここに来てブロックを導入して何か嬉しいんですか?見やすいですか?タイプ量が増えてうざいだけだと思うんですけど。
ラムダ式の文法が変
Kotlinで一番気に入らないのはラムダ式の文法です。
1 | { "aaa" } |
Kotlinで上記のコードは、() -> kotlin.String型と解釈されます。これ、ラムダ式です。初めて見た時引っくり返りそうになりました。
だって、みんなが知ってる構造化プログラミング言語で{}ってブロックって意味じゃない?なんでこれがラムダ式になるのよ。C#もJSもJavaも ()-> { ... } のスタイルじゃん。せいぜい->か=>かの違いじゃん。
※ラムダ式じゃなくてアロー関数だとかいうツッコミは勘弁してね
で、Kotlinで引数を取るラムダ式はこういう書き方になります。
1 | { a : String, b : String -> "aaa" + a + b } |
これは書き方の問題なんで、慣れりゃ問題ないとはおもいますが、わざわざこんな分かりにくい(当社比)文法にするのって意味あんのかなぁ。
ifやmatchでは普通にブロックを使う
ただ{}と書くとラムダ式(関数オブジェクト)になるのに、ifやmatchでは普通にブロックの意味で{}を使ってるんですよ。
1 2 3 4 5 6 7 8 | val data = getData(fileId) val statusString = when(data) { null -> { logger.warn( "access to illegal data" ) "no such data" } else -> "data exists" } |
↑これが
1 2 3 4 5 6 7 8 | val data = getData(fileId) val statusString = when(data) { null -> { logger.warn( "access to illegal data" ) "no such data" }() else -> "data exists" } |
↑こうならまだ筋が通ってると思うんですけど(分かりにくい&めんどくさいけど)。
ブロックの意味で{}を使えない
スコープを制限するために{}を使うことってありません?JSはオブジェクトになっちゃうので出来ませんが、C#やJavaでは私はたまに以下のような書き方をします。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | public void foo() { // pre-process { String a = foo(); String b = baa(); brahbrah(a + b); } // post-process { String c = boo(); String d = bee(); hoge(c + d); } } |
こうすることで、変数a, b, c, dは何処で必要なのかはっきりしますから可読性が上がりますし、頻度は少ないですが最初に出てきたa, bという名前を後で全く同じ名前で使いたい時に再代入しなくてもよくなります。
Scalaではブロックも値を返すので、変数の初期化時が非常に助かります。
1 2 3 4 5 6 | val baa = { val a = preProcess 1 () val b = preProcess 2 () val c = postProcess(a.multiply(b)) c.result } |
こうすることで、一つ値を取ってくるのに複数行書かなければならない場合に「ここはbaaを作るためのコードなんだよ」ってはっきり明示できます。
Kotlinでは{}はラムダ式になってしまうので、
1 2 3 4 5 6 | val baa = { val a = preProcess 1 () val b = preProcess 2 () val c = postProcess(a.multiply(b)) c.result }() |
こう書かなくてはなりません。すると、これは内部的には関数オブジェクト的なものを使ってそれを呼び出している事になるのでオーバーヘッドがありますし、最後の謎の()は何よ?って感じになるので可読性の点でもよろしくないと思います。
イディオムとしてこういうスタイルが普及すれば可読性の点では問題ないのかも知れませんが、そもそも後発の言語なのだからこんなもんは言語仕様で吸収しろよって話にもなると思います。
関数ではreturnを省略できない
ラムダ式ではreturnを省略できるのにfunから始まる関数の定義ではreturnを省略するのは許されません。これすごく違和感あります。だって、KotlinはScalaと同じくifやwhenが式になってるんですよ。なにが言いたいのかというと、例えばScalaの場合、
01 02 03 04 05 06 07 08 09 10 11 | val a = { "aaa" // returnいらない } val b = if (condition){ "a" } else { "b" } // returnいらない({}も省略可) val c = (boo : String) = > { boo + "hoge" } // returnいらない({}も省略可) def d() : String = { "aho" // returnいらない(つけても良い、 {}も省略可) } |
となってるんですよ。なので、とにかく{}の一番最後が値として返ってくるんだと覚えとくことができます。
ifもmatchも値返すのにreturnをつけなくていいならfunだってreturn無し許容しろや、って私はScalaを最初に覚えたのでそう思ってるだけなのかも知れないですけど、いや、省略できたほうが嬉しくない?そのほうが統一感あるじゃん。
引数リストを複数取れない
Kotlinだと、ラムダ式を一つ取る高階関数は()を省略できるみたいです。
1 2 3 4 5 6 | fun voo(f : (String) -> String) : String { return f( "bababa" ) + "hoge" } voo({it + ":" }) // -> returns "bababa:hoge" voo{it + ":" } // -> returns "bababa:hoge" |
でも引数リストを複数取ることが出来ないんですよね。あーっ!惜しい!実に惜しい!すごく惜しい!めっちゃ惜しい!
foldとか使うことを考えて下さい。普通に書いたらこうですよね。
1 | list.fold( 0 , { a, b -> a + b }) |
こう書きたくないですか…?
1 | list.fold( 0 ){ a, b -> a + b } |
Scalaでは引数リストを複数取ることができます。そして、最後の引数リストを以下のように書けます。
1 2 | def foo(a : String)(f : String = > String) : String = { f( "aaa" ) + a } foo( "bbb" ){ a = > a + ":" } // returns "aaa:bbb" |
これを応用することで独自の制御構文的なものを作れますので、ローンパターン(C#のusingとか)なんかを作れます。
01 02 03 04 05 06 07 08 09 10 | def using[A, R < : Resource](r : R)(f : R = > A) : A = try { f(r) } finally { r.dispose() } using(createDisposableResource()) { s = > s.writeOut() } |
とか。
私がよくやるのは、例えば認証が必要な場合に
1 2 3 | Authenticated(session){ userInfo = > blahblah(userInfo); } |
みたいな書き方ができるようにしておくことでしょうか。
Authenticatedはセッションからユーザー情報を取得してユーザーがあればふたつ目の引数リストに与えられた関数を実行して値を返し、そうでなければ認証エラーを意味するオブジェクトか何かを返します。
こういうのを作っておくとJavaでやるようなアノテーションベースのAOPを使わなくても良くなるんですね。そうするとコードが追って行きやすくなって可読性が上がります。
追記:
ごめんなさい、Kotlinでも同じように書けるみたいです。
1 | list.fold( 0 ){ a, b -> a + b } |
null safeのための演算子よりモナドのほうが便利
なんかすごく荒れそうな題ですけど、ホントそう思いますよ。
というかモナドが難しいとかスキル差が出るんで採用しないほうが良いとか言われてますけど、別にモナドなんて覚える必要無いんですよ。モナドを覚えるというかfor式とmapとflatMapを覚えればもう大体十分だと思うんですよね。だからこの項のタイトルも正確にはfor式とすべきですかね。
例えば、ユーザーAとユーザーBが友達か判定するという処理があったとします。
1 2 3 4 5 6 7 8 9 | val r = for { userA <- tryToGetUserInfo(arg 1 ) userB <- tryToGetUserInfo(arg 2 ) } yield userA.friends.contains(userB) match r { None = > Left( "invalid user id!" ) Some(r) = > Right(r) } |
LeftとかRightとかyieldとか何だよとScalaを知らない人は思うかも知れませんが、まあそこは本筋じゃないので流して下さい。本筋はforの部分なのでそこに注目して欲しいんですけど、userAとuserBがnullかどうか分からないまま処理(yield)をして、そのあとでどちらかがnullだった場合とどっちもnullじゃなくて値があった場合とで分けれるんですよ。userAもしくはuserBがnullでもヌルポが発生しないんですよ?これめっちゃ便利じゃないですか?私これ初めて使った時魔法かと思いましたよ。
でもKotlinのnull safetyな記法って、一つの対象がnullかどうかわからないってときはやりやすいんですけど、上記のように二つになったら条件分岐を挟まないといけなくなります。
01 02 03 04 05 06 07 08 09 10 11 | val userA = tryToGetUserInfo(arg 1 ) val userB = tryToGetUserInfo(arg 2 ) val result = if (userA ! = null ){ userB?.friends.contains(userA) } when(result){ null -> "invalid user id!" else -> result } |
KotlinにEitherが無いのでwhenのところは無理やりScalaに合わせたので厳密にこれで同じコードになっているわけではないですが、まあ察して下さい。
タイプ量もそうなんですけど、私はKotlinの方のif分のあたりが相当可読性が悪いように思います。これ、ぱっと見で何をやりたいかわかります?一方でScalaの方は「まぁnullかも知れんけどとりあえずやってみようや、ダメだったらあとでなんとかしたろ」っていう流れになってるので、わたしはこっちのほうが分かりやすいと思ってます。
Kotolinってミスタイプしそうになる
何回書いても間違いそうになります。
関数の宣言がfunだけど全然楽しくない
funって言ってるけどKotlinで打ってても全然楽しくありません。楽しくないのにfunと書かされる私の苦しみを味あわせてやりてえ。
まとめ
ここで書いたことは「それ、Scalaでできるよ」の繰り返しなのでScalaでできるハラスメントだ!!!って思う方もいるとおもいますが、でも色んなことできるなら便利な言語使ったほうがよくね?と私は思います。scalazとかcatsとかshapelessとかはまだしも、上記のfor式くらいまでは覚えておいたほうがメリットのほうが大きいと思うんですよね、Kotlinのnull safety / whenよりだったらscalaのfor式 / matchのほうが絶対便利だと思う。
そうじゃなくて、お前が便利だと思っているソレは実はこういう具体的な理由があるので俺は便利じゃない、というのならばわかります。じゃあkotlinなり何なりあなたの好きな言語で打って下さい。でも理由が「自分にはわからないから」だったらそれは損してると私は思いますよ。
でもまぁScalaをプロダクトで採用するのって多くの現場(特にSI)では嫌われますし、顧客がScalaヤダって言えばそれまでの話なんですけど。