debuggerの| Step over | Step into | Step out |の使い分け

デバッグ技法というのはプログラミングを学ぶ上でとても重要になってくるスキルの1つだと思ってる.
どの処理系でもデバッグ技法の概念は基本的に同じだし使い方も似ているところが多い.
ということで今回はChrome Developer Toolsでのデバッグ技法をまとめる.

次のJavaScriptプログラムをデバッグ対象としてStep実行の処理順序を見ていこう.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title></title>
</head>
<body>
  <h1>Debug技法</h1>
</body>
  <script>
    function third() {
      return 'done';
    }   
    function second() {
      return third();
    }   
    function first() {
      const result = second()
      debugger;
    }
    first();
  </script>
</html>

3つの関数が定義されており、処理順序は first() -> second() -> third() となる.
それぞれreturnキーワードにより戻り値を返し、呼び出し元に値を返している.

さて、デバッガにはStep, Step over, Step out, Step into という4つのステップ実行機能が備わっている.
次のようにChrome Developer Toolsを開くと見ることができる.

f:id:narinymous:20180807012748p:plain

どうやらStepはStep intoは同じ機能のようなのでStep以外の3つについて調べる.
それではdebug開始.

それぞれのCall Stackの違い

  • Step over
    anonymous() -> anonymous()

  • Step into
    anonymous() -> anonymous() -> first() -> second() -> third() -> inside third() -> second() -> first() -> anonymous()

  • Step out
    anonymous()

それぞれにanonymous()とあるが、これはグローバルスコープ内での実行はすべて無名関数からの呼び出しとなるということ.
それぞれ debugger が記述されたポイントでデバッガが止まり、次の行のfirst()が記述されたポイントで止まるので1度はanonymous()が登場する.

デバッガは関数が格納されたメモリアドレスを格納している.
デバッガ内にCall Stackというパネルが存在するはずだが、ここにその記録が保持されている.
Step実行により関数内に入った場合、処理を巻き戻すといった操作はできないが、特定の関数を再度見にいくことは可能だ.

Step into機能により、third関数までStep実行した際のCall Stackは次のようになっている.
f:id:narinymous:20180807015712p:plain

「処理を巻き戻す操作はできない」とは言ったが、処理を通過した関数群の中の任意の関数から再度実行し直すことは可能となる.
その使い方は Call Stackの中にある関数群の中で右クリックを押すと出てくる Restart frame という機能だ.
Restart frameは次のようにスタック上に積まれた関数の任意の時点までスタックを破棄して、その関数から実行し直してくれる機能でありかなり強力なのだ.
例えば次のようにStep into実行しスタック上に処理が積まれているとしよう.

anonymous() -> anonymous() -> first() -> second() -> third()

この時点で次の画像のようにRestat frameを使う.

f:id:narinymous:20180807020614p:plain

Restart frameを押したらfirst関数以降に積まれていたスタックは破棄され、あたかも次のようにdebuggerが起動してからfirst関数まできたばかりかのように再度実行し直すことができるわけだ.

anonymous() -> anonymous() -> first()

まとめ

もっともっと機能はたくさんあるが、今回はここまで.
そういえば最近、nodeのデバッガをGoogleのエンジニアさんが作ってくれていてndbというChromiumGUIを使ったデバッガまで登場していて本当にありがたい限りだ.
nodeのデバッガはnodeコマンドに搭載されていてオプションさえ知っていれば使えるわけだが、Chrome Developer Toolsのようにビジュアライズされたデバッガというのはかなり使いやすい (個人的には).
gccコンパイラllvmコンパイラなどの処理系のデバッガ(gdb, lldb)なんかはCLI実装なわけで、操作方法もnodeのデバッガとほぼ似ている.
でも実際のところXCodeなどの統合開発環境の内部ではlldbデバッガが使用されていて裏側では動いているということだけは念頭に置いておきたい.

Vim scriptのautoloadについて

それなりに規模の大きいVimプラグインを見ているとautoloadディレクトリが見つかることが多いと思う。
今回はautoloadの挙動を軽くまとめます。

仕様確認

とりあえずhelpで仕様を調べる。

help autoload

autoload scriptの仕様ではautoloadディレクトリ内にVim scriptファイルを配置すると動的に読み込みに行ってくれる。

