もやもやエンジニア

IT系のネタで思ったことや技術系のネタを備忘録的に綴っていきます。フロント率高め。

雑兵MeetUp #1で「REVEAL.jsとMilkcocoaで双方向LTする」というLTをしてきました

  • Twitterのタイムライン上でイベントをみかけて、あまり見ないコンセプトの勉強会で面白いなーと思ったので盛り上げる側で参加してきました。主催の @yodatomato さん、会場を貸してくれた 21cafeさんありがとうございました。ほかの参加者の方々もお疲れさまでした。

イベント

zohyo.connpass.com

まとめ

自分のLT資料

コメント

  • 外でLTするのは超久しぶりだったので案の定、時間内には微妙に収まりきらず、この辺は場数が必要だなという印象ですね。継続して何かネタを投下していきたいと思います。
  • 発表してるときに気づいたのですが、流れるコメントにz-indexつけ忘れてたっぽくてスライドの画像の後ろにコメントが回ってしまっていました。Github上のコードは直しています。
  • 今回はゆるふわなネタで発表しましたが、スライドで使ったMilkcocoaは国産のBaasとしてはめっちゃ簡単に導入できてアイデアも広がるので知らない方はぜひ触ってみてほしいです。npmでNode.js用のパッケージも提供されてるので、IOTにNodeが動かせる環境が乗っていれば簡単にIOTで集めたデータをPublishすることもできます。IOTの入門としてはハードルは低めではないでしょうか。
  • LT後にMilkcocoaの中の人からTシャツをもらいました! @bakuonboogie さんありがとうございました!!

f:id:Rei19:20151030142946j:plain

Android StudioとKotlinでAndroidアプリを作る

Kotlinとは

導入

  • Android Studioは同じJetBrains製品のInteliJベースで作られているだけあって、プラグインを入れるだけで簡単に導入可能。Preferences -> pluginから以下の2つをインストールして再起動するだけです。なお自分の環境はAndroid Studio 1.4、compileSdkVersionは23、minSdkVersionは16にしてます。
    • Kotlin
    • Kotlin Extensions For Android

プロジェクトを作ってJavaのコードをKotlinに変換する

  • 適当にBlankActivityだけ追加したプロジェクトを作ります。そうするとこんなActivityが出来ますね。
package me.rei_m.kotlinsample;

import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
            }
        });
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }
}
  • プロジェクトツリーにappがフォーカスされている状態でメニューバーからcodeを選ぶと一番下にConvert Java File to Kotlin Fileが追加されていているのでこれを選択します。すると、Kotlinへのコンバートが開始されます。
  • コンバートが完了するとMainActivityはこのように生まれ変わり、拡張子も.ktになります。ハイライト効かないのでみづらいですね。。。早速、valが見えているあたり、Scalaに近い雰囲気を感じます。
package me.rei_m.kotlinsample

import android.os.Bundle
import android.support.design.widget.FloatingActionButton
import android.support.design.widget.Snackbar
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.view.View
import android.view.Menu
import android.view.MenuItem

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val toolbar = findViewById(R.id.toolbar) as Toolbar
        setSupportActionBar(toolbar)

        val fab = findViewById(R.id.fab) as FloatingActionButton
        fab.setOnClickListener(object : View.OnClickListener {
            override fun onClick(view: View) {
                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG).setAction("Action", null).show()
            }
        })
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        // Inflate the menu; this adds items to the action bar if it is present.
        menuInflater.inflate(R.menu.menu_main, menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        val id = item.itemId

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true
        }

        return super.onOptionsItemSelected(item)
    }
}
  • 次にメニューのTools -> Kotlin -> Configure Kotlin Projectを選択してそのままOKを押します。

f:id:Rei19:20151017201814p:plain

  • するとbuild.gradleにKotlinでビルドするための設定が追加されるので、この状態でビルドをかけて実行します。

f:id:Rei19:20151017204542p:plain

  • 動きました!
  • 次にKotlin Extensionを使うための設定を追加します。build.gradleを開いてdependentciesにextensionsを追加します。
buildscript {
    ext.kotlin_version = '0.14.451'
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version" <- これを追加
    }
}
  • そしてMainActivity.ktを開いてimportを追加します。syntheticから後ろはxmlの名前です。
