やわらかテック

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

【第16回清流elixir勉強会】今年のElixirに関する学習を振り返る会

トピック

elixir-sr.connpass.com

早いもので今年ももう終わろうとしています。今年度より清流elixirというコミュニティを立ち上げて勉強会を行い今回を含めて合計16回の勉強会を開催することが出来た。幅広い方に参加して頂き、多くの方と知り合うことが出来た。それに見える世界観も大きく変わった

ここまで、この勉強会を継続出来たのは多くの方のご助力のおかげです。いつも有難うございます。今年度の勉強会はこれで最後になりますが、また来年からドシドシ活動をしていきますので、よしなにお願い致します🙇‍♂️

elixir-sr.connpass.com

清流elixir-infomation
開催場所: 丸の内(愛知)
参加人数: 4 -> 3 コミュニティ参加人数 : 37 -> 37
2019/12/21現在

第16回の勉強会について

今回は参加者の方と共に、今年のElixirに関する学習を振り返って共有した。やり方はシンプルかつノープランで各々、5~10分程度で軽く今年のElixirに関する学びを話す。それを順に回していくというスタイルになる
自分が何を話していたのかを正確には覚えていないが、参加者のtorifukukaiouさんがメモをとって下さりました。有難うございます。非常に助かります

自分が話したこと。今年の学び

  • 3月から清流elixirを開始
  • 知人がPythonの勉強会を全然知らない頃からはじめていまは約300名。その話を聞いたことで開催を決意
  • 当時は関数型なんぞやから。Elixirを知らなかった。パイプラインとか 分散とかやってみたい。面白そう
  • 基本的にはプログラミングElixirで勉強
  • 今年の一番の学びは基礎文法、バックグラウンド(用途、得意なこと)
  • 清流elixirの今年度の目標 -> 「Elixirとはなにができるものか、用途が向いているか」は説明できるようになったので目標達成
  • アプリケーションを作ったりはまだできていない
  • 来年はElixirで何か作る悪だくみをする
  • 文法わかりました。仲間が増えた。Elixirという好きな物の話しが出来て楽しい

当初、清流elixirを立ち上げた際は何度も話しているが、Elixirについてはほとんど知らなかった。そんな状態から今年はスタートした。torifukukaiouさんElixirの学びをまとめていらっしゃるスプレッドシートが素晴らしかったので、パクリつつ今年の学習を後日談として細かく振り返ってみる。ブログにまとめていて良かったなぁとしみじみ思う

2019年のOKBとElixir

清流elixirを発足する

今年の3月22日のこと。発足の理由は何度も話した通り。当時からその気持ちはぶれていないつもりだ

www.okb-shelf.work

とりあえずアウトプットのためにブログをスタートする

何だこの痛い記事は...1000文字しか書いてねぇ...。この時点で「まつもとゆきひろ」さんの考えを大切にしている

www.okb-shelf.work

怒涛の勢いでElixirの勉強をしてアウトプット

関数型言語なるものを触るのがはじめて、再帰関数であったり、ElixirEnum、特にEnum.reduceなんかが面白くて、色々試してはコードを書いてブログにまとめていた。この辺りの時期がもっともElixirを書いていただろうか

www.okb-shelf.work
www.okb-shelf.work
www.okb-shelf.work
www.okb-shelf.work
www.okb-shelf.work
www.okb-shelf.work
www.okb-shelf.work
www.okb-shelf.work
www.okb-shelf.work
www.okb-shelf.work

第2回清流elixir勉強会を開催

パイプライン演算子にまつわる勉強会。引数が流れて気持ちいんじゃあ^〜と喜びあったのを覚えている
www.okb-shelf.work

ちょっと凝ったことを試し始める

ElixirからPythonで記述した関数を呼び出したり、http clientを扱ったりと、少し玄人感が出てきたなと思っていたはずだ

www.okb-shelf.work
www.okb-shelf.work
www.okb-shelf.work

第3回清流elixir勉強会を開催

パイプライン演算子に続き、Elixirと言えば、パターンマッチでしょということでパターンマッチをしまくる勉強会。map型のパターンマッチに苦戦したのを思い出す

www.okb-shelf.work

少し並行処理を意識しだす

以前行なったmessage passingを発展させたTaskがかなりしっくりきて、並行処理が出来たことに喜びを感じた

www.okb-shelf.work

Elixirでアルゴリズムを実装する

ゴールデンウィークに入ったこともあり、購入した「なっとく!アルゴリズム」という書籍を元にソートを始めとした様々なアルゴリズムElixirで実装していった。Elixirの勉強にもなったし、計算量という概念も身につき、コードを書く際の視点が増えて、業務で非常に役にやっている

www.okb-shelf.work

www.okb-shelf.work

www.okb-shelf.work

www.okb-shelf.work

www.okb-shelf.work

www.okb-shelf.work

www.okb-shelf.work

第4回清流elixir勉強会を開催

前回に引き続き、パターンマッチの学習を行なった。第4回の勉強会は常連参加者のりき さんが進行をしてくれた。難しいことは考えないようにしたきたが、「値の束縛」という概念など非常に分かりやすく説明してくれた。さらに第4回の勉強会の内容をいつも通り、はてなブログにまとめてtwitterに投稿したところ、fukuoka.exさんを主催されており、日本のElixir先駆者である、piacere さんにRTを頂き、多くの方に認知して頂けた。第4回の勉強会をきっかけに全国各地の方とつながりを持つことが出来た

www.okb-shelf.work

はじめてWebFrameworkを触る

なぜかPhoenixではなく、trotというマイクロなFrameWorkを使ってAPIを作ってみた。確か、git hubexploreで見つけた記憶がある

www.okb-shelf.work

第5回清流elixir勉強会を開催

再帰関数について取り組んだ。個人的に再帰関数という考えがお気に入りで、説明していて楽しかった記憶がある
www.okb-shelf.work

Elixirで学ぶコンピューターサイエンス

自分がコンピューターサイエンスを学んでいないことでマウントを取られることが多くて、悔しい思いをしたので書籍を購入して誰も挑戦していないElixirで各章の課題を記述した。パターンマッチが論理ゲートを実装する上で非常に役に立った