autoload scriptには関数や変数の命名規則があって、次のように関数や変数の前に名前階層をつけて定義してあげる必要がある。

" ~/.vim/autoload/dir1/dir2/hello.vim の場合 
function dir1#dir2#hello()
  " 処理
endfunction

let dir1#dir2#hello_variable = ""

autoloadの挙動

では試しに ~/.vim/autoload/hello.vim というファイルを作成して、次の関数を定義する。

function hello#world()
  echo "Hello autoload function!"
endfunction

これでautoloadの仕様で関数の実行時に動的にファイルを読みに行ってくれる。
いったんvimを閉じて再度vimを立ち上げるて関数を呼び出してみると確認できる。

:call hello#world()

Hello autoload function!

これで一応autoload scriptが読み込まれているのが確認できた。
が、動的に読み込まれていることも確認してみたいと思う。

" この行を追加
echo 'load autoload script hello.vim'

function hello#world()
  echo "Hello autoload function!"
endfunction

そして再度vimを開き直す。

f:id:narinymous:20180520215458p:plain

vimを開き直した時点ではechoによるメッセージが出ていないのでまだファイル自体が読み込まれていないことがわかる。
関数を実行した時点で次のようにechoメッセージが出力される。

:call hello#world()

load autoload script hello.vim
Hello autoload function

f:id:narinymous:20180520215507p:plain

関数の実行時にファイルが読み込まれたことで1つ目のechoメッセージが、次に関数の実行によるechoメッセージが確認できました。
プラグイン管理ツールを導入している場合は各プラグインのautoloadディレクトリも対象に含まれるようになる。
なので、プラグインの実装では単にプロジェクトのルート階層にautoloadディレクトリを配置しておけばいい。

gRPCのデバッグにはevansを使うと便利

gRPCクライアントevans
gRPC client: evans

  

先日会社のエンジニアブログでgRPCに関する以下の記事を書いていた。

blog.engineer.adways.net

blog.engineer.adways.net

gRPCを使うメリットや実装方法に関することを書きまとめ、ある程度の知識も得られたと思っている。
ある程度gRPCに関する実装に慣れてきたところで、gRPCもHTTPのようにcurlデバッグできないのだろうか?と思っていたところevansというOSSを発見した。
今回はgRPCのデバッグツールevansに関することを軽くまとめる。

会社のブログに載せた個人のGithubリポジトリをそのまま流用してデバッグ手法を試みる。

github.com

このリポジトリではKotlinでSpring bootを使用し、gRPCサーバをポート6565で公開するように実装している。
言語やフレームワークは特にデバッグ時には関係ないので前提として知っておくのは起動しているポート番号が6565であるということだけでいい。

デバッグに必要なのはgRPCのプロトコル定義ファイルのみ。
さっき載せたリポジトリ内のプロトコル定義ファイルの内容は以下となる。

syntax = "proto3";

package taskstore;

option java_multiple_files = true;
option java_outer_classname = "TaskStore";
option java_package = "com.example.demo.taskstore";

service Taskstore {
  rpc GetTasks(GetTasksRequest) returns (GetTasksResponse) {}
  rpc AddTask(AddTaskRequest) returns (AddTaskResponse) {}
  rpc UpdateTask(UpdateTaskRequest) returns (UpdateTaskResponse) {}
  rpc DeleteTask(DeleteTaskRequest) returns (DeleteTaskResponse) {}
}

message GetTasksRequest {
}
message GetTasksResponse {
  repeated Task task = 1;
}

message AddTaskRequest {
  string content = 1;
}
message AddTaskResponse {
  Task task = 1;
}

message UpdateTaskRequest {
  int64 id = 1;
  bool done = 2;
}
message UpdateTaskResponse {
  Task task = 1;
}

message DeleteTaskRequest {
  int64 id = 1;
}
message DeleteTaskResponse {
  bool success = 1;
}

message Task {
  int64 id = 1;
  string content = 2;
  bool done = 3;
}

evansのインストール

以下のリポジトリがevansというOSS
curlのようにサーバにリクエストを送ることもできるし、プロトコル定義をテーブル形式で表示したりと表現力豊かなgRPCクライアント。

