やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

【Elixirのサンプルコード有り】条件に一致した時に再帰を停止する方法4つを書き比べてみた

なんでbreak_reduceみたいなのはないんだろう

配列(リスト)に対して、各要素を精査する際に、特定の条件にマッチする要素が見つかった時点で処理を停止して、Elixirであれば{:exist, value}のような値を返す関数をよく作成することがある。頻出の処理パターンではあるが、ElixirEnumを覗いてみても該当する関数は見当たらない。

(ありそうでない)

# ※これは架空の関数です
Enum.break([1,2,3,4,5], {:ok, 0}, fn n, _ -> n == 3 end)

自分は関数プログラミングの専門家ではないが、これは関数プログラミングがデータの変換を行うことを行うことを重視しており、reduceという関数が、列挙可能な値を全て確認した上でデータ変換を行うという性質であると考えれば、reduce関数の内部でbreakをするというのはイケてないことをしようとしているのではないかと考えることが出来る。
合わせてEnumは列挙可能なデータ構造(enumerables)に対して、処理を適応させるような関数をまとめたモジュールなため、途中で処理が停止して、残りの要素には何もしないという実行方法はミスマッチなんだろうか。

... という仮説を元に、indexを指定して、その値を取得するEnum.atの実装を見てみた。どうやら普通に再帰関数を使って停止させているようだ。仮説は誤っていたのだろうか。

defmodule Enum do
  def at(enumerable, index, default \\ nil) when is_integer(index) do
    case slice_any(enumerable, index, 1) do
      [value] -> value
      [] -> default
    end
  end

  defp slice_any(list, start, amount) when is_list(list) do
    list |> drop_list(start) |> take_list(amount)
  end

  defp drop_list(list, 0), do: list
  defp drop_list([_ | tail], counter), do: drop_list(tail, counter - 1)
  defp drop_list([], _), do: []
end

eg: list = [1, 2, 3, 4, 5], index = 2

-> Enum.at(list, index)
  -> slice_any([1, 2, 3, 4, 5], 2, 1)
    args = [1 | 2, 3, 4, 5], 2
    -> next fn([2, 3, 4, 5], 1)

    args = [2 | 3, 4, 5], 1
    -> next fn([3, 4, 5], 0)

    return [3, 4, 5]

  -> drop_list([3, 4, 5], 1)
    args = [3 | 4, 5], 1
    return [3]

  return [3]

# 最終的なreturn
3

Elixirを使っている立場から言えることは、「頻出の処理なのでEnumに実装して頂ければ有難いのですが....」ということ。
以下に同じ処理をいくつかの記述方法で実装したサンプルを載せているが、結果的にEnum.reduce関数を使わずとも、再帰関数を記述すれば、上記の処理をbreakする形式で記述することは出来る。要は面倒か、面倒ではないかのみ。

自分は一年程前に「関数型言語って副作用ないし、かっこいいし、流行ってる...」という理由で始めた不届き者です。今回の仮説と記述を通して、今更ながら「関数型言語とはどういうものなのか?」と改めて考える機会になり、非常に勉強になった。

qiita.com

ロックの例えが分かりやすかった。ロックとは何かを定義するよりも、ロックをたくさん聞いた方が早いというのはなるほどなぁと感じた。

サンプル

mapにマイナスの値を持つkeyが1つでも存在すれば初登場のkvのセットを返すという処理を例に示す。実際にこの処理は以下の記事で過去に記述した処理を採用している。

この記事を書いた時は、再帰を使うのか...Enum.reduceを使うのか...とかなり悩んだ。

www.okb-shelf.work

本来はパフォーマンスの比較のために複数のkeyの数が異なるデータを用意するべきだが、記事が長くなりすぎてしまうため、今回は割愛。シンプルに5つのkey(A, B, C, D, E)を持つmapを用意して4種類の記述方法を試してみる。

data = %{
    "A" => 1,
    "B" => 1,
    "C" => -1,
    "D" => 1,
    "E" => -1
}

条件として、mapにマイナスの値が存在しており、確認した際は{:exist, %{"C" => -1}}のような該当したデータを返し、存在しないのであれば{:ok, %{}}を返すようにする。

Enum.reduce編

普通にreduce使って書く。

Enum.reduce(data, {:ok, %{}}, fn {k, v}, acc -> 
    {_, acc_map} = acc
    if v < 0 do
        if Enum.count(acc_map) == 0 do
            {:exist, Map.put(acc_map, k, v)}
        else
            acc
        end
    else
        acc
    end
end)
|> IO.inspect()

# {:exist, %{"C" => -1}}

ちゃんとif else endを使用して返す値を明示的に記述してあげないと、nilが返ってしまうため、思わぬ結果となってしまうので注意。個人的にはインデントとコード行数が複雑になるので、簡潔な記述とは言えないのが残念。

再帰関数編

いつも自分が採用する形式。なぜ再帰関数を使っているかというと、条件に一致した時に再帰を停止させることが出来るため。普段使いするgolangpythonでいうところの以下の処理に該当するのでデータ数が大きくなればなるほど、Enum.reduceで記述したものとはパフォーマンスに差が現れてくるだろう。計算量自体は最悪時を考えるため、一緒になるが(O(N), N=mapのkeyの数)。

package main
import "fmt"
func main(){
    // Your code here!
    
    num := 3
    for i := 0; i< 10; i++ {
        if i == num {
            fmt.Println("hit: ", i)
            break
        }
    }
}
defmodule Sample do
    def reduce_break(map) do
        _reduce_break(Map.keys(map), map, {:ok, %{}})
    end
    defp _reduce_break([], _, acc), do: acc
    defp _reduce_break([head | tail], map, acc) do
        {_, acc_map} = acc
        if Map.get(map, head) < 0 do
            {:exist, Map.put(acc_map, head, Map.get(map, head))}
        else
            _reduce_break(tail, map, acc)
        end
    end