www.okb-shelf.work
www.okb-shelf.work
www.okb-shelf.work
www.okb-shelf.work
www.okb-shelf.work

Elixir/Erlang Fest2019に行ってきた

トップの人たちが集まる環境に行けることは滅多にないので、東京に行ったことが2度しかなく、初めての1人東京だったが参加してきた。当日のLTはレベルが高すぎて自分にはよく分からなくて、いい刺激を貰えた。参加した知見を清流elixirに持ち帰ろうと思っていたが、常連参加者の全員がElixir/Erlang Fest2019に参加してため、世間の狭さを感じた

www.okb-shelf.work

第6回清流elixir勉強会を開催

今まで使用していた4畳程のレンタル会議室を抜け出し、Misoca様のオフィスにお邪魔させて頂き勉強会を開催した。今まで競技プログラミングの経験は無かった為、かなり苦戦したが、Elixirのパイプラインが非常に役になった。参加者のthara さんが書くコードがスタイリッシュすぎて感動したことを覚えている

www.okb-shelf.work

第7回清流elixir勉強会を開催

初めてのリモート勉強会。fukuoka.exさんで勉強会を主催されているkoga さんに準備をして頂き人生初のリモートでの勉強会参加をした。この頃から福岡のエンジニアのレベルの高さには驚かされた

www.okb-shelf.work

第8回清流elixir勉強会を開催

以前行なった、競技プログラミングの問題をElixirを使って解くというのが面白かったので、再度同じテーマで開催。ちょうど、この頃から新たなレンタルオフィスを見つけてそちらへ移動した。以前より広くモニターも使えて、安く使い勝手が良い

www.okb-shelf.work

第9回清流elixir勉強会を開催

ここまでElixirについて自身が学習をするということを行なってきたが、新規の参加者を増やす、Elixirを東海で布教するという目的に挑戦するために、Elixirに興味がある、やってみたいという方向けにElixirの入門会を初めて開催した。当日は新たに3名の方に参加して頂き、Elixirの魅力を伝えることが出来たのではと思う

www.okb-shelf.work

色々、挑戦する

Enum.map再帰関数どちらが早いのかを調べたり、今更ながらPhoenixに取り組んだりと、的を絞らず様々なことをやっていた。基本的な文法を触れ終えて実践的なことを始めようと動き出していたはずだ。OTPがかなりしっくりきた

www.okb-shelf.work
www.okb-shelf.work
www.okb-shelf.work

第10回清流elixir勉強会を開催

ブログで行なったEnum vs 再帰関数の知見を元に、条件分岐構文とパターンマッチのどちらが早いのかを調べた。ちょうど、この辺りから業務が忙しくなり、Elixirを触る時間が確保出来なくなってしまった

www.okb-shelf.work

第11回清流elixir勉強会を開催

ついに勉強会で並行処理に挑戦。内容はTaskを使ったシンプルなものだが、mixを使ってプロジェクトを作成するところまでを完遂することが出来てかなり内容も充実させられるようになってきた

www.okb-shelf.work

第12回清流elixir勉強会を開催

以前よりずっと、行いたかったCode ReTreatというテーマを決めて、各チームでそれに取り組むというミニハッカソンのようなイベントを開催した。テーマはソートアルゴリズムを採用した。30分のセクションを2回行なったが、とても面白かった。もっと時間と良いテーマを決めて、再度取り組みたいイベントだ

www.okb-shelf.work

尖りだす

Erlangの知見を習得し始めたり、環境を構築したりと少し尖ったことを学習し始める。しかし、個人的にはこんな感じの学習が結構好きだったりする。業務は忙しいが何とか時間を作って、自分の取り組みたいことをするようにした。以前のような学習に義務感はなく好きなテーマを好きに取り組むようになった

www.okb-shelf.work
www.okb-shelf.work
www.okb-shelf.work

第13回清流elixir勉強会を開催

Elixirのことをそこそこ理解しているつもりになっていたが、この勉強会で自分はまだまだだと思い知らされた。随所で使われているマクロや、見たこともない構文に手も足も出なかった。fukuoka.exさんのpiacereさんの解説がなければ詰んでいた

www.okb-shelf.work

人生で初めてLTをする。しかもリモートで

業務で登壇したり、セミナーで答弁したりすることはあったが、それは自分の意志ではない。自分からLTをしようと気持ちはあまりなかったが、piacereさんからお誘い頂き、これまでの清流elixirの活動をfukuoka.ex秋のLT大会にてお話させて頂いた。ブログとはまた違うアウトプットの方法に刺激を受けた

www.okb-shelf.work

マクロの学習を始める

前回の勉強会で非常に悔しい思いをしたのでマクロの勉強を始める。構文に関してはこのアウトプットで何とかなったはずだ

www.okb-shelf.work

第14回清流elixir勉強会を開催

以前、悔しい思いをしたマクロに挑戦する勉強会を開催した。今回はfukuoka.exさんと合同での勉強会を開催して、計全国3カ所を繋いだ壮大な勉強会となった。マクロを学習する中で、今までのプログラミング言語の概念がぶっ壊されて非常に勉強になった。まだまだ知らないことが多すぎて驚くばかりだ

www.okb-shelf.work

少しレイヤーの低い知識をインプット

Elixirの仕様を理解するためにErlangの理解が必要になることがあり、Erlangの知見をインプットした。Elixirに比べて日本語情報が少なく、理解がかなり難しかった

www.okb-shelf.work
www.okb-shelf.work
www.okb-shelf.work

qiitaに初投稿

fukuoka.exさんのアドベントカレンダーに参加さえて頂き、LTした内容をさらに詳細にまとめたものを投稿した。qiitaに投稿することで、また普段とは違い方に見て頂けるチャンスが得られることが分かった。自身の登校日のPVが普段の3倍ほどになっていた

qiita.com

第15回清流elixir勉強会を開催

fukuoka.exさんのもくもく会に並行開催させて頂き、qiitaのアドベントカレンダーに記載する記事を書くために色々触ってみた。もとより、分散処理を華麗にこなしたいという気持ちがあったので、Node間での通信について、取り組んだが時間が足りず途中で終わった。

www.okb-shelf.work

そして伝説へ

