パステル色な日々

気ままに綴るブログ

Elmのアプリケーションコードを読んでみよう

ElmアプリケーションはElmアーキテクチャーに沿って構築していきます。構築したアプリケーションコードはElmのコアコード上でアプリケーションとして振る舞います。

Elmのコアコードがどんなことをしているのか気になったので、今回はElmアプリケーションが起動するまでにどういうことをやっているのかを追っていきたいと思います。

起動呼び出し

Elmアプリケーションを起動するには以下の呼び出しを最初に行います。

var app = Elm.Main.fullscreen()

他にもいろいろ起動するために呼び出せる関数はありますが、とりあえずこちらから考えます。見て分かる通り Elm.Main オブジェクトを操作しているので、まずはこれがどういうオブジェクトなのか知ることから始めていきましょう。

Elm.Main

var Elm = {};
Elm['Main'] = Elm['Main'] || {};
if (typeof _user$project$Main$main !== 'undefined') {
    _user$project$Main$main(Elm['Main'], 'Main', undefined);
}

非常にシンプルなコードですね。_user$project$Main$main をみると、どうやらコンパイラモジュール名$関数名 という名前付けをしているようです。

Elm['Main'] にどういう変更が加えられるのか見ていきましょう。

makeProgram

function makeProgram(flagChecker)
{
    return F2(function(debugWrap, impl)
    {
        return function(flagDecoder)
        {
            return function(object, moduleName, debugMetadata) // == _user$project$Main$main
            {
                var checker = flagChecker(flagDecoder, moduleName);
                if (typeof debugMetadata === 'undefined')
                {
                    normalSetup(impl, object, moduleName, checker);
                }
                else
                {
                    debugSetup(A2(debugWrap, debugMetadata, impl), object, moduleName, checker);
                }
            };
        };
    });
}

_user$project$Main$mainobject, moduleName, debugMetadata を引数に取る無名関数みたいです。flagCheckercheckNoFlagscheckYesFlags が用意されています。

F2 は関数を受け取ってカリー化して返します。逆に A2 はカリー化された関数に引数を渡して評価する関数です。 debugWrap_elm_lang$virtual_dom$VirtualDom_Debug$wrap です。

implHtml.program に渡すおなじみのレコードです。見てわかるように Elm['Main']debugMetadata の有無で呼び出す関数が異なります。

今回は undefined でした。Html.program はこの関数の実行結果で実装が定義されます。

normalSetup

function normalSetup(impl, object, moduleName, flagChecker)
{
    object['embed'] = function embed(node, flags)
    {
        while (node.lastChild)
        {
            node.removeChild(node.lastChild);
        }

        return _elm_lang$core$Native_Platform.initialize(
            flagChecker(impl.init, flags, node),
            impl.update,
            impl.subscriptions,
            normalRenderer(node, impl.view)
        );
    };

    object['fullscreen'] = function fullscreen(flags)
    {
        return _elm_lang$core$Native_Platform.initialize(
            flagChecker(impl.init, flags, document.body),
            impl.update,
            impl.subscriptions,
            normalRenderer(document.body, impl.view)
        );
    };
}

ここで Elm['Main'] に変更が加えられます。これ以上、 Elm['Main'] に変更は加えてなさそうです。

initialize

function initialize(init, update, subscriptions, renderer)
{
    // ambient state
    var managers = {};
    var updateView;

    // init and update state in main process
    var initApp = _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) {
        var model = init._0;
        updateView = renderer(enqueue, model);
        var cmds = init._1;
        var subs = subscriptions(model);
        dispatchEffects(managers, cmds, subs);
        callback(_elm_lang$core$Native_Scheduler.succeed(model));
    });

    function onMessage(msg, model)
    {
        return _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) {
            var results = A2(update, msg, model);
            model = results._0;
            updateView(model);
            var cmds = results._1;
            var subs = subscriptions(model);
            dispatchEffects(managers, cmds, subs);
            callback(_elm_lang$core$Native_Scheduler.succeed(model));
        });
    }

    var mainProcess = spawnLoop(initApp, onMessage);

    function enqueue(msg)
    {
        _elm_lang$core$Native_Scheduler.rawSend(mainProcess, msg);
    }

    var ports = setupEffects(managers, enqueue);

    return ports ? { ports: ports } : {};
}

続けて fullscreen 呼び出しによる起動に至るまでの処理を眺めていきます。この関数はかなり重要そうです。

_elm_lang$core$Native_Scheduler.nativeBinding に渡されている関数はどちらも同じような内容です。おそらく view を更新して effectManagercmdssubs を渡して変更を通知しています。

_elm_lang$core$Native_Scheduler.nativeBinding を始めとする _elm_lang$core$Native_Scheduler モジュールの関数はElmアプリケーションのイベントループに大いに関係しています。

core/Scheduler.js at 5.1.1 · elm-lang/core · GitHub

nativeBinding が返すオブジェクトは後にenqueueされて処理されます。initApp は初期化に必要な処理、onMessage はメッセージを受け取った時に呼び出す処理だと推測できます。

setupEffectsdispatchEffects のための準備ではないでしょうか。もしかするとEffectManager関係の処理かもしれないです。ここで返しているオブジェクトは app に代入されますが利用してないですね。

spawnLoop