github.com

$ brew tap ktr0731/evans
$ brew install evans

evansでgRPCのデバッグ

以降はgRPCサーバが立ち上がっていることを前提として書く。
念のため再度言及するが、今回の僕のリポジトリではSpring bootでポート6565でgRPCサーバが起動している。

evansのhelp表示が以下。

$ evans --help

evans 0.2.8
Usage: evans [--interactive] [--editconfig] [--host HOST] [--port PORT] [--package PACKAGE] [--service SERVICE] [--call CALL] [--file FILE] [--path PATH] [--header HEADER] [PROTO [PROTO ...]]

Positional arguments:
  PROTO                  .proto files

Options:
  --interactive, -i      use interactive mode
  --editconfig, -e       edit config file by $EDITOR
  --host HOST, -h HOST   gRPC host
  --port PORT, -p PORT   gRPC port [default: 50051]
  --package PACKAGE      default package
  --service SERVICE      default service. evans parse package from this if --package is nothing.
  --call CALL, -c CALL   call specified RPC
  --file FILE, -f FILE   the script file which will be executed (used only command-line mode)
  --path PATH            proto file path
  --header HEADER        headers set to each requests
  --help, -h             display this help and exit
  --version              display version and exit

--portでターゲットとなるgRPCサーバのポートを指定し、プロトコル定義ファイルの場所を教えてあげる。