ここまで今年度の主な自身のElixirに関する活動になる。実はいろいろとgit hubのprivateのレポジトリで色々と作ったりはしていたが、どれも上手くいかずに途中で投げ出している。Phoenixで作りたいアプリの構想もいくらかあるし、Nervesを触るために購入したラズベリーパイも触れていない。ある程度、基礎的な知識は身についているはずので、来年度は形があるものを作って、世の中に出せたらと思う

今回の参加者の方々の「ものを作る」という活動にとても心打たれた。自分も作ってみようと思う

結論として、Elixirの学習を始めてよかった。言語仕様の他にも色んな知見やつながりを得ることが出来た。来年度も清流elixirの活動は積極的に行なっていきますので、また宜しくお願い致します。

【サンプルコード有り】golangとclosureで作ったクールなカウンターをElixirで書き直した

closure(クロージャー)とは何か

難しい概念の説明は強いエンジニアや大学の賢い先生方にお任せするため、深い説明は行わない。ざっくりと言うと、関数の中である値を保持させておいて、その値を変化させる(操作する)ための変数を保持している関数の内部に用意された無名関数のことだ。変数にアクセスするためにはclosure経由でのみしか許可させない、変数の値を変化させるためにはclosure経由でしか行わせないとすることで、変数のスコープを上手く隠すことが可能になる。実際にgolangで書いたclosureと動作をお見せする

golangで作ったclosureを利用したカウンター

実装はめちゃくちゃシンプル。go tourで解説されているclosureのサンプルを引数のコマンド経由で色んな処理が行える様にしただけだ。なぜ、このカウンターを作成したかというと、業務でgolangを使って、jsonからマーシャルしたデータを集計して返すみたいな処理が多発しており、大量のカウント用の変数の用意と管理がめんどくさくて、何かオシャレにしたろと思い出来上がったのがこれ。コマンドで操作できるので、割と気に入ってるし、コードの改修も行いやすそうだと思っている

package main
import "fmt"

// カウンターをclosureで作成。コマンドによって処理を切り分ける
func ClosureCounter(init, increment int) func(string) int {
    sum := init
    return func(command string) int {
        // commandを用いて行いたい処理を切り替える
        switch command {
        // 引数で指定した値分、sumに加算する
        case "ADD":
            sum += increment
            return sum
        // sumの値を返す
        case "GET":
            return sum
        // sumの値をリセットする
        case "RESET":
            sum = 0
            return sum
        }
        return sum
    }
}

// 実行部分
func main() {
  counter := ClosureCounter(0, 1)
    for i := 0; i < 10; i++ {
        counter("ADD")
        fmt.Println(counter("GET"))
    }
    fmt.Println("最終結果: ", counter("GET"))
 
    // 値をリセット
    counter("RESET")
    fmt.Println("RESET後: ", counter("GET"))
}

結果

1
2
3
4
5
6
7
8
9
10
最終結果:  10
RESET後:  0

このように動作する。先ほどの説明の通り、sumという変数の操作には、returnで返された無名関数とコマンド(引数)経由でのみしか行うことが出来ない

では本命のElixirで

無名関数と聞いてElixirで実装しないわけにはいかない。さくっと作れそうなので作ってみた。と、思ったがこれがかなり難しい。関数型言語で値が変化されないことが約されるので、先ほどのように元の変数に対して操作を行うという処理をするのは想像以上に難しい。というか出来ない。なので、あれこれ工夫した結果、以下の様な形に追いついた。一言で言えば、プロセスを立ち上げてメッセージを送る方法になる

./counter.ex

defmodule LikeClosure do
  # 受け取ったメッセージを元に処理を分岐
  def loop(sum, increment) do
    receive do
      # 引数で受け渡し分、加算を行う
      {:add, pid} ->
        send(pid, {:ok, sum+increment})
        loop(sum+increment, increment)
      # 値をリセット
      {:reset, pid} ->
        send(pid, {:ok, 0})
        loop(0, increment)
      # 現在の値を返す
      {:get, pid} ->
        send(pid, {:ok, sum})
        loop(sum, increment)
      # プロセスを安全に停止させる
      {:exit, pid} ->
        send(pid, {:exit})
        exit(:safety)
    end
  end

  # コマンドの送信と受け取り
  def messenger(pid) do
    # カリー化した無名関数を返す
    fn command ->
      send(pid, {command, self()})
      receive do
        # 返答を受け取る
        {:ok, sum} -> sum
        {:exit} -> IO.puts("stop counter and counter process is exit")
        _ -> IO.pus(":error")
      end
    end
  end

  # 処理を内部化して使用者に意識させない
  def counter(sum, increment) do
    counter_pid = spawn(__MODULE__, :loop, [sum, increment])
    messenger(counter_pid)
  end
end

流れとしては、カウント用のプロセスを立ち上げて、そのプロセスに対してメッセージという形でコマンドを送り、プロセスが保持している値(カウンター)を操作するという形になる。また、その一連の処理を簡潔に行いたいので、counterという関数にプロセスの生成から、メッセージのパッシングまでを内部化させて使用者に見えない様にしている。動作を見てみよう

# ファイルをコンパイル
iex(1) > c("counter.ex")
[LikeClosure] 

# 無名関数をreturnで受け取る
iex(3)> counter = Closure.counter(0,1)
#Function<0.133727158/1 in Closure.messenger/1>

# コマンドを送って指定の操作をさせる
iex(4)> counter.(:add)
1
iex(5)> counter.(:add)
2
iex(6)> counter.(:add)
3
:
:
iex(16)> counter.(:add)
13
iex(17)> counter.(:add)
14

# 値の確認
iex(18)> counter.(:get)
14

# 値のリセット
iex(19)> counter.(:reset)
0
iex(20)> counter.(:get)
0

# プロセスの停止
iex(21)> counter.(:exit)
stop counter and counter process is exit
:ok

思ったことが出来ている。使用者には内部の実装を意識させないようにすることも出来た。しかしながら、カウントさせるだけの処理にこれだけコード量を記述するのは正直なところ「うーん」という感じなので、何か良い方法はないかと模索していたところ、以下の回答を発見した

elixirforum.com

なるほど、Agentを使えば、確かに簡単に実装できそう。なので次はAgentで同様の処理をさせてみた

