現在、ScalaMatsuri2019
が開催されております。
二日目に当たる2019/6/28、会社に「勉強会への参加を出社扱いにしてくれる制度」の申請を出して参加し、話を聞きに行きました。
その感想を述べさせていただきたいです。
筆者レベル
- Scalaを業務で使い始めて約3カ月
- C#やっていたので、オブジェクト指向寄りの考え方(むしろ関数型の考え方は全然わかっていない。モナドについて説明すらできない)
- DDDはかなり勉強したつもり(じわじわと実践中)
私のScalaレベル感は、以下の記事を見ていただけると分かるかと思います。初心者に毛が生えた程度です。
拝聴したセッション
- 関数型オブジェクト指向命令型 Scala
- Scala における型クラス入門
- コードから理解するPlayframeworkの脆弱性
- プロジェクトで引き回す型をEffにするメリット
- こんなに違う!<-ScalaとKotlin->
- Scala における関数型並行処理入門
- ScalaとDDDで作る広告配信システムの実態
- ピュアなドメインを支える技術
- 継続とDI
タイムテーブルを見ていただけると分かりますが、複数のセッションが同時に開催されます。その中で拝聴したいセッションを選ぶのですが、まあ選べない。聴きたいものが多すぎます。悩んだ末に、上のセッションを選びました。
感想とか
個々のセッションの感想は省きます。全体的に思ったこと、そして、Scalaを3カ月使い続けて思ったことを混ぜて書いてみます。
Scalaらしさ
関数型らしさ、オブジェクト指向らしさ、という観点ではそれぞれ良し悪しがあると思います。
関数型は、状態を持たない、副作用を持たないことが良しとされます。それにより、関数を連鎖させるのが安全に行える等の多くのメリットを生み出しています。しかし、連鎖が続くとソースが理解しがたくなる、という部分があります。(個人的な感想を多分に含むので、反論はあると思います)
オブジェクト指向は、オブジェクトに状態を閉じ込められる、関連のあるものをまとめられる、オブジェクト間のメッセージングを明確にしやすい等が良いところだと思います。しかし、オブジェクトが基本的に可変であるため、状態がどうなっているか保証が取れない場合があります。
関数型とオブジェクト指向の良いところをうまく融合することで、それぞれの持つ欠点をうまく克服しているのが、Scalaという言語だと実感しています。
オブジェクト指向的観点から言うと、関数型の複雑さはオブジェクトに閉じ込めればいいし、case classによる不変の保証はとても便利に扱えます。
DDDでは、わりとよく型変換が現れますが、これは型クラスを使っていくと解決しやすいです。ただ、変換を全体に当てていくと、EitherT
が変換されていくような型パズル*1が始まってしまいます。この型パズルが本当に初心者につらく、ハードルが高くなります。こういう、理解しがたい複雑さに関しては、オブジェクト指向でいうところのカプセル化で覆い隠せます。
また、オブジェクト指向では「クラスのフィールドは変更できる」点が多くの地獄*2を生み出してきました。DDDでも不変オブジェクトについて言及されます*3。その「不変オブジェクト」を、Scalaは言語レベルでサポートしています。関数型で扱う値としては必須の条件であるだけでなく、DDDの値オブジェクトとして扱うのにも適切ですし、並列処理を行う際も大きなメリットとなります*4。
個人的には、アプリケーション全体はオブジェクト指向、もっと言うならDDDをベースにした形に構成し、それを採用するうえで実現が難しい部分や汚くなりやすい部分について関数型の考え方を適用する、という形が良いのだろうと思います。
どちらかに寄りすぎることなく、両者の長所を活用して構築していくことがScalaらしさなのかな、となんとなく思い始めました。
式を構築する、という感覚に慣れる
Scalaが少々とっつきにくいと感じていた原因の一つが、「式は評価値を返すだけ」という部分が腑に落ちなかった点です。
とにかく、forとかmapが連鎖していくとわけが分からない、という感覚が強かったです。
値があるかどうか分からない文字列から、「値があるときだけ値を変換してから返してね」というときに、mapを使います。Option[String]
に対しmapを行うとOption[T]
(Tは処理に応じた型)が返ってきます。元の文字列が存在しない場合、map内に定義した関数はそもそも呼ばれない、という挙動です。
ざっくりした例です。
ResultType res = null; string value = DoSomething(); //stringを返す if (string.IsNullOrEmpty()) { res = // 変換処理 } return res;
val res = for { value <- doSomething //Option[String]を返す } yield { // valueを変換。valueがNoneならそもそも実行されない } res //Optionのまま返したり、デフォルト値を返したりできる
上のforの<-
が、map
の呼び出しになっています。これが基本で、これだけならそこまで難しくないです。
処理が複雑になるといろんなmapが出てきて理解不能で、mapやflatMapが、単に自身が内包する型を変換する手法を事前定義しているだけということが腑に落ちるまでは意味が分かりませんでしたし、forの右辺が揃っていないといけない理由も分かりませんでした。
ここが理解できてくると、OptionT
やEitherT
が便利な理由がわかってきます。今回拝聴した中では、継続モナドや型クラスがなぜ便利なのか、という話も理解できます。
…まあ、これがわかるための前提がOptionT
ぐらいは理解していること、というのがハードル高いと感じるところかもしれません。私の中でモナドは、「なんかこう、うまいところ型のネストを整理してくれて扱いやすくしてくれる便利なやつ」みたいな印象で、どういう考え方が裏にあるのかよくわかっていません。今の知識で改めて学習すると、理解が深まる気はします(記事書いたらググります)。
この程度の理解しかできていない私のような奴でも、それなりにコーディングできているScalaは、分かりやすい言語だなと思っています。
動かさないと分からない(から分かるまで動かす)
ScalaとKotlinのセッションで、「同じような名前のものでもちゃんと動作確認すること」というお話がありました。
これは、学習しているときも同様だと思いました。Option
の挙動が分からんなら、とにかくソースを書く。理解できるまで自分でサンプルソースを書く。
OptionT
やEitherT
についても、私は理解できるまでソースを書いていました。自分が「感覚的にこう動くものなのかな?」と想定した挙動と実際の挙動を比較して、事実と感覚のすり合わせを行います。それを腑に落ちるまでやるのは大事です。腑に落ちると、その事象は自分にとって制御可能になります。
業務で巨大なものを扱っていると、大きなままなんとかしなきゃ、と考えることがあります。このときも、どうやったら自分に制御可能なほど小さくするか、という観点を持ちたいです。たまに忘れるので。
おわりに
思うのは、どういう言語でどういう手法を取ったとしても、「分かりやすくする」という点が大事ということです。どんな言語、どんな手法を取ったとしても、複雑さはどこかに現れます。
Scalaは表現力豊かな言語で、いろんな書き方ができますが、いかにしてドメインを汚さないかという点が強調されている点は、異なるセッションであっても同様でした。
ドメイン層は業務知識(システムが対象としているドメイン)を表すので、技術的な詳細から守る必要があります。ここを分かりやすくしつつ、システム全体でもわかりやすくしていく。これは難しいことですが、挑戦する価値がありますし、Scalaはそれを実現するだけのポテンシャルがある言語だと感じています。
そのためには私自身もっと勉強と実践を繰り返さなければ…と日々感じています。ドメイン層にFutureとか出したくないと感じていたので、本日のセッションはすごく勉強になりました。
明日も(寝坊しなければ)行くので、楽しみです。
ふと思ったのですが、Scalaで Null Objectパターンってほとんど聞きませんね。Optionで事足りるからでしょうか。