ツクールMVで関数型プログラミングに入門する その1

はいどーも三度の飯より関数が嫌いなツミオです。
本日は関数型プログラミングに入門してみようと思います。
なお僕は関数型についてはHaskellを少しかじったくらいです。
ここでは色々と書いていますが、あくまでも「僕が勉強したことをまとめている」くらいのものなので、正確性は保証しません。参考程度にしておいてくださいね。

あ、あと例によってツクールMVでエンヤコラします。

アクターの名前を取得する方法

ツクールMVで、パーティ内の1番目のアクターの名前を取得してみましょう。
これをオブジェクト指向のパラダイムで考えたとき、例えば以下のようなコードになります。

あまりにも普通ですね。
$gamePartyグローバルオブジェクト(Game_Partyのインスタンス)が保持しているmembersメソッドが返す最初の配列の要素(Game_Actor)が保持しているnameメソッドを呼んでいます。
改めて文章にしてみるとややこしいですが、多分みなさん無意識に使っていると思います。

一方で関数型のパラダイムで考えたとき、例えば以下のようなコードになります。

fetchMembers関数はGamePartyのインスタンスを受け取り、Game_Actorが詰まっている配列を返します。
fetchFirstMember関数は、Game_Actorが詰まっている配列から、空要素を除く最初の要素を返します。
getActorName関数は、Game_Actorのインスタンスのnameメソッドを呼び、文字列を返します。

最後のやつはそれをまとめて呼んでいる感じです。
めちゃんこ読みにくいので、可能であればメソッドチェーン形式とかにしたほうがいいですね。
というかあとでします。

なおわかりやすさのためにfetchFirstMember関数を定義しましたが、もうちょっと分解するなら以下のようにできるかと思います。

関数型はthisが嫌い

上記のコードだけでは「こんなのオブジェクト指向の方がいいじゃないか」となると思います。
ではここで改めてオブジェクト指向のパラダイムに戻りましょう。
先程の結果として得られた文字列を反転させたいとします。
オブジェクト指向ならば例えば以下のようなコードが考えられるでしょう。

上記のコードを見てください。
reversedNameは自分のインスタンスを表すthisを使っています。
関数型は基本的にこのthisを避ける傾向にあります。
実際、最初に示した関数型のコードにはthisは一つも出ていません。

では関数型で文字列を反転させてみましょう。
まずは反転用の関数を用意しなければなりません。
というわけでドン。

あとは先程の結果をコイツでくるんでやるだけです。
こんな感じ。

これで反転した名前が得られます。

関数型だと何が嬉しいのか

ここまで見たとき、関数型だと何が嬉しいのかイマイチよくわかりません。
なんかごちゃごちゃした書き方になっただけのように思います。
まあ実際これだけだと大してメリットない気がするんですが、例えば文字列を反転させる関数はアクター名以外でも反転させることができます。
対してGame_ActorクラスのreversedName関数は自身の保持する名前にしか使えません。
まあGame_ActorクラスにreverseStringみたいな名前の関数を追加して、それ使えばいいじゃん、みたいな話はないこともないのですが、これは単一責任原則に明らかに反します。
あとライブラリを使えばごちゃごちゃしたコードがマシになります。これについてはあとで触れます。

ただの関数との違い

ここまでで作成した関数には以下のようなものがあります。

これらと普通の関数は一体なにが違うのでしょうか?
まず着目するべきなのは、全ての関数が何らかの値を受け取り、何らかの値を返していることです。
関数型は値を返して、その結果をいじくり回すので基本的にvoidはあり得ません。
また全てのコードは参照透過性を持っています。
すなわち、引数が同じなら必ず同じ結果が返ってきます。
「そんなの当たり前じゃないか」と思うかも知れませんが、例えば手続き型に典型的なコードとして、以下のような関数があります。

引数に「ハロルド」を渡したとき、スイッチ5番がtrueならば何も表示されず、そうでなければコンソールログに「ハロルド」と表示されます。
引数を受け取り値を返してはいるものの、このメソッドは参照透過性を持っていません。
なぜなら、引数が同一であっても、グローバルオブジェクトである$gameSwitchesの状態によって結果が変わってしまうからです。
こういったグローバルオブジェクト(典型的にはシングルトン)に依存するコードは管理しにくく、テストも難しくなるため、オブジェクト指向のパラダイムでもなるべく避けたほうがよいと考えられています。

さて次に注目して欲しいのが、全ての関数が副作用を持っていないことです。
副作用を持っていないとは、すなわちオブジェクトの状態を変更しない、ということです。
オブジェクト指向のパラダイムでは、オブジェクトの状態は変更するのが普通です。
ですが関数型プログラミングのパラダイムではそもそも状態を持たないことが普通なため、従って状態も変更されません。
ただオブジェクト指向のパラダイムでも、「状態を変更しながら値を返すメソッド」はかなり使いにくいため(単に返された値を使いたいだけなのに、状態まで変更されると都合が悪いですよね。getterに副作用があるときのことを想像してもらうとわかりやすいと思います)、基本的にこの2つは分離するようにコードを書くのが普通です。

チェーン状にする

関数型のコードとして紹介した

これ、ぶっちゃけ読みにくいですよね。
こんなので書かなきゃいけないなら、オブジェクト指向でいいやとなりかねません。
チェーン状で書けるようにするため、Lodashを利用します。
Lodashの機能を利用すると、例えば以下のように書けます。

ずいぶんわかりやすくなりましたね。
ただ個別の関数を書くよりも、Lodashにビルトインの関数を使ってメソッドチェーンを組み立てた方がよりわかりやすいかもしれません。

どうもLodashの仕様として、firstした時点でラップが解けるようです。
なのでfilterのあとにtake(1)するという面倒な手順を踏んでいます。
また、見ての通り最初はthru(fetchMembers)と変わっていません。
無理やりthru(x => x.members())みたいなこともできなくないですが、ウーン。
マイチな感じが否めないのですが、こういう場合はどうするのがいいんでしょうねえ……。

map等について

上述したコードにはmapやfilterが使われています。
これはArrayオブジェクトのものと似ていますが、遅延評価される点が違います。
mapやfilterはループ処理を置き換えることに使えるため、ガンガン使っていきたいですね。
あとそもそもの話ですが、関数型プログラミングではforやらwhileやらはあんまり使わないようです(少なくともHaskellには構文自体が存在しない)。
代わりに再帰処理を使いますが、まあそれはおいおい。

遅延評価とは

関数は本質的に遅延評価です。
例えば以下のコードを見てください。

このコードが宣言されたときに「5」という値が返るわけではありません。
returnFiveという関数が実行されたときに5という値が返されるのです。
すなわち、実行するまで値は返ってこないのです。
「なにを当たり前な……」と思うかもしれませんが、関数型ではこの性質をうまく使ってナンヤラホイするみたいですよ。

終わりに

Haskellでの関数型プログラミング入門が無理ゲー過ぎたので、とりあえず馴染み深いJavaScriptで再入門してみることにしてみました。
今のところ特に詰まる部分はなくいい感じですが、モナドやらナンヤラが出てきたあたりで呻いています。
ウーン続くかどうかは謎ですがとりあえずその1ということで。

ほなまた。
お仕事も募集中です。

フォローする