end

Sample.reduce_break(data)
|> IO.inspect()

# {:exist, %{"C" => -1}}

モジュール使ったので若干、コード量は増えてはいるが見通しの良さは引数パターンマッチのおかげだ。先ほども記述したように、条件に一致した際に、再帰を終了して値を返して関数の実行を終了する。

if Map.get(map, head) < 0 do
    # return value
    {:exist, Map.put(acc_map, head, Map.get(map, head))}
else
    # recursive: call own
    _reduce_break(tail, map, acc)
end

Enum.filter + Enum.reduce編

最近、意識するようになった記述方法でほぼ全ての処理をEnumとパイプラインで記述することが可能なため、「Elixirを書いてるんだ〜」という熱い思いを感じることが出来る。

filtered = Enum.filter(data, fn {k, v} -> 
    if v < 0 do
        {k, v}
    end
end)

if length(filtered) > 0 do
    Enum.at(filtered, 0)
    |> (fn {k, v} -> {:exist, Map.put(%{}, k, v)} end).()
    |> IO.inspect()
end

# {:exist, %{"C" => -1}} 

書き終えてから気づいたが、reduce使っていない。あと、パイプラインで無名関数使うのはスタイルガイド的にはnot preferredなので、乱用しないこと。

無限リストと遅延評価を使った(Stream.transform)実装

今回の記事を書く前まで無限リストと遅延評価について、全く知らない状態だったが、記事をまとめる中で完全に理解して、チョットワカル状態になった。 こちらの実装方法は普段、大変お世話になっているpiacereさんから教えて頂いたものだ。

執筆されている、こちらの記事も非常に分かりやすかったです。

qiita.com

Stream.transform(data, false, fn {k, v}, judge ->
  if judge do
    {:halt, judge}
  else
    if v < 0 do
        {[{:exist, Map.put(%{}, k, v)}], true}
    else
        {[], judge}
    end
  end
end)
|> Enum.to_list()
|> (fn lst -> 
        if length(lst) > 0 do
            Enum.at(lst, 0)
        else
            {:ok, %{}}
        end
    end).()
|> IO.inspect()

# {:exist, %{"C" => -1}}

注意点としては、Stream.transformは少し癖があり、条件に一致しない時は要素を2つ持つタプルを返す必要があり、第1要素にはリストを。第2要素には次の再帰で使用したアキュムレーターの値を渡す。最終的な戻り値は{:halt, _}を返す直前の条件に一致しなかった際に記述してきた{[], judge}の第1要素になる(これで少し迷った)。

無限ストリームを使用しているため、必要な要素分だけ判定されるため、再帰を用いてbreakをした時と同様のパフォーマンス(データ生成のパフォーマンスは考えない)になるのではないかと考えられる。

結論

パフォーマンスを意識するのなら再帰関数かStream.transformを使うのが良くて、Enumとパイプラインの強力なコンビネーションを使いたいのであれば、Enum.filter -> Enum.reduceを使うのが良いのではないだろうか。

個人的な推しは記述の楽さからEnum.filter -> Enum.reduceです。データ数が増えてきたら再帰に実装し直せばいいかなと思います。

参考文献

【実装コード有り】銀行家のアルゴリズムの実装と検証

今更作った理由

たまたまgoogle scholarで並行処理に関する資料を眺めていたらダイクストラ法で有名なエドガー・ダイクストラさんの名前を見かけた。自分の中では数学者という認識をしていたが、実は分散コンピューティングの分野で、どのようにシステムの信頼性を保証するかという手法を最初に提唱したこともある、バリバリのコンピューター科学の精通者だった。当時、go to構文を用いてプログラミングを行なっていた従来の手法を改めて、現在、スタンダートになり、多くの人が使用するif文などを提唱した人物らしく驚いた。

ダイクストラさんに興味を持ち、彼の文献を漁っていたところ「銀行家のアルゴリズム」たるものを発見。デットロックを回避するためのアルゴリズムであり、自分の興味関心のある並行処理の分野に通ずるものがあったため、試しに実装しようと思った。

銀行家のアルゴリズムについて

詳しいことは参考にしたwikipediaの記事を参照。簡単にだけまとめておく。

  1. 全体の資源を保持しているメインプロセスは倉庫を持っており、大量のN種類の備品が用意されている。
利用できる資源:  
A B C D
3 1 1 2
  1. それぞれ、合計Pの各企業に試供品してメインプロセスの倉庫からN種類の備品がそれぞれいくつかレンタルの形で届く。
プロセス(および現在割り当て済みの資源):
   A B C D
P1 1 2 2 1
P2 1 0 3 3
P3 1 1 1 0
  1. あまりにも、試供品が素晴らしかったため、全ての企業から備品に関する問い合わせが届いた。メインプロセスには担当者が一人しかいないため、順番に問い合わせをさばいていく。

  2. P1社から問い合わせられた備品が倉庫に、それぞれ数があるかを確認してレンタル可能であれば、追加で備品を貸し出す。なお、各企業の契約状況によって貸し出せる備品の数は決まっている。

P1 がさらに Aを2、Bを1、Dを1 の資源を獲得し、最大値に到達する
- システムは A1、B0、C1、D1の資源を利用可能である
  1. P1社のレンタル期間が満了して、メインプロセスからレンタルしていた資源を返却する。
P1 終了し、Aを3 Bを3, Cを2、Dを2返却する
- システムはこの時点でA4、B3、C3、D3の資源を利用できる