function spawnLoop(init, onMessage)
{
    var andThen = _elm_lang$core$Native_Scheduler.andThen;

    function loop(state)
    {
        var handleMsg = _elm_lang$core$Native_Scheduler.receive(function(msg) {
            return onMessage(msg, state);
        });
        return A2(andThen, loop, handleMsg);
    }

    var task = A2(andThen, loop, init);

    return _elm_lang$core$Native_Scheduler.rawSpawn(task);
}

一見するとこんがらがりそうなんですが、return 文に注目すると taskenqueue しているのがわかります。

core/Scheduler.js at 5.1.1 · elm-lang/core · GitHub

この task_elm_lang$core$Native_Scheduler.nativeBinding の仲間です。これは _elm_lang$core$Native_Scheduler.andThen で処理がラップされています。

core/Scheduler.js at 5.1.1 · elm-lang/core · GitHub

コールバック関数の loophandleMsg という task を設定して再度 andThen でラップしています。_elm_lang$core$Native_Scheduler.receive_elm_lang$core$Native_Scheduler.nativeBinding の仲間です。

core/Scheduler.js at 5.1.1 · elm-lang/core · GitHub

rawSpawn

core/Scheduler.js at 5.1.1 · elm-lang/core · GitHub

processtask をラップしています。 processenqueue します。

work

キューに溜まった process を処理します。取り出して process.root があれば step 関数に渡します。process がなくなると活動を停止します。

core/Scheduler.js at 5.1.1 · elm-lang/core · GitHub

step

enqueue された processstep 関数で処理されます

core/Scheduler.js at 5.1.1 · elm-lang/core · GitHub

enqueue された task をおさらいします。

process(andThen(loop, nativeBinding(task)))

順に処理していきますが、 loop が実行される気配がありません。 loopprocess.stack.callback に積まれたままです。

ちょっと謎ですね。 loop はいつ呼ばれるのでしょうか。

nativeBinding(task) の処理では登録していた関数を呼び出しています。ここで呼び出しているのは初期化処理をする関数です。

 var initApp = _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) {
        var model = init._0;
        updateView = renderer(enqueue, model);
        var cmds = init._1;
        var subs = subscriptions(model);
        dispatchEffects(managers, cmds, subs);
        callback(_elm_lang$core$Native_Scheduler.succeed(model));
    });

renderernormalRenderer(document.body, impl.view) の実行結果です。

function normalRenderer(parentNode, view)
{
    return function(tagger, initialModel)
    {
        var eventNode = { tagger: tagger, parent: undefined };
        var initialVirtualNode = view(initialModel);
        var domNode = render(initialVirtualNode, eventNode);
        parentNode.appendChild(domNode);
        return makeStepper(domNode, view, initialVirtualNode, eventNode);
    };
}

normalRenderer では初期DOMの生成を行い、stepper 関数の作成を行います。

function makeStepper(domNode, view, initialVirtualNode, eventNode)
{
    var state = 'NO_REQUEST';
    var currNode = initialVirtualNode;
    var nextModel;

    function updateIfNeeded()
    {
        switch (state)
        {
            case 'NO_REQUEST':
                throw new Error(
                    'Unexpected draw callback.\n' +
                    'Please report this to <https://github.com/elm-lang/virtual-dom/issues>.'
                );

            case 'PENDING_REQUEST':
                rAF(updateIfNeeded);
                state = 'EXTRA_REQUEST';

                var nextNode = view(nextModel);
                var patches = diff(currNode, nextNode);
                domNode = applyPatches(domNode, currNode, patches, eventNode);
                currNode = nextNode;

                return;

            case 'EXTRA_REQUEST':
                state = 'NO_REQUEST';
                return;
        }
    }

    return function stepper(model)
    {
        if (state === 'NO_REQUEST')
        {
            rAF(updateIfNeeded);
        }
        state = 'PENDING_REQUEST';
        nextModel = model;
    };
}

makeStepper は 1フレームごとに実行される関数の登録を行う関数 stepper の作成を行います。virtualDOMの振る舞いを記述しているのもここになります。DOM差分を計算して適用している様子がわかります。

applyPatches では差分適用のほかイベントに対する関数の登録も行っています。renderer によって作成された関数 updateViewonMessage 関数の中で利用されています。

dispatchEffects では特に何もしないため最後にコールバック関数を呼び出します。

modelsucceed でラップしてものをコールバック関数は受け取り新たな process.root としています。同時に processenqueue しています。

succeedstep 関数で処理されます。process.stack.callback に登録した loop 関数がようやく実行されます。

が、特に何事もなく process は処理されて初期化処理は終了します。

まとめ

Elmアプリケーションの初期化処理でした。今回の重要登場人物は次のとおりです。

  • makeProgram ... デバッグの有無を判定
  • normalSetup ... fullscreen, embed を定義
  • initialize ... 初期化タスク定義と初期化実行
  • spawnLoop ... 初期化タスクの合成
  • rawSpawn ... processでラップ、キューにタスクを登録
  • work ... キューに溜まった process を処理
  • step ... タスク処理
  • makeStepper ... DOMの差分処理

流れもざっくり振り返りましょう。

Elmオブジェクト準備

  1. オブジェクトを作る
  2. normalSetup を呼んで fullscreen を定義する

初期化する

  1. initialize を呼ぶ
  2. spawnLoop で初期化タスクを作成
  3. rawSpawn を呼び、キューに追加
  4. work を呼び初期化タスクを順番に実行
  5. step を呼びタスクを処理、process.root を設定
  6. initApp に登録された初期化処理を実行
  7. 初期viewの追加