$ evans --port 6565 proto/task.proto

  ______
 |  ____|
 | |__    __   __   __ _   _ __    ___
 |  __|   \ \ / /  / _. | | '_ \  / __|
 | |____   \ V /  | (_| | | | | | \__ \
 |______|   \_/    \__,_| |_| |_| |___/

 more expressive universal gRPC client


127.0.0.1:6565> 

あまりコマンドも多くなくシンプルなのため非常に使いやすい。
コマンドの一覧表を提示しておく。

コマンド名 意味
show package packageの一覧表示
show service 選択したpackageに存在するserviceを一覧表示
show message 選択したpackageに存在するmessageを一覧表示
show rpc 選択したservice内に存在するrpcメソッドを一覧表示
desc message名 引数に与えたmessageのパラメータ名と型を表示
package package名 引数に与えたpackageを選択する
service service名 引数に与えたserviceを選択する
call rpc名 引数に与えたrpcメソッドをコールする

一応解説する。
packageを選択しないと、serviceやmessageは一覧表示できない。
serviceを選択しないと、rpcメソッドの一覧は表示できないしcallもできない。
これは当たり前だがpackageは複数存在し得るし、ひとつのpackage内に複数のserviceが存在し得るから当然のこと。
なお、callコマンドは対話型でリクエストの引数を与えることができる。
早速試してみる。

127.0.0.1:6565> show package
+-----------+
|  PACKAGE  |
+-----------+
| taskstore |
+-----------+

127.0.0.1:6565> package taskstore

taskstore@127.0.0.1:6565> show service
+-----------+------------+-------------------+--------------------+
|  SERVICE  |    RPC     |    REQUESTTYPE    |    RESPONSETYPE    |
+-----------+------------+-------------------+--------------------+
| Taskstore | GetTasks   | GetTasksRequest   | GetTasksResponse   |
|           | AddTask    | AddTaskRequest    | AddTaskResponse    |
|           | UpdateTask | UpdateTaskRequest | UpdateTaskResponse |
|           | DeleteTask | DeleteTaskRequest | DeleteTaskResponse |
+-----------+------------+-------------------+--------------------+

taskstore@127.0.0.1:6565> show message
+--------------------+
|      MESSAGE       |
+--------------------+
| GetTasksRequest    |
| GetTasksResponse   |
| AddTaskRequest     |
| AddTaskResponse    |
| UpdateTaskRequest  |
| UpdateTaskResponse |
| DeleteTaskRequest  |
| DeleteTaskResponse |
| Task               |
+--------------------+

taskstore@127.0.0.1:6565> service Taskstore

taskstore.Taskstore@127.0.0.1:6565> show rpc
+------------+-------------------+--------------------+
|    RPC     |    REQUESTTYPE    |    RESPONSETYPE    |
+------------+-------------------+--------------------+
| GetTasks   | GetTasksRequest   | GetTasksResponse   |
| AddTask    | AddTaskRequest    | AddTaskResponse    |
| UpdateTask | UpdateTaskRequest | UpdateTaskResponse |
| DeleteTask | DeleteTaskRequest | DeleteTaskResponse |
+------------+-------------------+--------------------+

taskstore.Taskstore@127.0.0.1:6565> call GetTasks
{
  "task": [
    {
      "id": 65,
      "content": "Hello"
    }
  ]
}

taskstore.Taskstore@127.0.0.1:6565> 

最後のcallコマンドでGetTasksというrpcメソッドをコールしたらしっかりjson形式ではあるが、レスポンスが確認できた。

evansはコマンドライン一発で完結できるコマンドラインモードと今回説明したインタラクティブモードというものがある。
コマンドラインモードではgRPCサーバに対して、とあるスクリプトを定期実行したい時などに使えそうだ。
インタラクティブモードはやはりgRPC APIを手動で動作確認したい場合に頻繁に使えそう。

LWJGLの環境構築

LWJGLはJVM系言語で使用できる軽量OpenGLライブラリ。
ちょっと触ってみようと思ったので、環境構築手順を記述していく。

環境構築

GradleやMavenでプロジェクト管理をするならすぐ構築できる。
以下のサイトから必要なものを選んでダウンロードする。
LWJGL - Lightweight Java Game Library

今回はGradleで構築する。
Mode の項目でGradleを選択し、他は特にいじらずDownload build.gradleというボタンを押したらbuild.gradleファイルがダウンロードされる。
このbuild.gradleはそのまま使わないので、先にGradleプロジェクトを生成する。

$ mkdir lwjgl-example
$ cd lwjgl-example
$ gradle init --type java-application
$ ls -l
total 40
-rw-r--r--  1 naritatakuya  staff   993 Apr  4 01:38 build.gradle
drwxr-xr-x  3 naritatakuya  staff    96 Apr  4 01:38 gradle
-rwxr-xr-x  1 naritatakuya  staff  5296 Apr  4 01:38 gradlew
-rw-r--r--  1 naritatakuya  staff  2260 Apr  4 01:38 gradlew.bat
-rw-r--r--  1 naritatakuya  staff   577 Apr  4 01:38 settings.gradle
drwxr-xr-x  4 naritatakuya  staff   128 Apr  4 01:38 src

生成されたら先ほどダウンロードしたbuild.gradleの内容を生成したプロジェクトのbuild.gradleに移植する。
移植後は以下のような感じになるはず。

apply plugin: 'java'
apply plugin: 'application'

repositories {
    mavenCentral()
  jcenter()
}

switch ( OperatingSystem.current() ) {
    case OperatingSystem.WINDOWS:
        project.ext.lwjglNatives = "natives-windows"
        break
    case OperatingSystem.LINUX:
        project.ext.lwjglNatives = "natives-linux"
    break
    case OperatingSystem.MAC_OS:
        project.ext.lwjglNatives = "natives-macos"
        break
}


mainClassName = 'App'

/* LWJGL settings */
applicationDefaultJvmArgs = ["-XstartOnFirstThread"]

import org.gradle.internal.os.OperatingSystem

project.ext.lwjglVersion = "3.1.6"
project.ext.jomlVersion = "1.9.9"

dependencies {
  compile 'com.google.guava:guava:21.0'
  testCompile 'junit:junit:4.12'

  /* LWJGL */
    compile "org.lwjgl:lwjgl:$lwjglVersion"
    compile "org.lwjgl:lwjgl-assimp:$lwjglVersion"
    compile "org.lwjgl:lwjgl-bgfx:$lwjglVersion"
    compile "org.lwjgl:lwjgl-egl:$lwjglVersion"
    compile "org.lwjgl:lwjgl-glfw:$lwjglVersion"
    compile "org.lwjgl:lwjgl-jawt:$lwjglVersion"
    compile "org.lwjgl:lwjgl-jemalloc:$lwjglVersion"
    compile "org.lwjgl:lwjgl-lmdb:$lwjglVersion"
    compile "org.lwjgl:lwjgl-lz4:$lwjglVersion"
    compile "org.lwjgl:lwjgl-nanovg:$lwjglVersion"
    compile "org.lwjgl:lwjgl-nfd:$lwjglVersion"
    compile "org.lwjgl:lwjgl-nuklear:$lwjglVersion"
    compile "org.lwjgl:lwjgl-odbc:$lwjglVersion"
    compile "org.lwjgl:lwjgl-openal:$lwjglVersion"
    compile "org.lwjgl:lwjgl-opencl:$lwjglVersion"
    compile "org.lwjgl:lwjgl-opengl:$lwjglVersion"
    compile "org.lwjgl:lwjgl-opengles:$lwjglVersion"
    compile "org.lwjgl:lwjgl-openvr:$lwjglVersion"
    compile "org.lwjgl:lwjgl-par:$lwjglVersion"
    compile "org.lwjgl:lwjgl-remotery:$lwjglVersion"
    compile "org.lwjgl:lwjgl-rpmalloc:$lwjglVersion"
    compile "org.lwjgl:lwjgl-sse:$lwjglVersion"
    compile "org.lwjgl:lwjgl-stb:$lwjglVersion"
    compile "org.lwjgl:lwjgl-tinyexr:$lwjglVersion"
    compile "org.lwjgl:lwjgl-tinyfd:$lwjglVersion"
    compile "org.lwjgl:lwjgl-tootle:$lwjglVersion"
    compile "org.lwjgl:lwjgl-vulkan:$lwjglVersion"
    compile "org.lwjgl:lwjgl-xxhash:$lwjglVersion"
    compile "org.lwjgl:lwjgl-yoga:$lwjglVersion"
    compile "org.lwjgl:lwjgl-zstd:$lwjglVersion"
    compile "org.lwjgl:lwjgl:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-assimp:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-bgfx:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-glfw:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-jemalloc:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-lmdb:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-lz4:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-nanovg:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-nfd:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-nuklear:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-openal:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-opengl:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-opengles:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-openvr:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-par:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-remotery:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-rpmalloc:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-sse:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-stb:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-tinyexr:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-tinyfd:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-tootle:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-xxhash:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-yoga:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-zstd:$lwjglVersion:$lwjglNatives"
    compile "org.joml:joml:${jomlVersion}"
}

ウィンドウを表示してみる

以下の公式ページのGet Startedのサンプルコードを貼り付けて動かしてみる。
LWJGL - Lightweight Java Game Library

以下のコードをsrc/main/java/App.javaに貼り付ける。

import org.lwjgl.*;
import org.lwjgl.glfw.*;
import org.lwjgl.opengl.*;
import org.lwjgl.system.*;

import java.nio.*;

import static org.lwjgl.glfw.Callbacks.*;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.system.MemoryStack.*;
import static org.lwjgl.system.MemoryUtil.*;

public class HelloWorld {

    // The window handle
    private long window;

    public void run() {
        System.out.println("Hello LWJGL " + Version.getVersion() + "!");

        init();
        loop();

        // Free the window callbacks and destroy the window
        glfwFreeCallbacks(window);
        glfwDestroyWindow(window);

        // Terminate GLFW and free the error callback
        glfwTerminate();
        glfwSetErrorCallback(null).free();
    }

    private void init() {
        // Setup an error callback. The default implementation
        // will print the error message in System.err.
        GLFWErrorCallback.createPrint(System.err).set();

        // Initialize GLFW. Most GLFW functions will not work before doing this.
        if ( !glfwInit() )
            throw new IllegalStateException("Unable to initialize GLFW");

        // Configure GLFW
        glfwDefaultWindowHints(); // optional, the current window hints are already the default
        glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); // the window will stay hidden after creation
        glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); // the window will be resizable

        // Create the window
        window = glfwCreateWindow(300, 300, "Hello World!", NULL, NULL);
        if ( window == NULL )
            throw new RuntimeException("Failed to create the GLFW window");

        // Setup a key callback. It will be called every time a key is pressed, repeated or released.
        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if ( key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE )
                glfwSetWindowShouldClose(window, true); // We will detect this in the rendering loop
        });

        // Get the thread stack and push a new frame
        try ( MemoryStack stack = stackPush() ) {
            IntBuffer pWidth = stack.mallocInt(1); // int*
            IntBuffer pHeight = stack.mallocInt(1); // int*

            // Get the window size passed to glfwCreateWindow
            glfwGetWindowSize(window, pWidth, pHeight);

            // Get the resolution of the primary monitor
            GLFWVidMode vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor());

            // Center the window
            glfwSetWindowPos(
                window,
                (vidmode.width() - pWidth.get(0)) / 2,
                (vidmode.height() - pHeight.get(0)) / 2
            );
        } // the stack frame is popped automatically

        // Make the OpenGL context current
        glfwMakeContextCurrent(window);
        // Enable v-sync
        glfwSwapInterval(1);

        // Make the window visible
        glfwShowWindow(window);
    }

    private void loop() {
        // This line is critical for LWJGL's interoperation with GLFW's
        // OpenGL context, or any context that is managed externally.
        // LWJGL detects the context that is current in the current thread,
        // creates the GLCapabilities instance and makes the OpenGL
        // bindings available for use.
        GL.createCapabilities();

        // Set the clear color
        glClearColor(1.0f, 0.0f, 0.0f, 0.0f);

        // Run the rendering loop until the user has attempted to close
        // the window or has pressed the ESCAPE key.
        while ( !glfwWindowShouldClose(window) ) {
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // clear the framebuffer

            glfwSwapBuffers(window); // swap the color buffers

            // Poll for window events. The key callback above will only be
            // invoked during this call.
            glfwPollEvents();
        }
    }

    public static void main(String[] args) {
        new HelloWorld().run();
    }

}