この3 ~ 5の過程をPの分だけ繰り返す。もし問題なく全ての企業に備品を貸し出すことが出来たのであれば安全であると判断することが出来、どこかで備品が不足した際には安全ではないと判断する。

だいたい、こんな感じ

実装方法

もちろんElixirを使う。今回はElixir(Erlang/OTP)に用意されたGenServerを使ってmessage passingをサボってみることにする。

wikipediaにあるアルゴリズム擬似コードにあるP - プロセスの集合を真に受け取って、プロセスを1つ1つ立ち上げて、state serverに問い合わせるのが、インタリーブも複雑になり面倒だったので、プロセスは都度、立ち上げずに1つのプロセスから逐次依頼する形で実装する。
こうすればプロセスの処理の実行->終了->資源の解放の一連の処理を再現することも出来る。従って、wikipediaにあるforeach (p ∈ P)に近い形での実装になる。 おそらく、実務の場合では、複数のプロセスが逐次的ではなく並列的にリクエストを飛ばしてくるのでもっと複雑なものになる。今回は簡単のために上記を採用した。

実行したコード

全体像はgithubにpushしているのでこちらからどうぞ。

github.com

補足的にコードの説明をしておきます。

GenServerに用意したもの

銀行家のアルゴリズムを実装するにあたり、3つのAPIを用意した。どれも同期実行のhandle_callを使用している。(非同期はhandle_cast)

  • :init
  • :request
  • :state
  • :return

init

:initが使用された時に、GenServerにあるstateからリクエストにあった分だけを引き、次のstateとして使用する。

def handle_call({:init, %{"A" => _, "B" => _, "C" => _, "D" => _} = max_resource}, _from, state) do
  reply = Map.merge(state, max_resource, fn _, v1, v2 -> v1 - v2 end)
  {:reply, max_resource, reply}
end

今回は簡単のため、リクエストにはA, B, C, D以外のkeyのリクエストは受け付けないようにパターンマッチを使用している。

request

やっていることはほとんど:initと同じ。1つだけ異なるのは、差分更新をする際に値がマイナスになってしまったkeyが存在しているかを確認している。マイナスになるkeyがあるということは資源が不足している事を意味しており、この時にGenServer:unsafeを返す。

def handle_call({:request, %{"A" => _, "B" => _, "C" => _, "D" => _} = request}, _from, state) do
  reply = Map.merge(state, request, fn _, v1, v2 -> v1 - v2 end)
  {:reply, is_unsafe(Map.keys(state), reply), reply}
end

マイナスのkeyが存在しているかをstatekeysを元に再帰的に判定。

defp is_unsafe([], reply), do: {:ok, reply}
defp is_unsafe([head | tail], reply) do
  if Map.get(reply, head) >= 0 do
    is_unsafe(tail, reply)
  else
    {:unsafe, reply}
  end
end

state

現状のstateを返すだけで特にstateの更新は何もしていない。

def handle_call({:state}, _from, state) do
  {:reply, state, state}
end

return

プロセスの処理が終了した時に、貸し出していた資源を回収するためのもの。受け取った値をkey毎にstateに加えて、次のstateとする。

def handle_call({:return, %{"A" => _, "B" => _, "C" => _, "D" => _} = return}, _from, state) do
  merge_state = Map.merge(state, return, fn _, v1, v2 -> v1 + v2 end)
  {:reply, merge_state, merge_state}
end

BankersAlgo

ここからGenServerに対して、都度リクエストを飛ばして、資源の取得、更新を行う。処理は逐次処理にて実行して、順番はwikipediaにあった通りに作っただけ。ややこしいことをしている部分としては、一連の処理をシナリオとしてmapのデータ構造として定義しており、そのmapからイベントを実行していく。

呼び出されるのは以下の関数で、値の取得、GenServerの起動、シナリオの読み込みを行なっている。

def main(scenario_map) do
  # データ構造よりkeyを元に値を取得
  {init_state, init_each_resource, scenario} = get_values(scenario_map)
  # GenServerを起動
  {:ok, pid} = GenServer.start_link(Server, init_state)
  # Cpを再現するために、初回の資源割り振りを依頼
  Enum.each(init_each_resource, fn {_, req} -> GenServer.call(pid, {:init, req}) end)
  # 'scenario'(配列)を1つずつ読み込み、資源が不足しているかを確認する
  genserver_request(scenario, init_each_resource, pid)
end
defp genserver_request([], _init, _pid), do: :ok
defp genserver_request([scenario | tail], init_each_resource, pid) do
  # シナリオにある'process'の値から、初期資源の量の値を取得する
  {process, init} = get_init_resource(scenario, init_each_resource)
  # シナリオに定義された最大資源の値を取得
  max = Map.get(scenario, "request")
  # リクエスト = 最大資源 - 初期資源
  request = Map.merge(max, init, fn _k, v1, v2 -> v1 - v2 end)
  case GenServer.call(pid, {:request, request}) do
    {:ok, _} ->
      # 持っている資源をGenServerに返す
      GenServer.call(pid, {:return, max})
      # 初期資源をGenServerに返したとして値をクリアする
      updated_init = update_init_resource(init_each_resource, process)
      genserver_request(tail, updated_init, pid)
    {:unsafe, _} -> :unsafe
  end
end

検証の実行

testファイルにサンプルがあります。そのテストを実行することで値の変遷を確認することが出来る。安全なケースと安全ではないケースを1つずつ用意してある。

$ mix test

Compiling 1 file (.ex)

10:51:09.540 [info]  Set init state: %{"A" => 6, "B" => 4, "C" => 7, "D" => 6}

10:51:09.542 [info]  Available resource: %{"A" => 3, "B" => 0, "C" => 1, "D" => 2}

