ちょっと twitter で文法の話とか難しさの話とかを見たので、思い出して書いてみる。
チラ裏的なそんな何か。
そもそも今も別に業務で使ったりしてないので scala スキルは別に全然ないから、全く大した話はできない。
脱初学者が超初学者の助けになれば良いな、というユルい感じでお願いします。
背景
なんかプログラマになって2年目くらいで触った気がする。
当時は php だけ知ってか、python もちょっと触ってたか、というくらい。
java の経験はなかった。
あぁ、javascript と jQuery はあった。(今思うと全然わかってなかったけど)
java と scala の違いという趣旨の本を読みながら写経したけど、「そもそも java シラネー」って思いながら読んでた。
「へー、java って php と違って__constructじゃあないんだ...」ってレベル。
scala でつまずいた箇所
「なんか省略できるっぽい、記号が多い」って印象がほとんどだったと思う。
Optionとかmatch-caseは全然知らなかったので「へー...??」って流してたので、つまずいたとかではない。
staticやenumは当時は「同じ事が出来るけど書き方が違う」と思っていたので、都度 gg っていたのでつまずいた覚えはない。
具体的にはほとんどがclassの中に書いたりするdefの部分と、あとちょっとが_とかitとか=>とかだった気がする。
具体的に
実はもう何がわからなかったのかわからないのだけれど、こんなんだった気がするという感じで整理してみる。
普通の変数宣言
変数名と型があって、右辺に値がある一番シンプルな例
型の有無は違うけど、これは php と似てた気がしたので平気だった
def name: String = "john"
型は省略可能
明らかな場合は省略できるらしい
まだ平気
def name = "john"
メソッド定義
なんか引数とか戻りとかが逆で気持ち悪いなー
けどまぁそういう決まりだし、慣れるしかない
けどなんか=入ってるのが気持ち悪いなー
って思った
なんで=が気持ち悪いと思ったかはもう思い出せないけど、多分「メソッドはfunctionで宣言する特別な何か」で「=使うのはただの変数に使うやつ」とか思ってたんじゃあないかな
def add(x: Int, y: Int): Int = {
x + y
}
{ }が省略できるらしい
「本文が1行なら{ }は省略できる」とか見たんだっけかなぁ
この辺から「えーなにそれ、なんで1行だと特別扱いしてんの」ってくらいに思ってた
def add(x: Int, y: Int) = x + y
それと、あと上で似たこと書いたけど「=は変数への代入に使うもの」だと思っていたので、=より左に( )があることが気持ち悪かった気がする
=>出現
この辺から記述の切れ目がどこだかさっぱりわからなくなる
=>と=を「どっちが右か」とかで把握しようとしてると多分この辺で限界が来るんだと思う
コンパイルが通るまで適当にいじってみるけど、f:の型注釈とx:の型注釈の違いを理解していない
def f: (String => String) = x => x + "!"
def f = (x: String) => x + "!"
あとこれはどうして定義するときにfなのに、呼ぶときはf("hello")みたいに( )が出てくるのかもよく分かってない
( )が出来るのはadd(x: Int, y: Int)みたいにした時だけじゃあないのか?
どこからともなく現れる恐いお兄さんたちが残していった呪文
理解するのを放棄
def f(s1: String): (String => String) = s2 => s1 + " " + s2
ちなみに、どう実行するのかもわからない
引数がないと呼ぶときの( )は省略できる
def f(): String = {
"hello"
}
これは呼ぶときにfでもf()でも良い
scala> f
res8: String = hello
scala> f()
res9: String = hello
なのに下の例(再掲)は定義に( )はなかったのに、( )を付けなければいけなかった
何が省略できるのかさっぱりわからない
def f = (x: String) => x + "!"
つまずき方のまとめ
こうして振り返ると、見知らぬ記述があったときに「この場合は=より左に( )が書ける」とか「〜の場合は〜が省略できる」みたいに場当たり的に理解していたのですぐ限界が来た感じがする。
じゃあどうするのか
実はここから先をどうしようかとても困っている。
僕は結局 scala は「んー、scala じゃあないと出来ないこと今はないし、いーや」って思ってこの後ずっと触ってなかった。
んだけど、色々彷徨っている間に haskell をやって、ある程度 haskell がわかってからもう一度 scala 見てみたら上の問題は全部一発で理解できた。
だからと言って今困っている人に「 haskell やれ」じゃあ身も蓋もないので、ちょっと簡単に考えてみようかなと思う。
本当今思いつきで書くので全然程度は高くないです。
とりあえず右と左に分けて考える
( )とか{ }とか=>とかあるけど、一番ポイントなのは=だと思います。
上の例って全部「=が1つ」だと思います。
なので超雑におっきくまとまりで捉えると、こうなってると思います。
def 左 = 右
左
左は定義と捉えると良いと思います。
変数の名前やメソッドの名前です。
それから型注釈は定義部に入ります。
def f(s1: String): (String => String) = s2 => s1 + " " + s2
先ほど上げた僕が理解を放棄した例です。
=より左が定義部です。
その定義部はf(...)とfの型注釈に分けて捉えれば良さそうです。
fの引数がString1つで、fの戻りの型はString => Stringです。
ただ1行を眺めると=や( )がいくつか目に入りますが、最初に見るべきはこの様に=の左側で良いと思います。
右
対して右は値と捉えれば良いと思います。
= 5だったり、= return "John"だったり、= { 複数行 }だったりします。
ところでreturnは省略出来ますので、= 5は= return 5とも捉えられます。
さらに1行の場合は{ }も省略出来るので、= 5は= { return 5 }と捉えられます。
こうして考えると、= 5も= { 複数行 }も同じだと思えませんか?
また、値として扱えるのは5や"John"だけではありません。
def f: (Int => Int) = x => x + 2
この様にfという変数にx => x + 2という処理を束縛出来ます。
ですのでfの値はx => x + 2です。
def f: (Int => Int) = {
return x => x + 2
}
こうして見れば値部分は= 5も= { 複数行 }も=>が入ってるのも文法的には全部同じだと思える様になると思います。
もう一度先ほどのややこしい例をあげます。
def f(s1: String): (String => String) = s2 => s1 + " " + s2
(もしくは区切りが欲しければこう読み替えても良い)
def f(s1: String): (String => String) = {
return s2 => s1 + " " + s2
}
fの型注釈のString => Stringとreturn部を見ると、値はs2 => s1 + " " + s2の部分です。
そしてs1: Stringを受けたらそれを返すのがfですね。
fにStringを渡すとString => Stringが得られるので、以下の様に使います。
scala> f("Hello")("John")
res15: String = Hello John
scala> f("Hello")("Jane")
res16: String = Hello Jane
(おまけ f(...)(...)が何の役に立つのか)
s2 => s1 + " " + s2が値としてreturn出来ると言うことは、それを変数に受けることも当然可能ですよね。
なのでこういう事も出来ます。
scala> def greeting = ??? // 時間によって f("Hello") か f("Good night") が実行される何か
greeting: String => String
scala> greeting("John")
res17: String = Hello John
scala> greeting("Jane")
res18: String = Hello Jane
例えばまず「環境毎に異なる設定ファイルパスを指定して」次に「本当にやりたい処理を指定する」みたいに、段階を踏んだ処理を分離して記載したり出来ます。
他
今ざっと昔混乱した記述を見返していて、あと1つちょっと別に理解しておいた方が良いことがあったので、それだけ書いておしまいにします。
def f1(): String = {
"hello"
}
scala> println(f1)
hello
scala> println(f1())
hello
f1()で定義しているこれは( )の省略が可能で、
def f2 = (x: String) => x + "!"
scala> println(f2)
<function1>
scala> println(f2("foo"))
foo!
こっちはf2で定義してるのに( )の省略が不可能、という話について。
さっきのf("Hello")("Jane")の例もそうだけど、自分が何に( )を付けているのかを理解しながら読み書きする癖を付けると良いと思います。
f1の方はf1()に引数が必要なく、値部は"hello"なのでf1()を実行すると"hello"という文字列が得られます。
対してf2の方はf2という変数の中身がx => x + "!"という処理なので、そいつに対して("foo")をしています。
ついでに言うとf("Hello")("Jane")はs2 => s1 + " " + s2を手に入れるために("Hello")をして、手に入ったそれを実行するために("Jane")しています。
この様に「何に対する( )か」を考えると理解度がぐっとあがるんじゃあないかな、と思います。
イメージとしてはfを実行するために( )を付けるのか、fの中に入ってる処理のために( )と言った感じ?
BNF
ブロックで捉えるというのは BNF (wiki) を見るともっと厳密に学習できます。
https://www.scala-lang.org/files/archive/spec/2.12/13-syntax-summary.html
興味があれば覗いてみると良いと思います。defで文字列検索してそこから辿ると関連したのが見つかりそう。
最近わけあって java のはじっくり見てたけど、scala のは初見。雰囲気全然違うなぁ。
おしまい
あんまり解説する気はなかったんだけど、いくらなんでも投げっぱなしかと思って書いてみた。
けどとても拙いので、なんとも言えない気分。
この辺りの混乱した箇所は今はもう大筋は理解できているので難しいとは思わないけど、こういう解説(?)が必要になる時点で難しいだろ、という事なのだろうか...
多分難しい難しくないではなくて、違うパラダイムだよ、ってだけの話なんじゃあないかな...
当然の様に継承とかは使うだろうけど、それだって最初は難しかったんじゃあないかなぁ
ちなみにこのパラダイム(?)に近いのは身近なところだと javascript とか jQuery とかだと思ってます。
あいつらコールバックに処理自体を渡したり、{ }( )とか( )( )とか良くやってるイメージなので似ていると思う。
もちろん haskell も。ぶっちゃけ haskell の関数は全てカリー化されているというところまで分かれば scala のここまでは一発で分かると思う。
勢いで始めたけど微妙なポエムになった。ちょい残念。