最後にビルドして起動すると以下のような真っ赤なウィンドウが表示される。

$ ./gradlew build run

f:id:narinymous:20180404014703p:plain

エラーが起きる場合

Gradleのrunタスクでアプリケーションを実行する際に以下のような例外が吐かれて躓いたので記しておく。

> Task :run
Exception in thread "main" java.lang.ExceptionInInitializerError
        at org.lwjgl.glfw.GLFW.glfwCreateWindow(GLFW.java:1823)
        at App.init(App.java:38)
        at App.run(App.java:18)
        at App.main(App.java:81)
Caused by: java.lang.IllegalStateException: GLFW windows may only be created on the main thread and that thread must be the first thread in the process. Please run the JVM with -XstartOnFirstThread. For offscreen rendering, make sure another window toolkit (e.g. AWT or JavaFX) is initialized before GLFW.
        at org.lwjgl.glfw.EventLoop$OffScreen.<clinit>(EventLoop.java:39)
        ... 4 more

これはbuild.gradleに書いている以下の行を書くことで回避できる

applicationDefaultJvmArgs = ["-XstartOnFirstThread"]

原因はおそらくgradleのapplicationプラグインを使っていることだと思うが、実装したAppクラスがメインスレッドとして実行されていないのでJVM引数に渡してあげる必要があるようだ。

