LINE Game Cloudの例に見る関数型言語の特徴

こんにちは。LINEでゲームプラットフォーム開発を担当しているジュニア開発者のBusung Kim、Jaeho Leeです。LINE Game Cloudが関数型プログラミング言語の一つであるClojureで実装されているのを見て、関数型言語に興味を持つようになりました。本記事では、LINE Game Cloudの例に見る関数型言語の特徴をいくつかご紹介したいと思います。

LINE Game Cloudと関数型プログラミング言語

LINEは、グローバルにゲームサービスを安定して提供するため、LINE Game Cloudというクラウドベースのゲームサーバープラットフォームを構築・運用しています。LINE Game Cloudは、サービスのグローバル化やリリースプロセスの自動化を目指して始まったプロジェクトです。現在、同プラットフォームを通じて世界各地のユーザにゲームサービスが提供され、サーバーの自動手配、L4/L7ルーティング、DNS、自動スケールといった対応が行われています。プロジェクトの詳細については、下記ページからご確認いただけます。

LINE Game Cloudの仕組み

下の図はLINE Game Cloudの仕組みを表したものです。ドッカーコンテナ(Docker Container)のプロビジョンを担当し、様々な設定情報を生成するオーケストレーション・エンジン(Orchestration Engine)と、一つの物理デバイス上で動作し、オーケストレーション・エンジンから生成された情報を基にドッカーコンテナを制御するプロビジョニング・エージェント(Provisioning Agent)、そして各ドッカーコンテナ上でサービスインスタンスを管理するノード・エージェント(Node Agent)の3つのモジュールからなっています。これらのモジュールはいずれもClojureで実装されています。

LINE GAME Cloudに関数型プログラミング言語が選択された理由

LINE GAME Cloudの開発当初のメンバーはわずか2人。決して十分とはいえない人数と時間でグローバルサービスを提供するにはそれまでとは違うアプローチが必要でした。まず、プロトタイプの作成からテストに至るまでの一連のプロセスをスピーディーに進めるために、プログラムはなるべくシンプルに作成することが求められました。そして、世界中から収集される膨大な量のデータを効率よく捌くには、プログラムは並行性にきちんと対応したものでなくてはなりません。安定したランタイム環境も必要で、モジュールは以前作成したものをできるだけ再利用したいと考えました。

それらの要求に適していると目されたのが関数型プログラミング言語でした。関数型プログラミング言語を利用すると、コードが簡潔に書けるため開発時間の短縮につながります。さらに、副作用(Side Effect)を許容しない純粋関数(Pure Function)指向であるため、複数のスレッドで同時に問題なく動作するプログラムが簡単に作成できます。私たちの選択はClojureでした。

関数型プログラミング言語の特徴

このようにLINE GAME Cloudに採用された関数型言語ですが、その特徴としては以下のような点が挙げられます。

不変性(Immutability)

関数型言語は不変性を追求したプログラミング言語パラダイムといえます。要するに、可変の部分を極力少なくしたプログラミング言語、ということです。純粋関数指向のプログラミング言語とも言われます。純粋関数とは、内部状態を持たないため同じ入力に対して常に同じ出力が得られる関数、つまり副作用のない関数と言い換えることができます。数学における三角関数が純粋関数の一例です。関数内部に状態が存在せず、関数の出力は入力による影響しか受けないからです。このように不変性を追求することによって得られるメリットは様々ですが、いくつかご紹介します。

プログラムの検証が簡単

不変性は、プログラムの検証にかかる手間を減らしてくれます。まず、プログラムを構成する各モジュールが入力による影響しか受けないためテストコード作成が簡単です。そして、不意に変更されかねない内部状態を持たないためプログラムが予測可能なものになります。

最適化を実現

さらに不変性は、様々な最適化を実現してくれます。以前計算した関数の値をキャッシュして必要なときに再利用する、いわゆるメモ化(memoization)は、関数の不変性が保証されない限り不可能です。呼び出し毎に結果が変わるような関数ではキャッシュする意味がないからです。不変性はJVM、CLRのようなランタイム環境がコードの順番を任意に再配置できるようにする根拠になります。結果が関数の呼び出し順によらず入力にのみ依存するため、必要なときにランタイムがコードの実行順を自主的に変更し、性能向上につながるのです。

LINE GAME Cloudでは、以下のようにメモ化を活用しています。make-auth-config-という関数は、username、password、server-addressの3つの引数が入力されるとAuthConfigというクラスのインスタンスを返却します。この関数は純粋関数です。つまり内部状態を持たず、関数の結果は入力による影響しか受けません。よって、この関数はメモ化できます(最終行)。そしてその結果、関数が一度でも呼び出され、同じ値を持つ引数の組み合わせ(username, password, server-address)が入力されると、関数のボディを実行せず、以前保存しておいた結果を返すことになります。

(defn make-auth-config-
  [username password server-address]
  (-> (AuthConfig/builder)
      (.username username)
      (.password password)
      (.serverAddress server-address)
      (.build)))