iex(6)> {:ok, pid} = Agent.start_link(fn -> 0 end)
{:ok, #PID<0.113.0>}
iex(7)> fun = fn -> Agent.get_and_update(pid, fn i -> {i, i + 1} end) end
_#Function<20.127694169/0 in :erl_eval.expr/5>
iex(8)> fun.()
0
iex(9)> fun.()
1
iex(10)> fun.()
2
iex(11)> fun.()
3
iex(12)> fun.()
4

かなり、短くなった上に実装がシンプルに出来た
./counter.ex

defmodule AgentClosure do
  use Agent
  def start_link(init, increment) do
    # Agentプロセスを立ち上げ
    Agent.start_link(fn -> init end, name: __MODULE__)
    IO.puts("start counter agent")

    # パターンマッチを使用するためと内部化のため、無名関数を返す
    fn command ->
      counter(command, increment)
    end
  end

  # 加算
  def counter(:add, increment) do
    Agent.update(__MODULE__, &(&1 + increment))
  end

  # 値の確認
  def counter(:get, _) do
    Agent.get(__MODULE__, & &1)
  end

  # 値のリセット
  def counter(:reset, _) do
    Agent.update(__MODULE__, fn _ -> 0 end)
  end
end

結果

iex(1)> c("counter.ex")
[AgentClosure, LikeClosure]
iex(2)> counter = AgentClosure.start_link(0, 1)
start counter agent
#Function<4.81008950/1 in AgentClosure.start_link/2>
iex(3)> counter.(:add)
:ok
iex(4)> counter.(:add)
:ok
iex(5)> counter.(:add)
:ok
iex(6)> counter.(:get)
3
iex(7)> counter.(:reset)
:ok
iex(8)> counter.(:get)
0

同様の使用感で使うことが出来る。というか、これ関数のみで外部からの変化を許可していないので普通にclosureになっていることに今更気づいた

おまけ

上手くいかなかった時のコードその1

失敗: golangで書いた感じでそのまま移植しようとする

defmodule Closure do
  def counter() do
    sum = 0
    fn command ->
      case command do
        "ADD" -> 
          sum = sum + 1
          IO.puts(sum)
          sum
        "RESET" -> 
          sum = 0
          sum
        "GET" -> sum
      end
    end
  end
end

counter = Closure.counter()
IO.puts(counter.("GET"))

Enum.map(1..10, fn _ -> 
  counter.("ADD")
end)

IO.puts(counter.("GET"))

結果

0
1
1
1
1
1
1
1
1
1
1
0

そりゃ、そうなる

上手くいかなかった時のコードその2

失敗: 関数型で値を保持させるなら再帰関数使ってアキュムレーターやんけと思ってカリー化して値の保持までは出来たものの、結局、sumの値は不変なので「無理ゲーでは?」と気づく

defmodule Closure do
  def counter(init, increment) do
    sum = init
    fn command -> closure(command, sum, increment) end
  end
  
  def closure("ADD", sum, increment) do
    sum + increment
    # 結局sumに値がreturnされないので値を保持できない...
    # プロセス用意してmessage passingさせれば無理やりカウント出来るが...
  end
  
  def closure("GET", sum, _) do
    sum
  end
  
  def closure("RESET", _, _) do
    0
  end
end

# 変数のBINDまでは出来た
counter = Closure.counter(0, 2)
IO.inspect(counter)
counter.("GET") |> IO.puts()
counter.("ADD") |> IO.puts()
counter.("ADD") |> IO.puts()

結果

#Function<0.30015443/1 in Closure.counter/2>
0
2
2

惜しいけど、カリー化という知見が身についた

参考文献

【書評】Elixirの歴史を辿るうちに「電話はなぜつながるのか」をふと読んでいた

なぜ読んだのか

個人的な興味があって、以前よりElixirというプログラミング言語を学んでいる。このElixirの動作環境がErlangVMというものであり、Erlangというプログラミング言語を実行するためのものだ。このElixirErlangは高い堅牢性(robustness)を備えており、古くから金融システムや電話交換機に用いられているという。また、このErlangという言語を作ったのはJoeを中心とした、エリクソン社という通信機器を扱うスウェーデンの企業であり、実装にはErlangが用いられていたようだ

つまり、Elixirの歴史を追う中でErlangに出会い、Erlangの歴史を追う中で、用途を調べた所、「電話」というワードに辿りついたわけだ。どのような実装がされているのかを理解するためには、電話にはどのような仕様が必要なのかを知りたいと思った

また、電話の仕組みは現代のインターネットのベースになっているのだという。インターネットの仕組みという現代においては抽象化された事象への理解を深めるためにもこの書籍を読んで見て背景を知ってみるのはアリだと思った

あとDr.Stoneで千空が携帯電話作ってたので、そんなこと出来るん?と気になり、気付いた時にはポチっていた

加入電話の仕組みってすげー

電話の仕組みは至ってシンプルだった。詳細はぜひこの書籍を読んでほしい。本当に面白い。電話の仕組みについて全く知らない自分でも、核になる部分の情報はインプット出来たと思う

ベースにあるのは、声、つまりは空気振動という現象を電波に変換して、電話線という導線を通じて、対象の相手側に伝えるという物理現象だ。また、声を電波に変換するということは声をバイト情報(0と1の組み合わせ)にして電波を利用して導線を通じて、相手側に送り出すという事と等しい。電波を利用して送り出したバイト情報を受け手側で復元することで音声を再生する。電話がこのような単純な仕組みで動いていることに驚きを隠せない。しかも、何十年も前にだ...

さらに関心させられたのは、どのようにして相手側を探索するのかという仕組みと、どのようにして接続を保証しているのかというところだ。相手を探すために、電話交換機という全国に設置されている接続を繋げるための媒体を巡る旅がスタートする。発信者のリクエストを元に電話交換機Aから電話交換機Bに。電話交換機Bから電話交換機Cにと、電話交換機自身に保持されている接続者情報を探し、対象の接続先を探していく。この経路を記録して、発信元に知らせる。このような作業は特定のバイトパターンをコマンドのように定めて行われている。例えば、「00001111」のバイト情報であれば「交換機Bで対象の電話番号を持つ接続者を発見しましたよ」というFINDというコマンドとして扱っているわけだ(これは例なので実際には存在しない)。これは現代のIP(インターネットプロトコル)と非常に似ているし、実際にベースになっているのはこの部分からだそうだ

そして、この探索した経路を元に通話回線(音声をやり取りするための回線)を繋げることで初めて、通話ができる状態になる。つまり、音声を届けるための回線と情報をやり取りするための回線は分かれており、通話をするために2本の回線が用意されている。

話すとまだまだ面白い話があるが、このぐらいにしておく。どのようにして、届けられる音声の順序を保っているかという話も非常に面白い

IP電話の時代になって

電話をするために従来の電話線を用いる方法からIP、すなわちインターネット回線を用いた方式に変化していく。しかしながら、ベースの仕組みは全く変わっていない。新たに通話のためのプロトコルをインターネット回線上で定めて、相手側を探すために互いにプロトコルをベースにデータ送り合うだけで、加入電話の時と行なっていたことはほとんど変化していない。IPアドレスをいかにして非公開にするかなど、加入電話の時よりもセキュリティに対する意識と注意が高まっており、どのように秘匿性を保持しているのかが面白い。今まで、曖昧な理解のままだったが、なぜ GatewayNATというものが必要なのかがよく分かる

全く同じ様な仕組みでIP電話が実装されていることに加入電話というものが、いかに洗礼されて設計されたいたかということに恐怖さえ感じる。仕事柄、第2部の話はすんなり頭に入ってくるので、第2部から読み進めて、第1部に戻るという読み方もありかもしれない

業務に直結しないものを読む価値はあるのか

ある。オススメしたい。直接的には役に立たないかもしれないが、インターネットの背景を知ることが出来るため、非常に勉強になる。内容も優しく読みやすかった。この本を読んでみて、「あれ、電話ってもしかして作れるんじゃね」と思った。まずは、振動を電波に変える装置から取り掛かろう。待ってろ、千空..

こんなすげー仕組みに使われていて、安全に動作しているErlang半端ないって

参考文献

websocketを使ったgolang産チャットアプリのやらかしスレッド設計をkubernetesでスケール可能に修正した

機能に関する詳細

現在、業務でwebsocketを使用したチャットアプリケーションの開発に関わっている。自分が担当しているのはサーバーサイドのみで、実装はgolangを使用している。websocketのライブラリはgorilla/websocketを採用している

github.com

サーバーサイドの websocketのメッセージのやりとりの部分はgorilla/websocketのexamplesにあったものをパクリスペクトしている。そりゃ、ライブラリ作った人たちが作ったサンプルに間違いはないでしょという風に考えた

github.com

このサンプルに実装されているのはだいたい以下の通り

  • client.go -> clientに関する情報(struct)を生成。メッセージの受信送信のためのスレッドの実装
  • hub.go -> チャネル経由で受け取ったメッセージを配信するための処理の実装
  • main.go -> サーバーを起動してwebsocketの接続を受け付ける(switching protocol)

これだけでも十分な実装だが、自分が担当している業務では追加の機能要件がある。1つはredispubsub機能を使用した購読済みの別サーバーへのメッセージのpublish機能の実装で、これは予想の通り、kubernetesを使用してサーバーがスケールする予定であるため必要になる
とは言いつつも、参加当時はkubernetesに関する知識は皆無だったので、どのようにスケールするかなんて考えていなかった。というか、考えられなかったので、後のやらかしポイントになる

合わせて、メッセージの送受には並行処理が欠かせない。1つのプロセス、もしくはスレッドで処理を占有してしまうと、チャットとは呼べない代物が出来上がる。メッセージがサーバーに受信して送信されるまで、誰もメッセージを送ることが出来ないようなものになる。接続数が少ないうちは何とかなるかもしれないが、接続数に比例してストレスを感じるようになるだろう

やらかしたこと

f:id:takamizawa46:20191216225158p:plain
開発初期当時は環境の準備や実装の遅れなどの問題で、シングルサーバーでの動作確認しか行なっておらず、上手く動いているものだと錯覚していた。実際に、複数サーバーにしてみて動作を検証したはずなのだが、見落としていた様だ

では実際に何をやらかしていたかという話に入る。サーバーでのwebsocketのメッセージの処理とgolangの並行処理をするためにgoroutinesでどのようにスレッドを用意しているかを簡単に説明していく。ちなみにチャンネルはpubsubのチャンネルを指しており、チャネルはgolangのスレッド間通信で扱うデータ構造を指しており、タイポではない

setup

  • サーバーを起動する。この時に合わせて、Hub(examplesのhub.go)をgoroutinesを使用して立ち上げて待機させる
  • redispubsub機能を利用して対象のチャンネルを購読(subscribe)して、publishされたメッセージを受信するためのスレッドを待機させる
  • ユーザーがブラウザ経由でwebsocketの新規セッションのrequestを送信する
  • サーバー側で接続を許可して、このユーザー専用のメッセージ読み込みスレッドとメッセージ書き込みスレッドを立ち上げる
  • 新規でユーザーが接続される毎に同様に専用のスレッドを立ち上げる(認証で新規ユーザーなのかを判定している)

この時点でユーザーが2人であるとfixすると存在しているスレッドの内訳は以下の様に7つになる

  • メインスレッド(httpのrequestを受け付ける)
  • Hub(メイン処理を実行)スレッド
  • pubsubの受信用スレッド
  • ユーザーA専用の読み取りスレッド
  • ユーザーB専用の読み取りスレッド
  • ユーザーA専用の書き込みスレッド
  • ユーザーB専用の書き込みスレッド

メッセージの受信と送信

f:id:takamizawa46:20191216225218p:plain

  • webscoketのセッションを張っているユーザーから新規のメッセージが送信される
  • ユーザー専用の読み取りスレッドでメッセージを読み込んでredispubsub機能で購読済みのチャンネルにメッセージをpublish
  • publishされたメッセージを受信用のスレッドで受け取り、チャンネルを購読しているサーバーにメッセージを配信してチャネルを通じてHubにメッセージを渡す
  • Hubで受け取ったメッセージを元に、処理を行い対象のユーザーに配信するために各ユーザー専用の書き込みスレッドにメッセージをチャネル経由で送る
  • 専用の書き込みスレッドで受け取ったメッセージをwebsocket経由でユーザーに送信する

これが概要。あっ....これをスケールすると...

コードはかなり、省略してアレンジしたものを載せておく。動作は保証出来ないがイメージが伝わればと思う
hub.go(読み取りスレッド部分)

func (c *Client) readPump() {
    defer func() {
        c.hub.unregister <- c
        c.conn.Close()
    }()
    :
    :
        message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
        // publishは自前実装の関数。メッセージをpubsubを利用して送信する
        publish(message)
    }
}