原因がわかる方は是非教えていただきたい。

TypeScript2.8のConditional typesについて

先日(2018-03-27)TypeScript 2.8が正式リリースされました。
今回のリリースで搭載されたConditional typesという条件付きの型宣言構文についてまとめる。

Conditional typesについて

A extends B ? C : D

構文は上記の通りで、型Aが型Bに割当て可能ならC、それ以外はDになる。
簡単な使用例が以下。

interface Animal {
    live(): void;
}
interface Dog extends Animal {
    woof(): void;
}

// Has type 'number'
type Foo = Dog extends Animal ? number : string;

// Has type 'string'
type Bar = RegExp extends Dog ? number : string;

この簡単な例ではConditional typesによりFooはnumberになり、Barはstringになる。
しかしConditional typesなんて使わずに直にnumber, stringで型宣言してしまっても特に問題ない場面の方が多い気がする。
どんな時に使用するのかいまいち思いつかないが、ドキュメントにはジェネリクスと共に使用するとメリットがわかるとのこと。
ジェネリクスと共に使用したのが以下。

interface Id { id: number, /* other fields */ }
interface Name { name: string, /* other fields */ }

declare function createLabel(id: number): Id;
declare function createLabel(name: string): Name;
declare function createLabel(name: string | number): Id | Name;


type IdOrName<T extends number | string> =
    T extends number ? Id : Name;