import kotlinx.android.synthetic.activity_main.*
import kotlinx.android.synthetic.content_main.*
  • これで何ができるようになるかというとfindViewByIdを使わなくてもViewのコンポーネントを参照できるようになります。content_main.xmlTextViewのテキストをActivityから書き換えてみましょう。参照するためにはIDが必要なのでxmlを開いて適当にTextViewにidを追加します。ここではhelloとしています。
  • MainActivity.ktに戻り、findViewByIdを使っているところを書き換えてみましょう。
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // val toolbar = findViewById(R.id.toolbar) as Toolbar
        // setSupportActionBar(toolbar)
        setSupportActionBar(this.toolbar)

        // val fab = findViewById(R.id.fab) as FloatingActionButton
        // fab.setOnClickListener(object : View.OnClickListener {
        this.fab.setOnClickListener(object : View.OnClickListener {
            override fun onClick(view: View) {
                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG).setAction("Action", null).show()
            }
        })

        // TextViewを書き換える処理を追加
        this.hello.text = "Hello Kotlin !!"
    }
  • 今までfindViewByIdを使っていたところはimportしたことでthisから参照できるようになりました。わかりやすくthisを書いてますが、もちろん無くても動きます。これでビルドして動かしてみましょう。TextViewの表示が変わったはずです。

f:id:Rei19:20151017210329p:plain

  • まだReferenceを読んでいる途中ですが、Javaで書くよりだいぶ簡潔に書ける印象です。しばらく触ってみて既存の資産の使い方など確かめながら何か作ろうかと思います。

追記

  • 早速書き始めたらいきなりExtensionが原因でこけた。。。下のようにViewに追加するとクラスが見つからないと言われて死にます。
    <AppCompatButton
        android:id="@+id/open_hogehoge"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="なんか開く" />
  • AppCompatButtonをフルパスで指定したらビルド通りました。Support library系のコンポーネント使うときは注意しないといけないですねー
    <android.support.v7.widget.AppCompatButton
        android:id="@+id/open_list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Open sample of listView" />

Atomがだいたい使える感じになってきたので設定を整理してみる

Atom始めました

  • 仕事だと.Net開発してるので大部分の時間はWindows/VisualStudioを使ってますが、家だとMacBook使うので普通にテキストエディタ使ってます。で、世の中的にはVimEmacsで2分している中、Sublime Text 2でHTMLやらJavaScriptやらNode.jsやらGoやら書いてましたが、ここ最近はAtomに乗り換えてます。だいぶ馴染んだかなと思うので現状を整理。

使ってるPlugin

  • atom-material-ui : UI Themeに設定。Material Designっぽい感じの見た目になる
  • seti-syntax : Syntax Themeに設定。
  • atom-cli-diff : diffツール
  • autocomplete-emojis : 絵文字のAutoCompleteを追加
  • autocomplete-paths : ファイルパスを入力する際のAutoComplete
  • color-picker : カラーピッカーを表示してRGBを入力できたり
  • file-icons : ファイルアイコンが追加される。楽しい。
  • highlight-selected : ファイル内で選択した単語と同じ単語をハイライトする。
  • maximize-panes : 複数Pane開いている場合、ActiveなPaneを全画面表示できるように
  • merge-conflicts : Atom上でgitのconflictをいい感じに編集できる。らしい。ほぼソロ開発なのでまだお世話になってない。
  • regex-railroad-diagram : 正規表現書くとグラフィカルにその表現を図で表示してくれる。
  • project-manager : プロジェクトの管理をしやすくする。色んな場所にプロジェクトがあってもこのパッケージで管理されていれば簡単にたどり着ける
  • tabs-to-spaces : tabをspaceに変換してくれる
  • term2 : Atomのpaneでコンソール開ける
  • ask-stack : Atom上からStackOverFlowに検索をかけて検索結果を見ることができる。
  • language-babel : Babelサポート
  • language-elixir : Elixirサポート
  • react : Reactサポート
  • language-haskell : Haskellサポート。言語系のパッケージは必要になったら探して入れるという感じですな。
  • linter : Lintツール。リアルタイムでチェックしてくれる。下で挙げてるLintたちはこれがないと動かない
  • linter-csslint : CSSのLint
  • linter-jshint : JSのLint
  • 番外:atom-pronama-chan : プロ生ちゃんを背景に召喚できる。デフォだと音声付きなので、自宅以外の場所でうっかり試すと悲劇が起きる。

見た目こんな感じになる

f:id:Rei19:20151012084758p:plain

よく使うショートカット

