雑兵MeetUp #1で「REVEAL.jsとMilkcocoaで双方向LTする」というLTをしてきました
- Twitterのタイムライン上でイベントをみかけて、あまり見ないコンセプトの勉強会で面白いなーと思ったので盛り上げる側で参加してきました。主催の @yodatomato さん、会場を貸してくれた 21cafeさんありがとうございました。ほかの参加者の方々もお疲れさまでした。
イベント
まとめ
自分のLT資料
- スライド兼デモ
- スライドにコメント投稿するコントローラー
- Github
コメント
- 外でLTするのは超久しぶりだったので案の定、時間内には微妙に収まりきらず、この辺は場数が必要だなという印象ですね。継続して何かネタを投下していきたいと思います。
- 発表してるときに気づいたのですが、流れるコメントにz-indexつけ忘れてたっぽくてスライドの画像の後ろにコメントが回ってしまっていました。Github上のコードは直しています。
- 今回はゆるふわなネタで発表しましたが、スライドで使ったMilkcocoaは国産のBaasとしてはめっちゃ簡単に導入できてアイデアも広がるので知らない方はぜひ触ってみてほしいです。npmでNode.js用のパッケージも提供されてるので、IOTにNodeが動かせる環境が乗っていれば簡単にIOTで集めたデータをPublishすることもできます。IOTの入門としてはハードルは低めではないでしょうか。
- LT後にMilkcocoaの中の人からTシャツをもらいました! @bakuonboogie さんありがとうございました!!
Android StudioとKotlinでAndroidアプリを作る
Kotlinとは
- InteliJ IDEAでおなじみ、JetBrains社が主導して開発している言語で、JVM上で動く静的型付けのオブジェクト指向プログラミング言語。Java 言語よりも簡潔に書けることを目指しているらしい。これを使って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を押します。
- すると
build.gradle
にKotlinでビルドするための設定が追加されるので、この状態でビルドをかけて実行します。
- 動きました!
- 次に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.xml
のTextView
のテキストを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
の表示が変わったはずです。
- まだ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使うので普通にテキストエディタ使ってます。で、世の中的にはVimとEmacsで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 : プロ生ちゃんを背景に召喚できる。デフォだと音声付きなので、自宅以外の場所でうっかり試すと悲劇が起きる。
見た目こんな感じになる
よく使うショートカット
ショートカット | 内容 | 自分で追加/変更 |
---|---|---|
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/2
はquoted 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内の
a
をvar!
で囲むと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
しないと使えない。
- Macroはグローバル空間に注入することはできない。Macroはモジュールの中で
- Macros are explicit
- Macroは明示的に実装されていなければ動かすことができない。
- Macroはコンパイルの間に明示的にもたらされなければならない。
- Macros’ language is clear
- 多くの言語は
quote
やunquote
のためにシンタックスショートカットを提供している。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]}
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)"
fun
はhello/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
を返す必要がある。大事なのはexpression
とvalue
を区別することである。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とほとんど同じで、違いは
supervisor
にtask
を作成するときにnode名を渡しているだけである。lib/kv/supervisor.ex
を開いてTask Supervisor
を追加しよう。
supervisor(Task.Supervisor, [[name: KV.RouterTasks]]),
- 保存したらKVアプリケーションのルートで
iex --sname foo -S mix
とiex --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 table
をKV..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
はアプリケーションごとに持っていて共有されない。共有したい場合は他のアプリケーションのconfig
をimport
することができる。例えばkv_umbrella
のconfig.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
やりたいこと
- とにかくElixirでなんか作ろうと思って書いたのが上の記事ですが、書いていた時に微妙だなーと思ってた点が2つあります。
改善してみる
- コードはこちらにあがっています。
アプリケーションの起動まわり
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のコネクションを張り続けるモジュール
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
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"を流す。
test1はtest2のsleepに待たされることなく、即座にメッセージが返ってきたので、意図した通りに動いていてますね。
次に"test2"を流したあとに"test3"を流して"test3"のプロセスをクラッシュさせてみます。
test2はtest3のクラッシュの影響を受けずにメッセージを返していますね。test3のプロセスはメッセージを送る前に
raise
しているのでメッセージは返ってきません。という感じで
OTP
を使ってプロセス管理するとなかなかElixirっぽいコードになったかなという印象ですね。