declare function createLabel<T extends number | string>(idOrName: T):
    T extends number ? Id : Name;

let a = createLabel("typescript");   // Name
let b = createLabel(2.8);            // Id
let c = createLabel("" as any);      // Id | Name
let d = createLabel("" as never);    // never

createLabel関数は入力の型に基づいて、一つのcreateLabel関数を選択する。
これによりそれぞれ再代入時に許容される型はaはName型、bはId型、cはId型とName型両方、dはnever型となる。

他にも以下のような使い方があるようだ。

type Common<T, U> = T extends U ? never : T;  // Remove types from T that are assignable to U
type Diff<T, U> = Pick<T, Common<keyof T, keyof U>>;

interface UserA {
  name: string,
  age: number,
  email: string,
  phone: string,
}
interface UserB {
  name: string,
  age: number,
}

type CommonProperties = Common<UserB, UserA>; // inferred as { one: string; two: number; }
type DiffProperties = Diff<UserA, UserB>; // inferred as { one: string; two: number; }

const commonUser1: CommonProperties = {
  name: "Hoge",
  age: 23,
}

const commonUser2: CommonProperties = {
  name: "Hoge",
  age: 23,
  email: "example@example.com",
  phone: "xxx-xxxx-xxxx"
} 
//=> error TS2322: Type '{ name: string; age: number; email: string; phone: string; }' is not assignable to type 'UserB'.
//                 Object literal may only specify known properties, and 'email' does not exist in type 'UserB'.

const exeptUser1: DiffProperties = {
  email: "example@example.com",
  phone: "xxx-xxxx-xxxx"
}

const exeptUser2: DiffProperties= {
  name: "Hoge",
  age: 23,
  email: "example@example.com",
  phone: "xxx-xxxx-xxxx"
}
//=> error TS2322: Type '{ name: string; age: number; email: string; phone: string; }' is not assignable to type 'Pick<UserA, "email" | "phone">'.
//                 Object literal may only specify known properties, and 'name' does not exist in type 'Pick<UserA, "email" | "phone">'.

2つのObjectの部分集合を型としたい時や、その逆(部分集合とはならない部分)を型にしたい時とか。
このあたりは使う機会もありそう。

Vue.js & Typescript構成でのPWA環境構築

直近仕事でPWAを実装する機会がありそうなので, 少しづつ習得していこうと思います。

プロジェクトの初期化

PWAのセットアップにはvue-cliを使います。

$ yarn global add vue-cli

PWAの初期化。

$ vue init pwa progressive-web-app
$ cd ./progressive-web-app
$ yarn install
$ yarn dev

TypeScriptの導入

まずTypeScript本体とWebpackのローダをpackcageに追加します。

yarn add typescript ts-loader

次にVue.jsでTypeScriptを使用するためにvue-property-decoratorというものを入れます。

yarn add vue-property-decorator

packageはこれで以上です。

Webpackの書き換え

vue-cliプロジェクトでは build/webpack.base.conf.js にWebpackの共通設定が記載してあるので、そのファイルを一部修正する。

  module: {
    rules: [
      // .vueに関するルールは一部書き換え
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          esModule: true
        }   
      },  
      // .tsのルールは新しく追加
      {   
        test: /\.ts$/,
        loader: 'ts-loader',
        options:
        { appendTsSuffixTo: [/\.vue$/] }
      },

    // ~~~~~

Vue & TypeScriptで実装する

設定は以上なので、最後はjsファイルをtsで実装する。

src/
├── App.vue
├── assets
│   └── logo.png
├── components
│   └── Hello.vue
├── main.ts
└── router
    └── index.ts

ts-loderのError

Webpackのビルドでts-loaderが吐いているエラーに悩まされた。

Cannot read property 'afterCompile' of undefined

ググってみたところ以下のissueが上がっていたので参考にしてみたらエラーが解消されました。
https://github.com/angular/angular-cli/issues/9783

webpack, ts-loaderのバージョンを指定してインストールすると解消されるので、参考までに。

  • webpack: 3.1.0
  • ts-loader: 3.5.0

TypescriptとFlowどちらを採用するべきか

僕は普段フロントエンドフレームワークと共にTypescriptを使って開発をしています。