10:51:09.542 [info]  ---> process request: P1

10:51:09.542 [info]  Received request: %{"A" => 2, "B" => 1, "C" => 0, "D" => 1}

10:51:09.542 [info]  Updated sever state: %{"A" => 1, "B" => -1, "C" => 1, "D" => 1}
.
10:51:10.543 [info]  Set init state: %{"A" => 6, "B" => 4, "C" => 7, "D" => 6}

10:51:10.543 [info]  Available resource: %{"A" => 3, "B" => 1, "C" => 1, "D" => 2}

10:51:10.543 [info]  ---> process request: P1

10:51:10.543 [info]  Received request: %{"A" => 2, "B" => 1, "C" => 0, "D" => 1}

10:51:10.543 [info]  Updated sever state: %{"A" => 1, "B" => 0, "C" => 1, "D" => 1}

10:51:10.543 [info]  Return resource: %{"A" => 3, "B" => 3, "C" => 2, "D" => 2}

10:51:10.543 [info]  Received resource and merge: %{"A" => 4, "B" => 3, "C" => 3, "D" => 3}

10:51:10.543 [info]  ---> process request: P2

10:51:10.543 [info]  Received request: %{"A" => 0, "B" => 2, "C" => 0, "D" => 1}

10:51:10.543 [info]  Updated sever state: %{"A" => 4, "B" => 1, "C" => 3, "D" => 2}

10:51:10.543 [info]  Return resource: %{"A" => 1, "B" => 2, "C" => 3, "D" => 4}

10:51:10.544 [info]  Received resource and merge: %{"A" => 5, "B" => 3, "C" => 6, "D" => 6}

10:51:10.544 [info]  ---> process request: P3

10:51:10.544 [info]  Received request: %{"A" => 0, "B" => 0, "C" => 4, "D" => 0}

10:51:10.544 [info]  Updated sever state: %{"A" => 5, "B" => 3, "C" => 2, "D" => 6}

10:51:10.544 [info]  Return resource: %{"A" => 1, "B" => 1, "C" => 5, "D" => 0}

10:51:10.544 [info]  Received resource and merge: %{"A" => 6, "B" => 4, "C" => 7, "D" => 6}
.

Finished in 2.1 seconds
2 tests, 0 failures

Randomized with seed 378761

やりたいことはできたっぽい。

参考文献

【Elixir/supervisor入門】通常のモジュールに対してsupervisorを定義する

なぜ書いたのか

Elixirにおいてプロセスの死活管理に用いるsupervisorについて学習を進めていたが、どれもGenServer、すなわちElixirbehaviorを用いたサンプルばかりだった。また、そのプロジェクトはmix new --sup project_nameとして生成されるもので、後から「supervisor入れたい」となる事が割とあったが自前でmixプロジェクトにsupervisorを定義する方法についてよく分かっていなかった。

気軽に自身で作成したElixirbehaviorを用いないモジュールに対してsupervisorを定義するための手順がまとまった日本語記事が確認出来なかったので自身のためにこの記事を手順のまとめとして作成した。

今回扱うサンプルについて

プロセスの起動時に特定のメッセージをメインのプロセスに指定回数だけ送信し、上限に達した際にプロセスをkillするという簡単なものを扱う。せっかくなので、message passingが扱えるものを採用してみた。参考にした記事ではアキュムレーターをデクリメントしてカウントが0になったら終了するというシンプルなものだったので少し拡張させた。

medium.com

プロセスの関係性は以下のようになる。
f:id:takamizawa46:20200418124309p:plain:w550

プロジェクトの作成

コードの全体はこちら。
github.com

今回はmixを使わずに、プレーンに作っていくため、まずは適当にディレクトリを作る。

$ mkdir supervisor_sample
$ cd supervisor_sample

次にexファイルを用意。ファイル分割を考えたが手間になるため、今回は1ファイルに全てを記述する。

$ touch supervisor_sample/supervisor_sample.ex

各プロセスにて実行するモジュール内関数

関数名はsupervisorの公式のサンプルにあるような形式に乗っ取ってそれぞれ、start_linkinitとしておくが現在の推奨バージョンでは、特に関数名の縛りはないため自由な命名をすることが出来る。参考にした記事の実装は現在は非推奨 となっているため、一部を書き換えている。

defmodule Child do
  @moduledoc """
    supervisorから起動されるタスクを実行するプロセス
    上限回数まで親プロセスにメッセージを送信して、上限に達したら終了してkillされる
  """

  @doc """
    supervisorから実行される関数。
    supervisorからlinkされたプロセスの生成を行う
  """
  def start_link(receiver, max_send) do
    # launch process link to supervisor process
    pid = spawn_link(__MODULE__, :init, [receiver, max_send])
    {:ok, pid}
  end

  def init(receiver, max_send) do
    # set seed for each process
    IO.puts("Start child with pid #{inspect(self())}")
    Process.sleep(200)
  end
end

これでsupervisorからこのモジュールが呼び出されてタスクを実行するプロセスが立ち上がるようになった。spawn_linkを使っているのはプロセスが死んだことをsupervisorに通知するため。
しかし、このままではプロセスが立ち上がって何もせずに終了してしまうため、initに親プロセスに対してメッセージを送信するための処理を追加する。指定回数まで実行させたいので新たな関数を定義する。

defmodule Child do
  def init(receiver, max_send) do
    # set seed for each process
    IO.puts("Start child with pid #{inspect(self())}")
    Process.sleep(200)
    sender(receiver, max_send)
  end

  @doc """
    上限回数まで再帰的にメッセージを親プロセスに送信する関数
  """
  def sender(_, 0), do: :ok
  def sender(receiver, max_send) do
    send(receiver, {:MESSAGE, "hello! from #{inspect(self())}"})
    sender(receiver, max_send-1)
  end