pubsub.go(pubsubのメッセージを受信してHubに渡すスレッド) hub自体は起動時に引数で受け取る

func SubscribeChannel(channelName string, h *Hub) {
    :
    :
    // メッセージの受信ループを作成
    for {
        switch val := psc.Receive().(type) {
        case redis.Message:
            var message *Message
            h.broadcast <- message
        }
    }
}

hub.go(Hub)

type Hub struct {
         // userを識別するためにstringをkeyに持つmapに変更
    clients map[string]*Client
    broadcast chan []byte
    register chan *Client
    unregister chan *Client
}

func (h *Hub) Run(m *database.Mongo) {
    for {
        select {
        // websocket経由で受信したメッセージを読み取るスレッドから受信
        case binaryMessage := <-h.Broadcast:
            switch message.Type {
            case "MESSAGE":
                h.Client[message.UserID].Send <- binaryMessage
            }
        }
    }
}

スケールした結果

何と同じメッセージがスケールしたサーバーの数(Pods)だけ、送信されているではないか。そりゃそうだ。読み取ったメッセージをpublishして、受け取り配信するためのスレッドと各サーバー毎に立ち上がっているのだから。ついでに処理もサーバー台数分だけ走るので、databaseの値もめちゃくちゃになっている。開発初期当時は、kubernetesのスケールへの理解が不足しており、redispubsubpublishを担当するスレッドも同様にスケールされるという意識が無かった