ショートカット 内容 自分で追加/変更
cmd-, Setting-Viewを開く。パッケージいれたり何か設定するときはここから
ctrl-h, j, k, l, a, e おなじみのカーソル移動
ctrl-alt-h, l 単語レベルで左右に移動
ctrl-tab, ctrl-tab-shift おなじみのタブ移動
ctrl-enter go-to-declaration。選択した関数やら変数の定義元に飛ぶ。要ctags
ctrl-0 PaneとTreeのフォーカスを切り替え
ctrl-w ActiveなPaneを閉じる
ctrl-cmd-s 今開いているプロジェクトを名前をつけて保存する
ctrl-cmd-p 保存済のプロジェクトの一覧を表示して開く
cmd-k + left, right, top, bottom 指定した方向に新しいPaneを開く
cmd-k + cmd-left, right, top, bottom 指定した方向に新しいTerminalを開く
ctrl-] 次のPaneに移動する
ctrl-[ 前のPaneに移動する
ctrl-- Paneのサイズを小さくする
ctrl-; Paneのサイズを大きくする
cmd-n 新しいファイルを作成
cmd-o ファイルを開く
cmd-d カーソルが当たっている単語を選択される。複数個選択することもできる。
cmd-/ 選択中のコードをコメントアウト/コメントアウト解除
cmd-r ファイル内のシンボルの一覧を表示して移動
cmd-f 現在のファイルから検索
cmd-t プロジェクトからファイルを検索して開く
cmd-shift-F 現在のプロジェクトから検索
cmd-shift-enter maximize-panesを呼ぶ。カレントのpaneを最大表示する。その状態からもう一回叩くと元に戻る
cmd-shift-R プロジェクト内のシンボルの一覧を表示して移動
cmd-shift-C カラーピッカーを表示。ピッカーから選択したカラーコードがエディタに反映される
cmd-shift-A ask-stackを呼ぶ
  • 今のところこんな感じで、それなりに快適。

Elixir 入門 ~ META-PROGRAMMING IN ELIXIR ~ その2 - Macro

Macros

Foreword

  • Macroは通常のElixirのコードよりもわかりづらい。開発者は責任を持ってクリーンなコードを書くように心がける。
  • Elixirはすでに簡潔でわかりやすい仕組みを提供している。Macroを使うのは最後の手段であると理解すべきだろう。

Our first macro

  • Macroはdefmacro/2で定義される。macros.exsを作ってみよう。
defmodule Unless do
  def fun_unless(clause, expression) do
    if(!clause, do: expression)
  end

  defmacro macro_unless(clause, expression) do
    quote do
      if(!unquote(clause), do: unquote(expression))
    end
  end
end
  • iex macros.exs で作成したモジュールと使ってiexを開始して、以下のコマンドを投げてみる。
iex> require Unless
nil
iex> Unless.macro_unless true, IO.puts "this should never be printed"
nil
iex> Unless.fun_unless true, IO.puts "this should never be printed"
"this should never be printed"
nil
  • fun_unless!trueの評価にもかかわらず文字列が表示されている。これはunlessが呼ばれる前にすでにIO.puts ~が評価されているためである。もう少し詳しく見てみる。

  • Unless.macro_unless true, IO.puts "this should never be printed"マクロ が呼ばれた時、macro_unless は以下を受け取っている。

  • Unless.macro_unless(true, {{:., [], [{:aliases, [], [:IO]}, :puts]}, [], ["this should never be printed"]})
  • そしてMacroから返される式は以下になる。
{:if, [], [
  {:!, [], [true]},
  {{:., [], [IO, :puts], [], ["this should never be printed"]}}]}
  • Macro.expand_once/2を使って確認してみよう。
iex> expr = quote do: Unless.macro_unless(true, IO.puts "this should never be printed")
iex> res  = Macro.expand_once(expr, __ENV__)
iex> IO.puts Macro.to_string(res)
if(!true) do
  IO.puts("this should never be printed")
end
:ok
  • Macro.expand_once/2quoted expressionを受け取り、現在の環境に合わせて拡張した結果を返す。
  • このようにMacroはquoted expressionを受け取り何かに変換して返す。実のところunless/2, defmacro/2, def/2, defprotocol/2のようなElixirのコンストラクタはすべてマクロである。すなわち、開発者の環境に応じて拡張されている。
  • 我々はどのような関数やMacroもオーバーライドできる。ただし、Kernel.SpecialFormsは例外で、拡張はできない。

Macros hygiene

  • ElixirのMacroは遅延評価(late resolutionはこの訳でいいのか。。。)を持っている。これはquoted expressionの中の変数とMacroが拡張されるときの変数がコンフリクトしないことを保証している。以下の例を見てみる。
defmodule Hygiene do
  defmacro no_interference do
    quote do: a = 1
  end
end

defmodule HygieneTest do
  def go do
    require Hygiene
    a = 13
    Hygiene.no_interference
    a
  end
end
HygieneTest.go
# => 13
  • no_interference/0の中でaに値を束縛しているが、requireしているTestモジュールのaには影響を与えていない。
  • 次のようにするとどうだろうか。
defmodule Hygiene do
  defmacro interference do
    quote do: var!(a) = 1
  end
end

defmodule HygieneTest do
  def go do
    require Hygiene
    a = 13
    Hygiene.interference
    a
  end
end
HygieneTest.go
# => 1
  • Macro内のavar!で囲むとgo/0内のaに値を再束縛している。

The environment

  • Macro.expand_once/2 を使った時に引数に __ENV__ を指定したが、これはなんだろうか。
  • __ENV__ は、現在のモジュールに関する様々な情報が格納された Macro.Env 構造体のインスタンスを返す。
iex> __ENV__.module
nil
iex> __ENV__.file
"iex"
iex> __ENV__.requires
[IEx.Helpers, Kernel, Kernel.Typespec]
iex> require Integer
nil
iex> __ENV__.requires
[IEx.Helpers, Integer, Kernel, Kernel.Typespec]

Private macros

  • ElixirはPrivate Macroもサポートしている。モジュール内がスコープになるMacroだが、注意しなければならないことがある。
  • Macroは使う前に定義されていなければならない。例えば以下のような場合は、Macroを定義する前に使っているのでエラーになる。
iex> defmodule Sample do
...>  def four, do: two + two
...>  defmacrop two, do: 2
...> end
** (CompileError) iex:2: function two/0 undefined

Write macros responsibly

  • Macroは強力で、ElixirはMacroを責任を持って使うための仕組みを提供している。
  • Macros are hygienic
    • Macroの中で定義された変数はユーザーコードに影響されない。
    • さらにMacroの中での関数呼び出しや、aliasはユーザーのコンテキストを汚染しない
  • Macros are lexical
    • Macroはグローバル空間に注入することはできない。Macroはモジュールの中でrequireあるいはimportしないと使えない。
  • Macros are explicit
    • Macroは明示的に実装されていなければ動かすことができない。
    • Macroはコンパイルの間に明示的にもたらされなければならない。
  • Macros’ language is clear
    • 多くの言語はquoteunquoteのためにシンタックスショートカットを提供している。ElixirはMacroの定義やquoted expressionとの境界を明確にするために、別に明示的に書くようにしている。
  • 上記の仕組みを持ったとしても、開発者は大きな責任を負う。もしMacroを使う場合は、MacroはあなたのAPIではないということを覚えておこう。Macroの定義は短く保つべきである。次の例を見てみよう。
defmodule MyModule do
  defmacro my_macro(a, b, c) do
    quote do
      do_this(unquote(a))
      ...
      do_that(unquote(b))
      ...
      and_that(unquote(c))
    end
  end
end
  • これを次のように書き換える
defmodule MyModule do
  defmacro my_macro(a, b, c) do
    quote do
      # Keep what you need to do here to a minimum
      # and move everything else to a function
      do_this_that_and_that(unquote(a), unquote(b), unquote(c))
    end
  end

  def do_this_that_and_that(a, b, c) do
    do_this(a)
    ...
    do_that(b)
    ...
    and_that(c)
  end
end
  • Macro内のコードが明確でテストしやすくなった。テストを書く場合はdo_this_that_and_that/3を直接呼べばよい。

Elixir 入門 ~ META-PROGRAMMING IN ELIXIR ~ その1 - Quote and unquote

Quoting

  • Elixir のプログラムはtupleを伴った3つの要素に置き換えることができる。例えばsum(1, 2, 3)があった場合は以下のように書き換えることができる。
  • 以下、自分の環境だとsumなんかおらん!と言われるのでremとか適当なビルトイン関数に置き換えて試すといいかと。
{:sum, [], [1, 2, 3]}
  • quote マクロを使うと式の構造を見ることができる
iex> quote do: sum(1, 2, 3)
{:sum, [], [1, 2, 3]}
  • 1つ目の要素は関数名のatom、2つ目はメタデータのリスト、3つ目は引数のリストとなる
  • operatorも同様に確認できる。
iex> quote do: 1 + 2
{:+, [context: Elixir, import: Kernel], [1, 2]}
  • mapも同様に確認できる。
iex> quote do: %{1 => 2}
{:%{}, [], [{1, 2}]}
  • 変数も同様
iex> quote do: x
{:x, [], Elixir}
  • Macro.toString/1を使えば式を文字列で取得できる。
iex> Macro.to_string(quote do: sum(1, 2 + 3, 4))
"sum(1, 2 + 3, 4)"

Unquoting

  • Quote で式の構造を見ることができるが、場合によっては展開して欲しくない部分もあるかもしれない。以下の例を見てみる。
iex> number = 13
iex> Macro.to_string(quote do: 11 + number)
"11 + number"
  • numberがそのままstringとして扱われていて、欲しい結果ではない。numberに値を注入するためにはunquoteを使う。
iex> number = 13
iex> Macro.to_string(quote do: 11 + unquote(number))
"11 + 13"
  • さきほどとは異なり、unquoteで囲んだnumberは束縛されている値が表示された。
  • unquoteは関数名にも適用できる。
iex> fun = :hello
iex> Macro.to_string(quote do: unquote(fun)(:world))
"hello(:world)"
  • funhello/1で展開された。
  • Listの中に新しい要素を追加したい場合を考えてみる。
iex> inner = [3, 4, 5]
iex> Macro.to_string(quote do: [1, 2, unquote(inner), 6])
"[1, 2, [3, 4, 5], 6]"
  • これは期待した結果ではない。このような場合は unquote_splicing を使うとよい。
iex> inner = [3, 4, 5]
iex> Macro.to_string(quote do: [1, 2, unquote_splicing(inner), 6])
"[1, 2, 3, 4, 5, 6]"
  • Unquotingはコードの中に別のコードを埋め込むことができ、マクロを書くときにとても便利なので覚えておこう。

Escaping

  • Macro.escape/1を使っても展開できる。
iex> map = %{hello: :world}
iex> Macro.escape(map)
{:%{}, [], [hello: :world]}
  • マクロはquoted expressionsを受け取りquoted expressionsを返す必要がある。大事なのはexpressionvalueを区別することである。integerやatom、stringといったようなvalueはquoted expressionsが自分自身の値だが、mapのような値は変換される必要がある。
  • ちょっと訳が怪しいけど。。。おそらく以下のようにquoteをかけるとquoted expressionsが返るものと自身の値が返るもので違いがあるので気をつけろということかな。
iex> quote do: %{1=> 2}
{:%{}, [], [{1, 2}]}
iex> quote do: :hello
:hello

Elixir 入門 ~ MIX AND OTP ~ その9 - Distributed tasks and configuration

  • 最後にroutingの機能をKVアプリケーションに追加する。routing table はこのようになる。
[{?a..?m, :"foo@computer-name"},
 {?n..?z, :"bar@computer-name"}]

Our first distributed code

  • VMに名前をつけて開始する。iex --sname fooでREPLを起動する。
  • すると以下のように node名@computer-name がpromptに表示される
Interactive Elixir - press Ctrl+C to exit (type h() ENTER for help)
iex(foo@your_hostname)>
  • この中でモジュールを定義する。
iex(foo@your_hostname)> defmodule Hello do
...>  def world, do: IO.puts "hello world"
...> end

iex(foo@your_hostname)> Hello.world
hello world
:ok
  • 次にfooを生かしたまま、別のnode名で新しくREPLを立ち上げて、先ほどのHello.worldを呼ぶ
iex --sname bar
iex(bar@your_hostname)> Hello.world
** (UndefinedFunctionError) undefined function: Hello.world/0 (module Hello is not available)
    Hello.world()
  • 当然、barには定義されていないので返ってこない。ここで Node.spawn_link を使ってfooの新しいプロセスを作ることができる。
iex(bar@your_hostname)> Node.spawn_link :"foo@ your_hostname", fn -> Hello.world end
hello world
#PID<8897.78.0>
  • hello world` が 表示された。他のnodeのプロセスを生んでそのpidを返したことがわかる。次にそのpidを使ってメッセージの送受信をする。
iex(bar@your_hostname)> pid = Node.spawn_link :"foo@your_hostname", fn ->
...>   receive do
...>     {:ping, client} -> send client, :pong
...>   end
...> end
#PID<8897.86.0>
iex(bar@your_hostname)> send pid, {:ping, self}
{:ping, #PID<0.65.0>}
iex(bar@your_hostname)> flush
:pong
:ok
  • foo nodeに :ping を受け取って :pong を返す関数を定義したプロセスを作った。そしてbarから fooに :ping を送って :pong が帰ってきたことがわかる。ただ、このやり方はsupervison tree の管理からは外れるので避けたい。他の方法も探してみよう。

async/await

  • Elixirは async/await パターンも提供している。asyncで先に計算してawaitであとで結果を取り出すというようなことが可能。
iex> task = Task.async(fn ->
...> :timer.sleep(5000)
...> "hogehoge"
...> end)
%Task{pid: #PID<0.82.0>, ref: #Reference<0.0.2.103>}
iex>  Task.await(task)
"hogehoge"
  • Task.supervisorの下で使うことも可能。その場合はTask.Supervisor.start_child/2の代わりにTask.Supervisor.async/2でプロセスを作ってやればよい。結果の取り出しはTask.await/2で可能

Distributed tasks

  • distributeされたtaskはsuperviseされたtaskとほとんど同じで、違いはsupervisortaskを作成するときにnode名を渡しているだけである。lib/kv/supervisor.exを開いてTask Supervisorを追加しよう。
supervisor(Task.Supervisor, [[name: KV.RouterTasks]]),
  • 保存したらKVアプリケーションのルートで iex --sname foo -S mixiex --sname bar -S mix をそれぞれ立ち上げる。
  • 片方からもう片方にnode名を返すタスクを作ってasync/awaitでnode名が返ることを確認する。
iex(bar@your_hostname)> task = Task.Supervisor.async {KV.RouterTasks, :"foo@your_hostname"}, fn ->
...>   {:ok, node()}
...> end
iex(bar@your_hostname)> task = Task.Supervisor.async {KV.RouterTasks, :"foo@your_hostname"}, fn ->
...> (bar@your_hostname)> {:ok, node()}
...> (bar@your_hostname)> end
%Task{pid: #PID<12875.125.0>, ref: #Reference<0.0.7.9>}
iex(bar@your_hostname)> Task.await(task)
{:ok, :"foo@your_hostname"}

Routing layer

  • lib/kv/router.ex を作ってroutingの機能を実装する。
  • 以降のコードのyour_hostnameのあたりはサンプルを動かす実行環境に合わせて修正
defmodule KV.Router do
  @doc """
  Dispatch the given `mod`, `fun`, `args` request
  to the appropriate node based on the `bucket`.
  """
  def route(bucket, mod, fun, args) do
    # Get the first byte of the binary
    first = :binary.first(bucket)

    # Try to find an entry in the table or raise
    entry =
      Enum.find(table, fn {enum, node} ->
        first in enum
      end) || no_entry_error(bucket)

    # If the entry node is the current node
    if elem(entry, 1) == node() do
      apply(mod, fun, args)
    else
      sup = {KV.RouterTasks, elem(entry, 1)}
      Task.Supervisor.async(sup, fn ->
        KV.Router.route(bucket, mod, fun, args)
      end) |> Task.await()
    end
  end

  defp no_entry_error(bucket) do
    raise "could not find entry for #{inspect bucket} in table #{inspect table}"
  end

  @doc """
  The routing table.
  """
  def table do
    # Replace computer-name with your local machine name.
    [{?a..?m, :"foo@your_hostname"},
     {?n..?z, :"bar@your_hostname"}]
  end
end
  • テストもtest/kv/router_test.exs を追加する。
defmodule KV.RouterTest do
  use ExUnit.Case, async: true

  # 対応するnodeがrouterから返ってくるか
  test "route requests across nodes" do
    assert KV.Router.route("hello", Kernel, :node, []) ==
           :"foo@your_hostname"
    assert KV.Router.route("world", Kernel, :node, []) ==
           :"bar@your_hostname"
  end

  test "raises on unknown entries" do
    assert_raise RuntimeError, ~r/could not find entry/, fn ->
      KV.Router.route(<<0>>, Kernel, :node, [])
    end
  end
end
  • 作成したらテストを走らせるために事前に iex --sname bar -S mix で bar node を作成し、その後に elixir --sname foo -S mix test で foo node でテストが実行される。通ればOK。

Test filters and tags

  • テストは通ったが mix testだけでは走ることができない複雑な構造になってしまった。
  • tagを使おう。test/kv/router_test.exsを開いてテストにtagをつけてみる。
# @tag distributed: true と書いても同じ
@tag :distributed
test "route requests across nodes" do
  • 次に test/test_helper.exs を開いて、Nodeが生きている場合のみdistributed tag が付いているテストが実行されるようにする。
exclude =  if Node.alive?, do: [], else: [distributed: true]

ExUnit.start(exclude: exclude)
  • mix testを走らせると、route requests across nodes テストが対象から外れて実行されなくなったはずだ。
  • tagを使ったmix testのオプションとして以下がある。
    • mix test --include tag : include オプションで指定されたtagはテスト対象に含まれる
    • mix test --exclude tag : exclude オプションで指定されたtagはテスト対象から除外される
    • mix test --only tag : only オプションで指定されたtagはそのtagがついたテストのみ実行される
  • 今回のケースでdistributed tagがついたケースだけを実行させたい場合は elixir --sname foo -S mix test --only distributed で実行される。もちろん、この場合はテストで使うnodeが動いていなければテストは通らない。

Application environment and configuration

  • Routing tableKV..Routerモジュールにハードコードしたが、これはうまくない。developとproductionで異なるものを使いたいときもあるだろう。
  • application environmentを使う。apps/kv/mix.exsを開いてapplication/0を次のように書き直す。
def application do
  [applications: [],
   env: [routing_table: []],
   mod: {KV, []}]
end
  • 新たに:env keyを追加した。env:で設定されたkey-valueがapplication environmentのデフォルト値となる。KV.Router.table/0' を書き換えてapplication environmentからRouting table`の設定を読み込むようにする。
@doc """
The routing table.
"""
def table do
  Application.get_env(:kv, :routing_table)
end
  • Application.get_env/2 で指定したapplication environmentの値を取ることができる。次に実際に値を設定する。設定する場所は決まっていてconfig/config.exsに書く。routing_tableの設定を書こう。
# Replace computer-name with your local machine nodes.
config :kv, :routing_table,
       [{?a..?m, :"foo@your_hostname"},
        {?n..?z, :"bar@your_hostname"}]
  • config/config.exsはアプリケーションごとに持っていて共有されない。共有したい場合は他のアプリケーションのconfigimportすることができる。例えばkv_umbrellaconfig.exsと共有したい場合は次のようにimportする。
import_config "../apps/kv/config/config.exs"
  • 実際にはumbrellaオプションをつけて作成したアプリケーションは自動的にapps配下のすべてのconfigをimportするようになっているのでこのコードを書く必要はない。

さいごに

  • 課題っぽいのが与えられているので解いてみる
    • 内部のハードコードされているポートをapplication environmentから取るようにする。
    • kv_serverがローカルのKV.Registryからbucketを作っているところをroutingを使うようにする。テストも直す。
  • 最後はやっつけ仕事になってしまった。。。

とりあえず、今までのコード

elixir_training/kv_umbrella at master · rei-m/elixir_training · GitHub

Elixir 入門 ~ MIX AND OTP ~ 実際にOTPを使ってプロセス管理をする

  • ElixirのGetting StartもMix/OTPが終わり、次に進むのですが、復習がてら前にElixirで書いたコードをMix and OTPの章でやったことを使って改善してみます。
  • 元ネタはQiitaの自分の投稿 ElixirでSlackのbotを作ってHerokuで動かしてみる - Qiita
    • いまいちQiita とはてブの使い分けが明確になってないけど、今のとこ、完全に自分用のメモとかQiitaに上げるまでもないかなと思ったものははてブに書いてます。

やりたいこと

  • とにかくElixirでなんか作ろうと思って書いたのが上の記事ですが、書いていた時に微妙だなーと思ってた点が2つあります。
    1. アプリケーションの起動が微妙。わざわざboot用のファイルを作ってmix run で指定するなんてことはしなくてもできるはず。
    2. Slackからメッセージを受け取り解析してメッセージを送り返すという動作を一つのプロセスで回しているので、botに重いことをさせようとすると、全体が遅れてしまう。プロセスを分離してbotの行動の一つ一つは別プロセスで動かしたい。

改善してみる

  • コードはこちらにあがっています。

rei-m/magic_bot · GitHub

アプリケーションの起動まわり

  • mix.exs を以下のように編集してapplication起動時のモジュールを指定して、Applicationビヘイビアを実装したMagicBotモジュールを作成してそこに起動用のコードを書けば解決。
  def application do
    [applications: [:logger, :slack],
    mod: {MagicBot, []}]
  end
  • これでmix run --no-halt でアプリケーションが起動するようになりました。

Supervision Tree を使ってプロセスを分離する

  • 今、プロセス1本で動いてるところをこういうSupervision Treeに変える。

  • アプリケーションのsupervisor

    • Slackのソケットを張り続けるプロセス
    • Botに実行させたいコマンドを管理するsupervisor
      • 各コマンド。命令を受け取った時点でsupevisor経由で動的に作成される。
  • Slackからメッセージを受け取ったらパースしてsupervisor経由でやりたいことを実行させるという感じですね。

作ってみた

  • まずはアプリケーションの起点となるモジュールで全体を管理するSupervisorを起動

lib/magic_bot.ex

defmodule MagicBot do

  use Application

  def start(_type, _args) do
    MagicBot.Supervisor.start_link
  end

end
  • 次にBotのsupervisor

lib/magic_bot/supervisor.ex

defmodule MagicBot.Supervisor do

  use Supervisor

  def start_link(opts \\ []) do
    Supervisor.start_link(__MODULE__, :ok, opts)
  end

  # Define alias of process name
  @bot_name MagicBot.Bot
  @action_sup_name BotAction.Supervisor

  def init(:ok) do

    # Get API Token of Slack.
    api_key = case System.get_env("MAGICBOT_API_KEY") do
      nil -> Application.get_env(:MagicBot, :api_key)
      s -> s
    end

    # Make child process
    children = [
      supervisor(BotAction.Supervisor, [[name: @action_sup_name]]),
      worker(MagicBot.Bot, [api_key, [name: @bot_name, sup_action: @action_sup_name]])
    ]

    # 戦略は `one_for_one`でSlackとアクションのsupervisorを起動
    supervise(children, strategy: :one_for_one)
  end

end
  • MagicBot.Supervisorに管理されるSlackのコネクションを張り続けるモジュール

lib/magic_bot/bot.ex

defmodule MagicBot.Bot do

  use Slack

  def handle_connect(slack, state) do
    # Slackとの接続成功時に呼ばれるコールバック
    IO.puts "Connected as #{slack.me.name}"
    {:ok, state}
  end

  def handle_message(message = %{type: "message", text: _}, slack, state) do
    # Slackからメッセージを受け取った時に呼ばれるコールバック
    trigger = String.split(message.text, ~r{ | })
    
    case String.starts_with?(message.text, "<@#{slack.me.id}>: ") do
      # @bot名 ~ できたら :respond を渡してactionのプロセスを開始
      true -> BotAction.Supervisor.start_action(state[:sup_action], :respond, Enum.fetch!(trigger, 1), message, slack)
      # それ以外は :hear を渡して actionのプロセスを開始
      false -> BotAction.Supervisor.start_action(state[:sup_action], :hear, hd(trigger), message, slack)
    end
    {:ok, state}
  end

  def handle_message(_message, _slack, state) do
    {:ok, state}
  end
end
  • Botの行動を管理するSupervisor。

lib/bot_action/supervisor.ex

defmodule BotAction.Supervisor do

  def start_link(opts \\ []) do
    Task.Supervisor.start_link(opts)
  end

  def start_action(supervisor, command, trigger, message, slack) do
    # Slackのプロセスから呼ばれ、このSupervisor配下に新しいプロセスを作成する。
    # Task.Supervisorの子プロセスの戦略は `simple_one_for_one` になり、動的に追加できる。
    # クラッシュ時は再起動されない
    Task.Supervisor.start_child(supervisor, fn ->
      case command do
        :respond -> BotAction.Action.respond(trigger, message, slack)
        :hear -> BotAction.Action.hear(trigger, message, slack)
      end
    end)
  end

end
  • Botの行動を管理するモジュール。Botに何かさせたい時はこのモジュールに追加していく。

lib/bot_action/action.ex

defmodule BotAction.Action do

  def hear("lgtm", message, slack) do
    HTTPoison.start
    case URI.encode("http://www.lgtm.in/g") |> HTTPoison.get do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> body
        |> Floki.find("#imageUrl")
        |> Floki.attribute("value")
        |> hd
        |> send_message(message, slack)
      {_, _} -> nil
    end
  end

  def hear(_, _, _) do end

  def respond("エリクサーほしい?", message, slack) do
     send_message("エリクサーちょうだい!\nhttp://img.yaplog.jp/img/01/pc/2/5/2/25253/1/1354.jpg", message, slack)
  end

  def respond(_, _, _) do end

  defp send_message(text, message, slack) do
    Slack.send_message(text, message.channel, slack)
  end

end
  • これで完成。

意図した通りに動くか確認

  • lib/bot_action/action.exに以下の関数を追加してみる。
  def hear("test1", message, slack) do
    # 単純にメッセージを返す
    send_message("test1", message, slack)
  end

  def hear("test2", message, slack) do
    # 5秒待ってからメッセージを返す
    :timer.sleep(5000)
    send_message("test2", message, slack)
  end

  def hear("test3", message, slack) do
    # 意図的に例外を起こす
    raise "oops"
    send_message("test3", message, slack)
  end
  • botを起動してSlackに"test2"を流したあとに"test1"を流す。

f:id:Rei19:20150921140421p:plain

  • test1はtest2のsleepに待たされることなく、即座にメッセージが返ってきたので、意図した通りに動いていてますね。

  • 次に"test2"を流したあとに"test3"を流して"test3"のプロセスをクラッシュさせてみます。

f:id:Rei19:20150921140720p:plain

  • test2はtest3のクラッシュの影響を受けずにメッセージを返していますね。test3のプロセスはメッセージを送る前にraiseしているのでメッセージは返ってきません。

  • という感じでOTPを使ってプロセス管理するとなかなかElixirっぽいコードになったかなという印象ですね。