end

supervisorの起動時に受け取った親プロセスのPIDを元に{:MESSAGE, *本文}を送信する。再帰的に実行し、アキュムレーターの値が0になった時点で再帰を終了する。これでプロセスに実行させる処理の記述が完了した。

supervisorの定義

次に呼び出す元となる親プロセス、すなわちsupervisorの定義をする。ただ単にsupervisorbehaviorに従って実装しただけになる。

defmodule Parent do
  @moduledoc """
    supervisorの定義モジュール
  """
  use Supervisor

  @doc """
    supervisorの起動
  """
  def start_link(receiver, total_process) do
    Supervisor.start_link(__MODULE__, {receiver, total_process}, name: __MODULE__)
  end

  @doc """
    supervisorの設定と戦略をまとめた関数
  """
  def init({receiver, total_process}) do
    children = Enum.map(1..total_process, fn n ->
      %{
        id: "#{__MODULE__}_#{n}",
        # Childのstart_link関数に引数を渡して呼び出す。
        start: {Child, :start_link, [receiver, total_process]},
        restart: :permanent
      }
    end)

    # Aプロセスが死んだ時にAプロセスを復活させる -> 上限は10回(全プロセスで合算)
    Supervisor.init(children, strategy: :one_for_one, max_restarts: 10)
  end
end

実装が必要なのはstart_linkinit命名された2つの関数。start_linksupervisorの起動のための関数で、initsupervisorの設定、戦略をまとめた関数になる。start_linkに関しては特に手を加える部分はないが、第2引数に値を設定することでinit関数に引数を渡すことが可能となる。

init関数でsupervisorで管理するプロセスの設定をしている。設定を定義したmapを要素に持つリストを作成するか、Supervisor.child_spec/2という関数を用いる。どんな設定が出来るのかは下記に記述されている。特に重要なのはstart:の部分で、ここにsupervisorから起動させたいモジュールと関数名、引数値を指定する。TaskAgentで指定する形式と同じなので特に困惑する部分はない。

Supervisor.initではsupervisorの戦略を定義する。今回は特殊な設定はしておらず、死んだプロセスを1つ1つ都度、復活させる*one_for_one、最大復活プロセス数は10とした。

これにて実行するプロセスとsupervisorの設定が可能となった。

実行ファイル

最後に作成したsupervisorを気軽に呼び出すため、送信されてきたメッセージを受信するための再帰receiveを実装したwrapperモジュールを用意する。

defmodule SupervisorSample do
  @moduledoc """
    supervisorの呼び出しとメッセージの受信をサボるためのwrapperモジュール
  """

  @doc """
    supervisorの起動と再帰的メッセージ受信ループを実行
  """
  def launch(total_process) do
    # launch supervisor
    Parent.start_link(self(), total_process)
    receiver()
  end

  def receiver() do
    receive do
      {:MESSAGE, content} ->
        IO.puts("received message!: #{inspect(content)}")
        receiver()
    end
  end
end

実行結果

iexを立ち上げて、.exファイルをコンパイルして実行する。

$ iex

iex(1)> c("supervisor_sample.ex")  
[Child, Parent, SupervisorSample]  

今回は簡単のために立ち上げるプロセスは2つとして実行する。実行すると以下のようなlogが出力される。

