forked from elixirschool/elixirschool
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[JP] Translate GenStage and Protocols (elixirschool#1048)
* [JP] Translate GenStage * [JP] Translate Protocols * [JP] Translate GenStage: changed to more appropriate Japanese words.
- Loading branch information
1 parent
37213d0
commit c859801
Showing
2 changed files
with
378 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,262 @@ | ||
--- | ||
version: 1.0.0 | ||
layout: page | ||
title: GenStage | ||
category: advanced | ||
order: 11 | ||
lang: jp | ||
--- | ||
|
||
このレッスンでは、GenStage をどのように扱うのか、アプリケーションでどのように活用できるのかを詳しく見ていきます。 | ||
|
||
{% include toc.html %} | ||
|
||
## イントロダクション | ||
|
||
GenStage とは何でしょうか? 公式ドキュメントによると「Elixir 向けの仕様と計算フロー」とされていますが、これはいったいどういう意味なのでしょうか? | ||
|
||
つまるところ GenStage は、個別のプロセスで独立したステップ(ステージ)によって実行される、処理のパイプラインを定義する方法を提供します。パイプラインを使った処理をした経験がある方なら、こうした概念はよくご存知でしょう。 | ||
|
||
この仕組みがどうやって動くかをより良く理解するために、単純な生産者-消費者のフローを以下に図示します。 | ||
|
||
``` | ||
[A] -> [B] -> [C] | ||
``` | ||
|
||
この例では生産者(producer) `A`、生産者-消費者(producer-consumer) `B`、そして消費者(consumer) `C` という、3つのステージがあります。`A` は `B` により消費される値を生産し、`B` はなんらかの処理をしたのちに消費者 `C` によって受信される新しい値を返します。次のセクションで説明するようにステージの役割は重要です。 | ||
|
||
この例では、1対1の生産者対消費者となっていますが、どのステージにおいても、両方とも複数の生産者、複数の消費者を持つことが可能です。 | ||
|
||
この概念をより詳しく説明するために、GenStage を使ってパイプラインを構築しますが、まずはじめに GenStage の各役割をもう少し深く掘り下げてみましょう。 | ||
|
||
## 消費者と生産者 | ||
|
||
これまで見てきたように、ステージに与えられた役割は大切です。GenStage の仕様では以下の3つの役割があります。 | ||
|
||
+ `:producer` — 送り元。生産者は消費者からの催促を待ち、要求されたイベントに応じる。 | ||
|
||
+ `:producer_consumer` — 送り元と受け手の両方。生産者-消費者は、生産者からの要求イベントと同じように、他の消費者からの催促に応じる。 | ||
|
||
+ `:consumer` — 受け手。消費者は、生産者に要求してデータを受け取る。 | ||
|
||
生産者が催促を __待っている__ ことに注意してください。GenStage では、消費者が上流の生産者に催促を送り、生産者から送られてくるデータを処理します。これによって、バックプレッシャーとして知られる仕組みが容易になります。バックプレッシャーというのは、消費者の処理がビジーの時に過剰なプレッシャーがかからないよう、生産者側にその責任を負わせる仕組みです。 | ||
|
||
さて、GenStage の内部の役割についておさらいしたところで、実際のアプリケーションを作ってみましょう。 | ||
|
||
## はじめに | ||
|
||
この例では、数値を生成し、偶数をソートし、そして最後にそれらを出力する GenStage アプリケーションを構築します。 | ||
|
||
このアプリケーションでは、3つの GenStage の役割を全て使います。「生産者」は、数を数えたり出力したりする責任があります。「生産者-消費者」は、偶数だけにフィルタリングし、下流から来る催促に応えます。最後に、数字を表示する「消費者」をつくります。 | ||
|
||
まずはスーパーバイザーツリーを持つ mix プロジェクトを作成しましょう。 | ||
|
||
```shell | ||
$ mix new genstage_example --sup | ||
$ cd genstage_example | ||
``` | ||
|
||
続いて `mix.exs` ファイルを開いて依存ライブラリに `gen_stage` を加えます。 | ||
|
||
```elixir | ||
defp deps do | ||
[ | ||
{:gen_stage, "~> 0.11"}, | ||
] | ||
end | ||
``` | ||
|
||
この先に進む前に、依存ライブラリを取得してコンパイルを通しておきましょう。 | ||
|
||
```shell | ||
$ mix do deps.get, compile | ||
``` | ||
|
||
生産者を構築する準備が整いました! | ||
|
||
## 生産者 | ||
|
||
GenStage アプリケーションの第一歩は、生産者の作成です。先ほど述べたように、一定の数のストリームを生成する生産者を作成する必要があります。という訳で早速、生産者のファイルを作成しましょう: | ||
|
||
```shell | ||
$ mkdir lib/genstage_example | ||
$ touch lib/genstage_example/producer.ex | ||
``` | ||
|
||
そして以下のコードを書き加えましょう: | ||
|
||
```elixir | ||
defmodule GenstageExample.Producer do | ||
use GenStage | ||
|
||
def start_link(initial \\ 0) do | ||
GenStage.start_link(__MODULE__, initial, name: __MODULE__) | ||
end | ||
|
||
def init(counter), do: {:producer, counter} | ||
|
||
def handle_demand(demand, state) do | ||
events = Enum.to_list(state..state + demand - 1) | ||
{:noreply, events, (state + demand)} | ||
end | ||
end | ||
``` | ||
|
||
ここで着目すべき2つの重要な個所が `init/1` と `handle_demand/2` です。GenServer のところでやったように `init/1` で初期状態をセットしますが、もっと大切なのは自分自身に「生産者」とラベル付けすることです。GenStage は `init/1` 関数からの戻り値で自身のプロセスを分類します。 | ||
|
||
`handle_demand/2` 関数は生産者の大部分がある場所ですが、この関数は全ての GenStage 生産者が実装しなければなりません。ここで消費者によって催促された数のセットを返し、カウンターを増やします。消費者からの催促は、上記のコードでは `demand` となっていますが、これは処理可能なイベントの数に対応する整数として表されています(デフォルト値は 1000 です)。 | ||
|
||
## 生産者-消費者 | ||
|
||
数を生成する生産者ができたので、今度は生産者-消費者に移りましょう。生産者に数を催促し、数をフィルターして偶数のみに絞り、消費者からの催促に応えたいと思います。 | ||
|
||
```shell | ||
$ touch lib/genstage_example/producer_consumer.ex | ||
``` | ||
|
||
以下のサンプルコードのようにファイルを更新しましょう: | ||
|
||
```elixir | ||
defmodule GenstageExample.ProducerConsumer do | ||
use GenStage | ||
|
||
require Integer | ||
|
||
def start_link do | ||
GenStage.start_link(__MODULE__, :state_doesnt_matter, name: __MODULE__) | ||
end | ||
|
||
def init(state) do | ||
{:producer_consumer, state, subscribe_to: [GenstageExample.Producer]} | ||
end | ||
|
||
def handle_events(events, _from, state) do | ||
numbers = | ||
events | ||
|> Enum.filter(&Integer.is_even/1) | ||
|
||
{:noreply, numbers, state} | ||
end | ||
end | ||
``` | ||
|
||
生産者-消費者に `init/1` の中の新しいオプションと新しい関数 `handle_events/3` を導入したことに気づかれたかもしれません。ここでは `subscribe_to` オプションを用いて、ある特定の生産者と通信することを GenStage に指示しています。 | ||
|
||
`handle_events/3` メソッドは、入ってくるイベントを受取って処理し変換した値の集合を返す場所です。ここからもわかるように、消費者はほとんど同じ方法で実装されますが、重要な違いは、`handle_events/3` メソッドの戻り値とその使用方法です。プロセスに producer_consumer とラベル付けすると、タプルの第2引数(ここでは `numbers`)が下流の消費者からの要求を満たすために使われます。消費者側ではこの値は破棄されます。 | ||
|
||
## 消費者 | ||
|
||
最後に消費者があります。でははじめましょう: | ||
|
||
```shell | ||
$ touch lib/genstage_example/consumer.ex | ||
``` | ||
|
||
消費者と生産者-消費者はとてもよく似ているので、さほど変わりありません: | ||
|
||
```elixir | ||
defmodule GenstageExample.Consumer do | ||
use GenStage | ||
|
||
def start_link do | ||
GenStage.start_link(__MODULE__, :state_doesnt_matter) | ||
end | ||
|
||
def init(state) do | ||
{:consumer, state, subscribe_to: [GenstageExample.ProducerConsumer]} | ||
end | ||
|
||
def handle_events(events, _from, state) do | ||
for event <- events do | ||
IO.inspect {self(), event, state} | ||
end | ||
|
||
# 消費者としては二度とイベントを出力しない | ||
{:noreply, [], state} | ||
end | ||
end | ||
``` | ||
|
||
前のセクションで学習したように、消費者はイベントを出力しないので、タプルの2番目の値は破棄されます。 | ||
|
||
## 全部まとめる | ||
|
||
さて、生産者、生産者-消費者、そして消費者が生まれた今、すべてを結びつける準備が整いました。 | ||
|
||
`lib/genstage_example.ex` ファイルを開いて、スーパーバイザーツリーにこれらの新しいプロセスを追加しましょう。 | ||
|
||
```elixir | ||
def start(_type, _args) do | ||
import Supervisor.Spec, warn: false | ||
|
||
children = [ | ||
worker(GenstageExample.Producer, [0]), | ||
worker(GenstageExample.ProducerConsumer, []), | ||
worker(GenstageExample.Consumer, []), | ||
] | ||
|
||
opts = [strategy: :one_for_one, name: GenstageExample.Supervisor] | ||
Supervisor.start_link(children, opts) | ||
end | ||
``` | ||
|
||
もし間違いが一つもなければ、このプロジェクトを実行すると全てが正しく動くはずです。 | ||
|
||
```shell | ||
$ mix run --no-halt | ||
{#PID<0.109.0>, 2, :state_doesnt_matter} | ||
{#PID<0.109.0>, 4, :state_doesnt_matter} | ||
{#PID<0.109.0>, 6, :state_doesnt_matter} | ||
... | ||
{#PID<0.109.0>, 229062, :state_doesnt_matter} | ||
{#PID<0.109.0>, 229064, :state_doesnt_matter} | ||
{#PID<0.109.0>, 229066, :state_doesnt_matter} | ||
``` | ||
|
||
できました! アプリケーションが偶数だけを出力するのを期待していましたが、こんなにも __素早く__ 出力しています。 | ||
|
||
ここに処理パイプラインがあります。生産者は数字を出力し、生産者-消費者は奇数を破棄して、そして消費者はそれらの数を表示するというフローを続けています。 | ||
|
||
## 複数の生産者もしくは消費者 | ||
|
||
イントロダクションで言及したように、1つ以上の生産者、または、消費者を持つことができます。少し見てみましょう。 | ||
|
||
サンプルから `IO.inspect/1` の出力を調べると、各々のイベントは単一の PID によって処理されていることがわかります。`lib/genstage_example.ex` を修正して複数のワーカーが動くように少し調整してみましょう。 | ||
|
||
```elixir | ||
children = [ | ||
worker(GenstageExample.Producer, [0]), | ||
worker(GenstageExample.ProducerConsumer, []), | ||
worker(GenstageExample.Consumer, [], id: 1), | ||
worker(GenstageExample.Consumer, [], id: 2), | ||
] | ||
``` | ||
|
||
ここで2つの消費者を設定してアプリケーションを実行するとどうなるか、見てみましょう。 | ||
|
||
```shell | ||
$ mix run --no-halt | ||
{#PID<0.120.0>, 2, :state_doesnt_matter} | ||
{#PID<0.121.0>, 4, :state_doesnt_matter} | ||
{#PID<0.120.0>, 6, :state_doesnt_matter} | ||
{#PID<0.120.0>, 8, :state_doesnt_matter} | ||
... | ||
{#PID<0.120.0>, 86478, :state_doesnt_matter} | ||
{#PID<0.121.0>, 87338, :state_doesnt_matter} | ||
{#PID<0.120.0>, 86480, :state_doesnt_matter} | ||
{#PID<0.120.0>, 86482, :state_doesnt_matter} | ||
``` | ||
|
||
見ての通り、単に一行のコードを付け足し、消費者の ID を与えただけで、複数の PID ができました。 | ||
|
||
## ユースケース | ||
|
||
さてこれまでに GenStage について学習し最初のサンプルアプリケーションを構築してきましたが、GenStage の __実際の__ ユースケースはどのようなものがあるのでしょうか。 | ||
|
||
+ データ変換パイプライン - 生産者は、単純な数値ジェネレータである必要はありません。データベースや、Apache Kafka のような別のデータソースからイベントを生成することもできます。生産者-消費者と消費者を組み合わせれば、それらが利用可能になった時に、メトリクスを処理したりソートしたりカタログ化したり保存したりすることができます。 | ||
|
||
+ 処理キュー - イベントはどんなものでもありえますが、一連の消費者によって実行される、まとまった処理を生成することができます。 | ||
|
||
+ イベント処理 - データパイプラインと同様に、ソースからリアルタイムで出力されたイベントに対し、受信し、処理し、並べ替えたり等の処理することができます。 | ||
|
||
これらは、GenStage で出来ることのほんの数例にすぎません。 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
--- | ||
version: 1.0.0 | ||
layout: page | ||
title: プロトコル | ||
category: advanced | ||
order: 12 | ||
lang: jp | ||
--- | ||
|
||
このレッスンではプロトコルがどんなものなのか、そして Elixir でどのように使うのかを見ていきます。 | ||
|
||
{% include toc.html %} | ||
|
||
## プロトコルとは何か | ||
プロトコルとは何でしょうか? | ||
プロトコルは Elixir においてポリモルフィズムを獲得する手段です。 | ||
Erlang の苦痛の一つは、新しく定義する型のために、既存の API を拡張していることです。 | ||
Elixir ではこれを避けるため、関数はその値の型に基いて、動的にディスパッチされます。 | ||
Elixir には数多くのビルトインのプロトコルがあり、例えば `String.Chars` プロトコルは以前使った `to_string/1` 関数を担当します。 | ||
簡単な例で `to_string/1` を更に詳しく見てみましょう。 | ||
|
||
```elixir | ||
iex> to_string(5) | ||
"5" | ||
iex> to_string(12.4) | ||
"12.4" | ||
iex> to_string("foo") | ||
"foo" | ||
``` | ||
|
||
ご覧の通り、この関数を複数の型で呼び出し、どの型でも動くことが示されています。 | ||
`to_string/1` をタプル(もしくは `String.Chars` を実装していない何らかの型)で呼び出すとどうなるでしょうか? | ||
見てみましょう: | ||
|
||
```elixir | ||
to_string({:foo}) | ||
** (Protocol.UndefinedError) protocol String.Chars not implemented for {:foo} | ||
(elixir) lib/string/chars.ex:3: String.Chars.impl_for!/1 | ||
(elixir) lib/string/chars.ex:17: String.Chars.to_string/1 | ||
``` | ||
|
||
ご覧の通り、タプルには実装が無いのでプロトコル・エラーが発生しました。 | ||
次のセクションでは、タプルに `String.Chars` プロトコルを実装します。 | ||
|
||
## プロトコルを実装する | ||
|
||
タプルには `to_string/1` はまだ実装されていないことが分かっていますので、追加しましょう。 | ||
実装を行うには `defimpl` でプロトコルを指定し、`:for` オプションで型を指定します。 | ||
実際どんな風になるのか見てみましょう。 | ||
|
||
```elixir | ||
defimpl String.Chars, for: Tuple do | ||
def to_string(tuple) do | ||
interior = | ||
tuple | ||
|> Tuple.to_list | ||
|> Enum.map(&Kernel.to_string/1) | ||
|> Enum.join(", ") | ||
|
||
"{#{interior}}" | ||
end | ||
end | ||
``` | ||
|
||
IEx にこれをコピーして流し込めば、今度はエラーを出さずタプルに対して `to_string/1` を呼び出すことが出来るはずです。 | ||
|
||
```elixir | ||
iex> to_string({3.14, "apple", :pie}) | ||
"{3.14, apple, pie}" | ||
``` | ||
|
||
どうやってプロトコルの実装を行えばいいのかはわかりましたが、では、新しいプロトコルを定義するにはどうしたら良いのでしょうか? | ||
この例では `to_atom/1` を実装してみます。 | ||
`defprotocol` を用いる方法を見ていきましょう。 | ||
|
||
```elixir | ||
defprotocol AsAtom do | ||
def to_atom(data) | ||
end | ||
|
||
defimpl AsAtom, for: Atom do | ||
def to_atom(atom), do: atom | ||
end | ||
|
||
defimpl AsAtom, for: BitString do | ||
defdelegate to_atom(string), to: String | ||
end | ||
|
||
defimpl AsAtom, for: List do | ||
defdelegate to_atom(list), to: List | ||
end | ||
|
||
defimpl AsAtom, for: Map do | ||
def to_atom(map), do: List.first(Map.keys(map)) | ||
end | ||
``` | ||
|
||
さてプロトコルを定義しましたが、このプロトコルではいくつかの型に対する実装で `to_atom/1` メソッドが要求されます。 | ||
それではプロトコルができたので、IEx で使ってみましょう。 | ||
|
||
```elixir | ||
iex> import AsAtom | ||
AsAtom | ||
iex> to_atom("string") | ||
:string | ||
iex> to_atom(:an_atom) | ||
:an_atom | ||
iex> to_atom([1, 2]) | ||
:"\x01\x02" | ||
iex> to_atom(%{foo: "bar"}) | ||
:foo | ||
``` | ||
|
||
特筆すべきは、構造体の内部には Map があるものの、構造体はプロトコルの実装を Map と共有していないという点です。それらは列挙可能ではなく、それらにアクセスもできません。 | ||
|
||
以上でおわかりのように、プロトコルはポリモルフィズムを獲得するための強力な手段です。 |