単純にpublishをどのタイミングですれば良いのかという判断が甘かった

改善した結果

f:id:takamizawa46:20191216225241p:plain
構成に手を加えて、スケールしても問題なく、処理が1度のみ走り、メッセージが複数回、送信されないようにした。setupの部分の更新はなく、全体の流れは以下の様になった。大きな変更としてはスレッド間で扱うチャネルを1つ追加した。元のexamplesに実装されているのはbroadCastというチャネルでこれは、読み取り専用プロセスからHubに渡すために使用するチャネルとして扱い、新規で追加したチャネルはpubsub経由で受信したメッセージを受信するためのチャネルとして用意をした

元のHubのstruct

type Hub struct {
    // Registered clients.
    clients map[*Client]bool

    // Inbound messages from the clients.
    broadcast chan []byte

    // Register requests from the clients.
    register chan *Client

    // Unregister requests from clients.
    unregister chan *Client
}

チャネルを追加

type Pubsub struct {
        // 送信先
        Target string
        Message []byte
}

type Hub struct {
         // userを識別するためにstringをkeyに持つmapに変更
    clients map[string]*Client
    broadcast chan []byte
    register chan *Client
    unregister chan *Client
    pubsub chan *Pubsub
}

メッセージの受信と送信

  • webscoketのセッションを張っているユーザーから新規のメッセージが送信される
  • ユーザー専用の読み取りスレッドでメッセージを読み込んでHubにチャネル経由でメッセージを渡す
  • メッセージを元に処理を行い、redispubsub機能で購読済みのチャンネルにメッセージをpublish
  • publishされたメッセージを受信用のスレッドで受け取り、チャンネルを購読しているサーバーにメッセージを配信して別のチャネルを通じてHubにメッセージを渡す
  • Hubで受け取ったメッセージから送信先のユーザーのwebsocketのセッションが自身のサーバーに存在するかを確認。存在している場合に、ユーザー専用の書き込みスレッドにチャネル経由でメッセージを渡す
  • 専用の書き込みスレッドで受け取ったメッセージをwebsocket経由でユーザーに送信する

このように変更することで無事に、スケールした状態でもメッセージが1通のみ送信されるようなり、冷や汗が引いた。並行処理プログラミングの経験も、スケールの経験も全く無かったので大変、勉強になったし、どのように考えれば良いのかが理解出来た

hub.go(読み取りスレッド部分)

func (c *Client) readPump() {
    defer func() {
        c.hub.unregister <- c
        c.conn.Close()
    }()
    :
    :
        message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
        // exampleの通りチャネルを経由してHubにメッセージを渡す
        c.hub.broadcast <- message
    }
}

hub.go(Hub)

func (h *Hub) Run(m *database.Mongo) {
    for {
        select {
        :
        :
        // websocket経由で受信したメッセージを読み取るスレッドから受信
        case binaryMessage := <-h.Broadcast:
            switch message.Type {
            case "MESSAGE":
                h.Client[message.UserID].Send <- binaryMessage
                // 配信に必要な情報をpubsub経由で渡す
                publish(&Pubsub{Target: "対象のユーザーを認識する値(eg: unique id)", Message: message})
            }
        }
    }
}

pubsub.go(pubsubのメッセージを受信してHubに渡すスレッド)

func SubscribeChannel(channelName string, h *Hub) {
    :
    :
    // メッセージの受信ループを作成
    for {
        switch val := psc.Receive().(type) {
        case redis.Message:
            var message *Message
            // 全体配信用のチャネルにメッセージを渡す
            h.pubsub <- message
        }
    }
}

hub.go(Hub)のpubsub用のメッセージの受信