(def make-auth-config (memoize make-auth-config-))

並行性プログラムの作成が容易

マルチプロセッサ環境で動作する並行性プログラムを作成するとき、関数型言語は有効です。並行性プログラムが組み難いのは、たくさんのスレッドによってプログラムの状態が共有されるからです。ところが関数型言語では可変状態は根本的に排除されているので、プログラマはロック(Lock)、同期(Synchronize)といったスレッドまわりのややこしい問題を気にすることなく、コアロジックの実装に集中できます。

第一級(First-class)、高階関数(higher-order function)

関数型プログラミングで関数は「第一級市民(first-class citizen)」です。第一級市民としての関数とは、変数に割り当てたり、他の関数のパラメータとして渡したり、他の関数の戻り値として返したりすることのできる関数を指します。この関数は一つの値のように扱うことができることから、オブジェクト指向パラダイムでのクラスのように再利用することができ、コアコードをボイラープレートなしで簡単に表現できます。最近多くのプログラミング言語がラムダ(無名)関数に対応するようになったのも、ボイラープレートのないシンプルな関数を渡すためと思われます。第一級市民としての関数は、高階関数(Higher-order function)の表現を可能にします。高階関数とは、引数として渡される関数を利用して作られた新しい関数のことです。高階関数を利用すると、部分適用(partial application)やカリー化(currying)が可能になり、より簡潔なプログラムが作成できます。以下に、LINE GAME Cloudで第一級市民としての関数と高階関数がどのように活用されているか見ていただきましょう。

(defn listing-all-containers [docker]
  (let [lists (.listContainers docker (into-array [(com.spotify.docker.client.DockerClient$ListContainersParam/allContainers)]))]
    (map (fn [list]
           [:id (.id list)
            :names (.names list)
            :image (.image list)
            :imageId (.imageId list)
            :command (.command list)
            :created (.created list)
            :status (.status list)
            :ports (.ports list)
            :labels (.labels list)
            :sizeRw (.sizeRw list)
            :sizeRootFs (.sizeRootFs list)]) lists)))

上記`listing-all-containers`関数は、すべてのドッカーコンテナをリストアップするときに使われる関数です。その流れはまず、パラメータとして渡されたドッカーというインスタンスのメソッドである`listContainers`を呼び出してコンテナリストを取得し、そのリストの各要素にラムダ関数を適用して新しいコレクションを作ります。この関数から宣言部を除いてシンプルに表現すると、以下のような形になります。

(map
    (fn ...)
    lists)

`map`関数には、2つのパラメータが入力されます。1つ目はラムダ関数を利用してリストの各要素に適用するコードブロックで、2つ目はラムダ関数を適用するコレクションです。このように関数型言語では関数を他の関数のパラメータとして渡すことができますが、それはこの関数が「第一級市民」であるからです。そして上記例の`map`関数は高階関数の一例でもあります。`map`関数とラムダ関数を組み合わせて新しい関数が作られたのです。

遅延評価(Lazy evaluation)

関数型言語は、遅延評価(lazy evaluation)に対応します。遅延評価とは、ある値が実際に使われるまでその評価を遅らせることです。値をあらかじめ計算・保存しないためスペースが省ける上、必要なときに値の評価を行うことでプログラムの性能にもプラスになります。遅延評価を利用すると、無限数列を表現することもできます。そのときは数列のすべての値をメモリに保存するのではなく、数列の表現を宣言し、必要なときに評価を行って値を生成します。この遅延評価は主にメモ化とともに使われます。値が必要なときに評価を行い、次にその値が必要になると再度評価せずキャッシュしてある値を再利用するわけです。遅延評価が本領を発揮する代表例として、フィボナッチ数列(Fibonacci sequence)があります。

下記コードに、`fib`という名前のフィボナッチ数列生成関数を定義してあります。この関数は個数の定まっていない無限のフィボナッチ数列を生成するように実装されたもので、まだ値が必要なわけではないため、実際は一切評価を行いません。

(defn fib []
    (map first
        (iterate
            (fn [[a b]] [b (+ a b)])
            [1 1])))

以下のように必要な個数をパラメータとして渡して関数を呼び出すと評価を開始します。

(take 10 (fib))
;Output => (1 1 2 3 5 8 13 21 34 55)

おわりに

ここまで、LINE GAME Cloudの例に見る関数型プログラミング言語のいくつかの特徴について解説いたしました(LINE GAME Cloudに関数型プログラミング言語が選択された理由を説明してくれたYoungho Choiさん、ありがとうございます)。関数型言語で作成されたプログラムは生産性、メンテナンス性、並行性の面で多くのメリットがあります。ゲーム分野においても関数型言語を利用した様々な工夫が行われていて、成功例も報告されています。LINEもそうした流れを受け、ゲームサーバー開発に関数型言語を積極的に活用する方法を検討しているところです。どうか良い結果につながることを願っています。以上、お読みいただきありがとうございました。

Related Post