iex(2)> SupervisorSample.launch(2)
Start child with pid #PID<0.116.0>
Start child with pid #PID<0.117.0>
Start child with pid #PID<0.118.0>
Start child with pid #PID<0.119.0>
received message!: "hello! from #PID<0.116.0>"
received message!: "hello! from #PID<0.116.0>"
received message!: "hello! from #PID<0.117.0>"
received message!: "hello! from #PID<0.117.0>"
received message!: "hello! from #PID<0.119.0>"
received message!: "hello! from #PID<0.119.0>"
Start child with pid #PID<0.120.0>
Start child with pid #PID<0.121.0>
received message!: "hello! from #PID<0.118.0>"
received message!: "hello! from #PID<0.118.0>"
Start child with pid #PID<0.122.0>
Start child with pid #PID<0.123.0>
received message!: "hello! from #PID<0.120.0>"
received message!: "hello! from #PID<0.120.0>"
received message!: "hello! from #PID<0.121.0>"
received message!: "hello! from #PID<0.121.0>"
received message!: "hello! from #PID<0.122.0>"
Start child with pid #PID<0.124.0>
Start child with pid #PID<0.125.0>
received message!: "hello! from #PID<0.122.0>"
received message!: "hello! from #PID<0.123.0>"
received message!: "hello! from #PID<0.123.0>"
received message!: "hello! from #PID<0.124.0>"
Start child with pid #PID<0.126.0>
Start child with pid #PID<0.127.0>
received message!: "hello! from #PID<0.124.0>"
received message!: "hello! from #PID<0.125.0>"
received message!: "hello! from #PID<0.125.0>"
received message!: "hello! from #PID<0.126.0>"
** (EXIT from #PID<0.104.0>) shell process exited with reason: shutdown

こいつをElixirで整形して何回プロセスが再起動されているかを確認してみると12となっている。

log =
"""
Start child with pid #PID<0.116.0>
:
:
received message!: "hello! from #PID<0.126.0>"
"""

log
|> String.split("\n")
|> Enum.filter(fn s -> String.starts_with?(s, "Start child with") end)
|> length()
|> IO.puts() # 12

これは最初に起動される2つのプロセスは当然ながら再起動のカウントに含まれていないということを意味している。どうやら上手くsupervisorが動作しているようだ。

github.com

参考文献

【収益ほぼゼロ】何の考えもなしにブログ100記事を書き終えたので反省会をしました

100記事を書くことが凄いのか

f:id:takamizawa46:20200413225135p:plain
よく著名なブロガーが「まずは100記事書くといいですよ」ということを仰っている。なぜ100件なのかという根拠はないが、意図としては100も記事を書けばブログを書く習慣も身に付き、言葉の扱い方も多少は様になってくる...ということだと考えられる。あとはとりあえず始めることが重要なんだとか。

正確な数字は確認出来なかったが、ブログを始めて100記事の執筆に到達する前に全体の90%近くは挫折して、継続不可能になるのだそう。いわゆるブログは無理ゲーと言われる所以。そう言われてみると、自分が達成したブログ100記事の執筆は凄いことなのかもしれない。
そういえば、プログラミングの独学の継続率も確かそのぐらいの数字だった気がする。自分は独学で学習をして本業にするところまで辿り着けたが、当時、自分の周りでプログラミングをやっていた人は確かに全滅した。自分が唯一のサバイバーだった(気がする)。

そんなこともあり、ブログのジャンルとしては本業のエンジニアリングに関連する技術や、試した事を中心にたまにレビューなんかや、所感についてまとめたものを執筆してきた。

なぜ100記事書けたのか

今まさにブログを始めたばかりでブログをこれから書き進めていく方や、一度ブログを挫折した方に自分がなぜ100記事も書けたのかをより詳細に話しておこうと思う。

おそらく、この記事にたどり着いた方がの多くは「ブログを書いて稼ぎたい」という考えの人が多いと思う。自分ももちろん「稼げるに越したことはない」と思っているし、ハッキリ言って稼ぎたい。そりゃもちろん。

しかし、ブログを始めた当時のマインドと100記事を執筆し終わった今に至るマインドの多くは

  • 文章を書くのが好き
  • 技術のインプットが好き

というもの。収益のために書いたという記事はほんの数記事しかない。

実は過去にブログを挫折済み

偉そうに「100記事も執筆してやったぜ」と豪語しているものの、実はブログに関しては過去に一度、挫折をしている。それは大学の2年生の時で当時は「ガリガリ男が筋トレする」というコンセプトでブログをWordPressを使って運営していた。

www.okb-shelf.work

ブログを立ち上げた当初は順調で、毎日のように書く記事が思いついた。20記事程度、執筆したところでGoogle Adsenseの審査が通り、ブログ内に広告の配置をすることも出来た。その後、ゆるやかに執筆頻度が落ちていき50記事に到達する前に、あえなく挫折してしまった。

なぜ挫折したのかという理由の詳細は上の記事でも話してるが改めて、100記事を執筆し終わった今思うこととしては以下の3つが良くなかったと思う。

  • 筋トレという知識がほとんどない分野を攻めた
  • 筋トレの効果が現れるまでにかなり時間があるため、記事にしにくい
  • 稼ぎたいという欲が先行して作業的になってしまった

一言で言うとジャンル選びとモチベーションのキープが非常に重要。書き続けても苦にならない、毎日情報収集したくなるようなジャンルを選ぶ事をオススメしたい。

とりあえず書きたいことをひたすら書いた

色々と言われているが、ただ記事を執筆してもダメでユーザーが必要としているテーマを選定して、Googleの検索ページの1ページに表示されるように努めるというのが、いわゆる良い記事のことだろう。90記事を書くあたりで、上記のようなことを意識をし始めるようにはなったが、それまでは何の考えもなしにひたすらに書きたい記事を書きたい時に書き続けていた。

(ただ書くだけではダメだと気づいた記事)

www.okb-shelf.work

気づけば、需要があるのかないのかよく分からないテーマの記事が大量に出来上がった。自分にとっては学習のアウトプットになっているし、好きなテーマで書けているので幸福度が非常に高い。振り返ってみると自分の書いた記事はかなりニッチを攻めている。日本語で同じような記事を書いている人は恐らくいない。

www.okb-shelf.work

www.okb-shelf.work

100記事書いて得られた知見・能力

100記事の執筆が終わった時点で個人的に得られたと思う知見について4つほど共有しておこうと思う。

文章を書くことへの習慣

一番、大きいと感じるのはこれ。中々、普段これだけの文量のデータを作ることはないが、ブログを書いていれば否が応でも文量は多くなる。結果的に書いた文量というのは一般的な人と比べるとかなり多いと思う。

文量が多くなれば、ボキャブラリーも増えるし、より良い言葉を選ぶようにもなってくる。何よりも恩恵を感じるのは業務でのチャットコミュニケーション。他の人たちの文章が稚拙とは言わないが、明らかに自分の文章とレベルが違うと感じる。これはブログを習慣的に書き始めてから気づくようになった。まぁ、自分の文章も別に上手いとは思わないが、少なくとも素人では無くなった。

技術に関する知識

自分にとってブログを書くことはイコール、技術のアウトプットになっている。新しく覚えたことを抽象化して記事にまとめる、この作業の繰り返し。つまり記事数が多いと言うことはこのルーティーンを多く行ってきたということになる。

ブログを始めた当初と比べると技術的な知識もかなり増えたと感じる。濃い一年を過ごすことが出来た。合わせて、ブログがポートフォリオとなっているし、転職する際や自己紹介をする際にもブログのリンクを貼っておけば、何をやってきて何に興味があるのかを言葉ではなくデータで提示することが出来る。

エンジニアがブログなりアウトプットをやらないのは非常にもったいない。文章が下手だろうが、コードに自信がなくても続けていればマシになってくるのでくすぶっているなら絶対にやったほうがいい。

Rubyの作者である「まつもとゆきひろ」さんもエンジニアのアウトプットには言及している。

インプットは必要、でも差別化要因にならない
しかし、アウトプットすることで差別化になる

引用
qiita.com

PVを伸ばしたいならただ書くだけではダメ

ただ書いていてはテーマが良くない限りはPVは絶対に増えていかない。PVが増えないということは一般的に言えば、収益も発生しにくい。ブログで収益を得たいと考えている方にとってはこの負のサイクルは致命的になる。

先ほども少し触れたように、PVを伸ばす記事を書くには以下3点のようなことを少なくとも意識する必要がある。

  • ユーザーが抱えている課題を解決するための記事となっていること
  • Googleの検索結果の1ページに出るような構成・文字量etcにすること
  • 競合が多い分野を分析し、チャンスのある分野を攻めること

詳しいことは当ブログでは解説しないが、「ブログ PV増やし方」とでも検索すれば、著名な結果を出しているブロガーのSEO対策の情報を見ることが出来るのでそちらへ。少なくとも、自分が感じたことは、かなりニッチなジャンルを扱っているのであれば、何の考えもなしにブログを書いても絶対にPVは増えていかないということ。

f:id:takamizawa46:20200414095038p:plain

単語が持つパワーを理解することも非常に重要になる。PythonElixirでは元々の単語が持つパワーが違いすぎる。Pythonについて書かれた記事一つのPVに対してElixirについて書かれた100記事のPVを合わせても敵わないというのが現実である。事実、Pythonjanomeを使う方法をまとめた記事が当ブログでは1番のPV稼ぎ頭となっている。

(これがその記事)

www.okb-shelf.work

あと収益を作りたいならページにアフェリエイトリンクへの導線を作ることも非常に重要。この記事に「ブログの始め方」という記事へのリンクや、レンタルサーバーのアフェリエイトリンクもないことから「あ、こいつ、収益出せていないんだなー」と察してもられば。

内容よりも誰が書いたかの方が重要

これはnoteに始めて有料記事を投稿した時に感じたこと。名前の知られていない者が書いた内容の濃い記事よりも、著名な方が書いた決しても内容の濃くない記事(個人的な感想で批判ではないのでご注意)の方が影響力はでかいしPVも桁違いになる。

(めちゃくちゃ熱を込めて書いたので良ければぜひ)
note.com

今の歌謡業界に通ずるところはあり、どんな歌を歌っているかよりも、誰が歌っているかの方が重要ということと同じなのだろう。

プログラミングの未経験者向けに入門のための熱い記事を一つ書くよりも著名な方が書いたプログラミング初心者向けに書かれたスクールへの誘導記事の方が見られる。つまり、セルフブランディングも非常に重要で自分のキャラクターを作って名と共に信頼度を上げていく必要があると感じた。

最後に

文字を書く、すなわちブログは自分の肌に合っていた。アウトプットのために使うという目的も良かった。純粋に書きたいという思いだけで続けることが出来た。しかし、最近はせっかくなので、書いた記事を見てもらいたいという思いが強くなっており、テーマ、キーワード選定に意識を置くようになってきた。

100記事執筆したという自信を持って、少しは収益のために記事を書いてもいいんじゃないかと考えている。

本編としては以上で終了。 以降はおまけでせっかくなので一年間続けて、どれだけの収益とPVを得られたのかを公開しておこうと思う。

収益とPVについて

まずはPVから。よく100記事書きました系の記事で見られるPV数の推移は指数関数のようになっているが、当ブログは一定のPV数を上下しているのみで、全くPV数が増加して行く傾向がない。よく続いているなぁと改めて思ってしまった。

f:id:takamizawa46:20200414211843p:plain

ちょっとプチバズりがある時は、twitterに記事を投稿したところ、多くの方に拡散してもらえた時だったり、最近だとnoteのはてなブログのリンクを貼った時にかなりPVが増える日があった。それも常習的に発生するわけではないので、常にPV数がその状態の数値になるわけではない。

次に収益について。一応結果は出ているが、前に運営していたブログで発生した収益もいくらか含まれているので実質は1000円程度がこのブログで発生した収益になる。トホホ...

f:id:takamizawa46:20200414212133p:plain

最近は記事のリライトやタイトルを作成し直してPV数が増えるような施策を行っているが目に見えて変化を感じることは出来なかった。残念。

参考文献

【golangのサンプル有り】クロージャ(closure)完全に理解した人のためのクロージャを使った便利サンプル集

クロージャ完全に理解した...で何に使うの?

実は使いこなせると結構便利。特定の値に対する操作を共通化することが出来て、想定しているもの、言い方を変えれば作者の作成しているもの以外の操作を制限することも出来る。あと、かっこいい。
クロージャユースケースとしては小さなデータベースに対してコマンド経由で何かをしたい時と考えると分かりやすい。今回はそのサンプルを4つ用意したので、便利さを体感してみてほしい。1つでも刺さるものがあれば嬉しい限り。

サンプル集

設計方針としては2つの考えをベースにしている。

  • 初期値となる変数の用意(データ構造によってやりたいことを考える)
  • 用意した初期値に対する操作(取得, 更新, 削除)を行うコマンドを引数で受け取る無名関数を作成してコマンドの判定を行い、それぞれに対応する処理を記述する

クロージャを使ったタイマー

動作確認はこちらから
play.golang.org

// closure timer -> 経過時間をsecondsで返す
func ClosureTimer() func(c string) float64 {
    start := time.Now()
    return func(command string) float64 {
        switch command {
        case "GET":
            return time.Now().Sub(start).Seconds()
        case "STOP":
            return time.Now().Sub(start).Seconds()
        case "RESET":
            start = time.Now()
            return 0
        }
        return 0
    }
}

実行結果

func main(){
    // 新たなtimerを作成
    newTimer := ClosureTimer()
    time.Sleep(1 * time.Second)
    // 経過時間が1sとなるはず
    fmt.Println(int(newTimer("STOP")))
    
    newTimer("RESET")
    // timerがリセットされて0sとなるはず
    fmt.Println(int(newTimer("STOP")))
}
1
0

高階関数を使って関数内のローカルなスコープを持つ変数、この場合はClosureTimerを呼び出した時の時刻を初期値として保持しておく。この時刻と現在時刻との差分を返すのが1番の目的(GET or STOP)となるが、その他に比較時間のリセットのためのRESETというコマンドも用意してある。switch caseを増やせばいくらでも拡張できるため、都度都度、時刻時間の初期値を用意して差分を出して...値をリセットして...という変数管理をコマンドだけで行うことが出来る上にコードも共通化することが出来る。
自分は関数の実行時間を測る際にパッケージに記述したこの関数をよく使っていた。

クロージャを使ったカウンター

動作確認はこちらから

play.golang.org

func ClosureCounter(init, inc int) func(c string) int {
  count := init
  return func(command string) int {
    switch command {
      case "ADD":
        count += inc
        return count
      case "RESET":
        count = init
        return count
      case "GET":
        return count
    }
    return -1
  }
}

実行結果

func main(){
    // 新たなcounterを作成
    counter := ClosureCounter(0, 1)
    // 初期値の確認
    fmt.Println(counter("GET"))
    for i := 0; i < 10; i++ {
      counter("ADD")
    }
    
    // 正しくカウントされているかどうか
    fmt.Println(counter("GET"))
    // 値のリセット
    fmt.Println(counter("RESET"))
}
0 
10
0

やっていることは先ほど全く同じでデータ構造がintに変わったのみ。ClosureCounterを使うことでインクリメント変数の用意と管理を先ほどと同じように共通化、コマンド経由でのみ実行することが出来るため非常に便利。動作させているスレッドのカウントや、集計処理で条件にマッチしたデータ数をカウントするのによく使っていた。

クロージャを使ったkvs(キーバリューストア)

いわゆるredisに近いことがスコープが有効な範囲でやれる。

動作確認はこちらから
play.golang.org

func ClosureKVS() func(c string, k string, v interface{}) interface{} {
  init := make(map[string]interface{})
  return func(cmd string, k string, v interface{}) interface{} {
    switch cmd {
      case "ADD":
        init[k] = v
        return nil
      case "GET":
        res := init[k]
        return res
      case "RESET":
        init = make(map[string]interface{})
        return nil
      case "KEYS":
        resp := make([]string, 0)
        for k, _ := range init {
          resp = append(resp, k)
        }
        return resp
      case "VALUES":
        resp := make([]interface{}, 0)
        for _, v := range init {
          resp = append(resp, v)
        }
        return resp
    }
    return nil
  }
}

実行結果

func main(){
    // 新たなkvsを作成
    kvs := ClosureKVS()
    
    // key valueを追加
    kvs("ADD", "okb", "cool")
    fmt.Println(kvs("GET", "okb", ""))
    
    // さらにkey valueを追加
    kvs("ADD", "bko", "bad")
    fmt.Println(kvs("GET", "bko", ""))
    
    // 現在登録されているkeyを全取得
    fmt.Println(kvs("KEYS", "", ""))
    
    // 現在登録されているvalueを全取得
    fmt.Println(kvs("VALUES", "", ""))
    
    // 一度kvsをリセット
    fmt.Println(kvs("RESET", "", ""))
    
    // リセットが問題なく行われたかを確認
    fmt.Println(kvs("KEYS", "", ""))
}
cool
bad
[okb bko]
[bad cool]
<nil>
[]

正直、interface{}型ってあんまり使うのは好きではないのですが、半ばしょうがなく採用。keyが常にstring型なのは良いとしてvalueの値の型を固定すると、都度都度、対応する型のkvsを作る必要があるのでこんな形となった。

クロージャを使ったキュー

ここまで来たらもう何でも出来そう。やっぱりコマンド経由で...(ry

動作確認はこちらから
play.golang.org

func ClosureQueue() func(c string, v interface{}) interface{} {
  queue := make([]interface{}, 0)
  return func(cmd string, v interface{}) interface{} {
    switch cmd {
      case "QUEUE":
        queue = append(queue, v)
        return true
      case "DEQUEUE":
        if len(queue) > 0 {
          head := queue[0]
          queue = queue[1:len(queue)]
          return head
        }
        return nil
      case "SIZE":
        return len(queue)
    }
    return nil
  }
}

実行結果

func main(){
    // 新たなキューを作成
    queue := ClosureQueue()
    queue("QUEUE", 1)
    queue("QUEUE", 2)
    queue("QUEUE", 3)
    
    fmt.Println(queue("SIZE", ""))
    
    fmt.Println(queue("DEQUEUE", ""))
    fmt.Println(queue("DEQUEUE", ""))
    fmt.Println(queue("DEQUEUE", ""))
    
    fmt.Println(queue("DEQUEUE", ""))
    
    queue("QUEUE", 99)
    
    fmt.Println(queue("DEQUEUE", ""))
}
3
1
2
3
<nil>
99

最後に

どうでしょうか。クロージャも使ってみると意外とやれることが多いことに気づいてもらえれば何より。やはり値に対する操作を制限出来るというのが良い。想定外が発生しにくくなるし、共通化もされるため、インクリメント処理などを後から修正する事になっても、クロージャを使って共通化しておけば退屈な修正は最小限に済むだろう。

参考文献