ReactやVue.jsの内部実装にはFlowが採用されていて、AngularやRxJSはTypescriptを採用しています。
今後開発を進めていく上で両者はどのような開発現場でメリットが最大化されるのか調査していみたいと思います。
TypescriptをベースにFlowにはどんな機能が盛り込まれているのか、または不足しているのかを書いていきます。

変数の型宣言後の再代入

Typescriptではvarletで宣言した変数を適当な値で初期化すると、型宣言がされていなくても型推論により以降の再代入は初期化時の値の型に束縛されます。
Flowでは、初期化時に型宣言をしていない場合は以降の再代入はどんな型でも代入可能です。

Typescript

let age = 21;
age = '22' // => Error

let name: string = 'Anonymous';
name = 21  // => Error

Flow

// @flow
let age = 21;
age = '22' // => OK

let name: string = 'Anonymous';
name = 21  // => Error

null型 / undefined型の扱い

null, undefinedはTypescriptではどんな型で束縛された変数にでも代入可能です。
Flowは厳密に精査してくれるので、例えばstringで宣言した変数にはnullundefinedも代入できません。
逆も同様でnull宣言の変数に対してstringの値は許容しません。

Typescript

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}
new Person(null);      // => OK
new Person(undefined); // => OK

Flow

// @flow
class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}
new Person(null);      // => Error: Cannot call Person with null bound to name because null is incompatible with string.
new Person(undefined); // => Error: Cannot call Person with undefined bound to name because undefined is incompatible with string.

Typescriptはnullを許容し、Flowではnullを許容しないということです。
しかし、Typescript2.0からはtsconfig.jsonコンパイラオプションを与えるとnullを許容しない設定も可能です。
ここで紹介したのはtsc(Typescript compiler)にオプションを与えていない場合になります。
Flowでもnullを許容して(nullable)宣言をしたい場合?キーワードを付けてあげればいいです。

// @flow
function nullableFunction(value: ?number) {
  if (value != null) {
    /* ...... */
  } else {
    /* ...... */
  }
}

nullableFunction(42);         // => OK
nullableFunction();           // => OK
nullableFunction(undefined);  // => OK
nullableFunction(null);       // => OK 
nullableFunction("42");       // => Error

引数として受け取る変数がnull許容(nullable)の場合は、多くの場合上記のように関数ブロック内でnullチェックすることになると思います。

型推論について

上述した通りTypescriptでは変数宣言時に初期化する場合、初期化した際の値の型を推論します。
Flowの変数宣言時は型推論は行われませんでしたが、関数やクラス内のメソッドの内部で行われる演算処理に対しては引数を厳密にチェックします。
先ほどのPersonクラスの例のconstructor内に1行だけ演算処理を追加しました。

Typescript

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
    this.name * 1;
  }
}
new Person(null); // OK

Flow

// @flow
class Person {
    name;
    constructor(name) { // => Cannot add this.name and 1 because mixed could either behave like a string or like a number.
      this.name = name;
      this.name * 1;  // null*1は妥当な演算ではない
    }
}
new Person(null); // Error

anyにしてしまえばコンパイルエラーは吐かれませんが、それ以外の場合その変数を参照している箇所すべてをコンパイラがチェックしてくれます。
constructorではなく新たにメソッドを定義してその中で妥当ではない演算処理等をした場合もしっかりエラーを吐いてくれます。

結論

普段Typescriptを使用していてる僕がFlowを魅力的だと思えるのは型推論のあたりでしょうか。
Flowは型定義ファイルをflow-typedというリポジトリで扱っています。

github.com

実際のところFlowをサポートしているライブラリはまだまだ現時点では少ないようです。
FlowにはTypescriptにはない多くの言語機能が盛り込まれていて、他のライブラリには依存しないようなOSS開発に向いているような印象でした。
この記事では紹介してないOpaque TypeType Casting Expressionsという機能もTypescriptにはない便利そうな機能です。

Typescriptはサポートしてくれてるライブラリも多いので一番大きな決めてになっているのが現状です。
まだ感触程度でしかFlowについて知れていませんが、Flowの型推論は非常に強力で気に入りました。
今後もしサポートが手厚くなりそうならすぐにでも乗り移りたいくらいです。