func (h *Hub) Run(m *database.Mongo) {
    for {
        select {
        case publish := <-h.pubsub:
            if val, ok := h.Clients[publish.Target]; ok {
                val.Send <- publish.Message
            }
         :
         :
    }
}

こんな感じで落ち着いた。並行処理の設計は奥が深い...
draw.ioというサービスを構成図に書くのに初めて使ってみたが、かなり良いのでオススメ

おまけ Elixir/Erlangの勉強が意外なところで役に立った

並行処理と聞いて、反応せずにはいられない。ElixirErlangの得意とするところだ。実際に実装に使用しているのはgolangだが、このチャットアプリを実装するにあたって、「プログラミングElixir」や「プログラミング Erlang」で得たプロセスの設計方法やアクターモデルの考え方はgoroutinesを扱う上で非常に参考になった

スレッドの設計をし直す時にも「プログラミング Erlang」でJoeがどのように複数プロセスでの処理を考えるのか(最初にメッセージシーケンス図を書くらしい)を参考に、シーケンス図をそこそこ書いた
まさか、こんな形で役に立つとは思わなかったがElixirErlang万歳

参考文献

【現役エンジニアが考察】はじめてのプログラミング言語習得のコツ。挫折しないためにはどう学ぶべきか

この記事を書く理由

個人的にElixirというプログラミング言語の勉強会を主催したり、セミナーに登壇する中で多くのプログラミングの初学者や、現在、学習をしているが、何をしていいのか分からないなど多くの相談を受けた。毎度、回答する中で「参考になりました」と良い反応をもらうことが多くなってきたので、せっかくなので記事にして多くの方に情報を共有できればと思う

  • 挫折を防ぐため
  • 効率的にプログラミング学習を進めるため
  • 楽しくプログラミングの学習を続けるため etc...

にぜひ参考にして頂ければと思います

想定している読者

この中で1つでも自身に当てはまるものがあれば、この記事を読んでいただく価値はあるかと思います

  • これから新たにプログラミングを始めたい
  • プログラミングの学習は何からやっていいのか分からない
  • プログラミングの学習をしているがどこまで学習すればいいのか分からない
  • どのプログラミング言語を選べばいいのか分からない
  • 過去にプログラミングに挑戦したが挫折したことがある

著者について

twitter.com

OKB(岡部)と言います。今年度から新卒として名古屋のITベンチャーでエンジニアをしています。学部生の頃は土木と建築を学んでおり、挫折の結果、独学でプログラミング学習をして今に至ります。情報系の学部の出身ではないため、いわゆる未経験という状態からのスタートをしました。プログラミング自体は始めて2年未満になります
業務ではWebアプリケーション開発のバックエンド、機械学習案件の取り回し(まれに実装)に携わっています。経験言語は以下の通りです

ほかに気休め程度に触っているものもありますが、今のベースになっているのはこのあたりです。とは言いつつも、私自身、過去に一度、プログラミング学習に挫折をしたことがあります。「猫でもわかるC言語」という著書があるのですが、全く分かりませんでした。なので挫折した面と、それでも業務にありつけたという面の両面を知っているからからこそ見えてくる視点があると思っています

これから記述することは格闘技と同じように流派があり、必ずしも正解ではありません。私の考えであって世間の考えではありません。しかしながら、先ほど書いた通り、「挫折」と「業務にありつけた」という両面を知っているからこそ自信を持って、この情報を共有します

それでは順に説明をしていきます

プログラムを書くという意味を見失わないで

f:id:takamizawa46:20191208014733p:plain
あなたはなぜプログラミングを学習しようと思うのか、またしているのか。今、世間ではエンジニアになるというのが一種のブームになっている。自由な時間に勤務でき、高額な報酬が手に入る。そんなイメージだけが一人歩きしてしまっており、プログラミングを始める人の頭にあるのは上記のような理想ばかりだ
それを悪いことだとは思わないし、私自身も同じような理由でプログラミングを始めた。誰だって楽して儲けたいものだ。Pythonってのを覚えたら年収が増えるんか!!!」という学生の浅知恵で始めただけのことである。しかしながら、その思いだけでこの長く辛い、独学の道を走り続けるのは難しい

「なぜ挫折するのか?」

答えはシンプルで、お金儲けが目的になって、プログラムを書くという意味を無視して通り過ぎているからだ
プログラミングの根本にある考え方は「自動化・効率化・高速化」のような人間の惰性の解決である。同じような計算処理を何度も繰り返すのが面倒臭い、毎日、水を汲んで温めるのが面倒臭い...など

プログラムを書いてお金を貰うということは自身の、もしくは誰かの面倒ごとをプログラムを書くことで効率化した、自動化したという結果ということになる。プログラマーになりたいだけという考えは止めよう。きっと続かない


まとめ

【プログラムを書くことを楽しもう】
面倒ごとをどのようにプログラミングという方法で自動化、高速化、効率化するという所にプログラミングの面白さがある

ほとんどのプログラミング言語の習得の進め方は同じ

楽しくプログラミングをするためには「プログラミング言語の習得」は欠かせない。しかし、このステップで挫折をしてしまう人が多く非常にもったいない。向き不向きはあるが、興味を持っただけに悲しい結果となってしまった

人間誰しも1つは言語を習得して日々、話しては聞き、使いこなしているのになぜプログラミング言語の習得に挫折するのか
それは「自動化、高速化、効率化」するために、なぜプログラミングが必要なのかを理解していないからだと言える

実際にプログラムを書いて、入荷した100個の果物(りんごとオレンジ)を仕分けるプログラムを書くことになったとする。さて何から始めるか。ざっくり作業の流れを書き出してみると以下のようになるだろう

  • 箱から果物を1つ取り出す
  • 取り出した果物がりんごなのかオレンジなのかを判定する
  • りんごであればりんご用の箱に。オレンジであればオレンジ用の箱に
  • まだ箱に果物が残っていればこの作業を繰り返す。もう無ければ終了する

別になんら難しくない作業の流れだと思う。このようにプログラムを書く際には処理の流れを書き出すことが重要であるが、今重要なのはそこではない。この作業の流れに登場する処理は3つだけであり、基本的にはどの言語でも3つの処理が用意、もしくは記述可能であるということだ

ではその3つの処理とは何なのか。凄くシンプルだ  

  • 変数の宣言(値を用意する)
  • 繰り返し(同じ処理を何度も行う)
  • 条件分岐(条件によって行う処理を変える)

たったこれだけだ。作業の効率を無視すれば、どのような処理も上記3つの処理を組み合わせれば記述することが可能になる。まずは各言語に用意された上記3つの構文を覚えることがスタートだ。逆に言えば、3つの処理の書き方を知っていればほとんどの処理は記述することが可能になっているはずだ
それにプログラミングという作業をめちゃくちゃ抽象化してしまえば、データをAという状態からBという状態に変化させているだけだ。先ほどの3つの処理を用いて説明をすれば、変数(用意した値)を繰り返し処理や条件分岐を使って、別の状態にするという訳になる

しかしながら、手が動かないという場合はプログラミング言語の知識の他にOSネットワークの知識が不足している可能性がある。安心しよう。プログラミング言語を習得出来ていないからではない

配列オブジェクトマップなどのデータ構造やその他の構文は必要になった時、処理が面倒だと思った時に「こんなものが用意されているんだ」という流れで覚えた方が使用すべきケースも分かるし、理解も深まる

例えば「配列を覚えるタイミング」は変数の宣言を10個、100個...もしなくてはいけないようになった時。どう考えても面倒だし、効率化するためにプログラムを書いてるのに全く効率化されていない。そこで配列というものが登場する。配列を用意すれば1つの変数(配列)に100個の値を格納することが出来る

このように「あれ、効率化されてない?」と思ったタイミングが新たな学びのタイミングになる

まとめ

プログラミング言語で最も基本的な構文を覚える】
- 変数の宣言
- 繰り返し
- 条件分岐

どのプログラミング言語を学ぶべきか

最初のプログラミング言語を何にするかは、常に現役のエンジニア間でも議論が行われている

  • コンピューターサイエンスを学ぶために
  • 楽しくプログラムを書くため
  • お金儲けが出来る言語を選ぶべき

色々な考え方があるため、確定的な答えを出すことが難しいが、私はプログラムの根本にある「自動化、高速化、効率化」が達成できるものなら何でもいいと思っている。その一方で最初の言語は、やはり楽しくシンプルに学ぶことができるものを選んだ方が良い
初学者の状態で「メモリ管理」だの「変数のスコープ」だの、「静的型付け」だということを考えて覚える時間を費やすよりも、シンプルな構文と処理を覚えて実際にプログラムを書く時間を用意した方が良い

オススメはPythonRubyのどちらかだ
Pythonは構文がシンプルで覚えやすい。また、コミュニティも活発で分からないことがあっても調べたら何となる可能性も高い。また、Webに機械学習にと採用される範囲が広く書けて損はないだろう
Rubyも同様に構文がシンプルで覚えやすい。Rubyの一番の強みはやはり、日本産(まつもとゆきひろ先生)であり、豊富な日本語情報が用意されているという点にある。それにRuby on railsという強力なWebフレームワークがあり、Web業界の仕事に繋がりやすい

この2つの言語、もしくは他の言語に触ったのちに「プログラミングってどうやって動いているんだろう」とか「なぜ、配列というデータが扱えるのだろう」とか「なぜPythonは処理が遅いと言われているんだろう」という話に興味を持ち始めた時、それからC言語などを始めてみれば良い。ありがたさが分かるのはそれからで良い。まずは書いて楽しむことが重要だ


まとめ

【最初の言語のオススメ】
PythonRubyが良い。まずはシンプルな構文を覚えて実際にプログラムを書いてみよう。難しい話に触れるのはその後で十分だろう

プログラミング言語の学習は書籍でサクッとやればいい。最初の一冊は1週間あれば、全てに目が通せるようなもの(P200ぐらい)の物がオススメ。いきなり、難しい書籍を対象に選んで、途中で挫折するのが典型的な残念パターンだ

Rubyに関しては良書を知らないので、省略するが、Pythonの学習にはサクッと読める「スラスラ読めるPythonふりがなプログラミング」がオススメだ。初学者の高校時代の部活の後輩に読んでもらったが、非常に分かり易かったと高評価を頂けた

どんなレベルの情報でも良いのでアウトプットしよう

これからエンジニアとしてやっていこうと考えているのならば、アウトプットをする癖をつけておくと良い。なぜアウトプットをする必要があるのかをRubyの作者のまつもとゆきひろ先生がこう答えている

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

アウトプット、ブログの記事にしたりという作業は面倒臭いし、100%お金が貰えるというわけではない。ではなぜ、アウトプットをするのか。エンジニア視点では2つ理由があると考えている
1つはアウトプットすることで自分の学習になるからで、学習した内容をブログ記事にまとめようとすると半端な知識では不可能だ。順序よく理解している必要がある。それに言語化してまとめることで自分の再学習にもなる。そうすると簡単には忘れなくなる
もう1つは世間に自分の存在を知ってもらうことが出来るからだ。アウトプットしないエンジニアは世間からすれば、無名の選手と変わらない。どんなレベルの情報であれ、アウトプットをしているということはGoogle検索に自分の情報が現れるということになる。また、アウトプットした情報はそのままポートフォリになる上に、どういった事柄に興味があるかという証明書にもなる

これだけのメリットがありながらアウトプットを実行しているエンジニアは少ないのだ
私がアウトプットをこのブログでし続けて得られたものは非常に多い。ぜひ、興味があればこちらの記事で詳しい話をしているので、一度目を通して見て頂ければと思う

www.okb-shelf.work

反応を貰える状態を作る

結局、ブログにしろyoutubeにしろ続かない理由は凄くシンプルで反応がもらえないから、もしくは報酬が発生しないからのどちらかになる。例を挙げてみると、「誰も食べもしない料理を作り続ける(反応がもらえない)」、「無賃できつい仕事を続ける(報酬が発生しない)」というイメージだろうか

この状態を抜け出すために何かしらの工夫をすると良い。プログラミングのコミュニティに所属してみるのも良いし、ハッカソンに出場してみるのも良い。プログラミングのアルバイトを初めてみるのも良い戦略だ

独学する上で大切なのは「続けられる仕組みを作ること」だと考えている。何を選ぶかはあなた次第だが、決まった期間にやり切りたいというのであれば、プログラミングスクールに通ってみるというのも手だ。しかしながら、決して安い額ではないのでスクールの評判や得られるものは慎重に選んだ方が良い

最終的に伝えたいこと

長々と自分の考えを記述してきた。ここまで読んで頂いた方には感謝しかありません。現在、エンジニアになるということが一種のブームのようになり、悪質な情報や、デマカセが多く、多くの初学者を混乱させる原因となっている。しかしながら、プログラミングの根底にあるのは今回の記事で紹介したような内容であり、楽しいものだと伝われば嬉しい

twitterのDMなどでも相談を受けているので気軽に相談してもらっても問題ない
学習の際の参考に少しでもなればと思います

参考文献