やわらかテック

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

【プログラミングErlang7章より】並行処理とアクターモデルの概念について

並行処理について

人間なら誰でも並行処理を理解している。
この世界は並列だ。
Erlangのプログラミングは人間の思考と対話をモデル化している。
人間はそれぞれメッセージを送ることによって通信し合う独立した存在として動く。
誰かが死ねば、他の誰かが気づいてくれる。

Joe Armstrong

プログラミングErlangより引用

1つのコンピューターで出来ることには限りがある。より高度な処理をしたいと思った時には並行処理や並列処理を行う必要がある。 並行? 並行?なにそれ、めちゃくちゃ難しそう…と感じてしまうが、プログラミングErlangを読む限り、そう焦る必要はないらしい
実際に我々、人間が日々、暮らしているこの世界でも並行処理が行われている

たとえば、誰かと話をしている時。自分が友人に昨日の愉快な出来事を語る。その話を友人は相槌を打ちながら聞いてくれる。この何気ない日常のやりとりでさえも並行に処理が行われていることに気づく

自分が話をするという動作をするのと同時に相手は話を聞いて相槌を打つという動作を行なっている。これを創造主の視点から見てみると、あたかも二人が同時に、すなわち並行に処理をしているように見える。というか、している。つまりは、我々人間が1つのプロセスという単位で動いていると見なすことが出来る

これが人間の日常生活は並行処理に溢れているという一例だ

並行処理と並列処理の違い

以下のブログにて詳細に解説がされていた。せっかくなので、自身の言葉で並行と並列について違いをまとめておこう
yohei-a.hatenablog.jp

違いを説明するのに5人の人間に命令を出すというケースを例にしてみる。5人の人間に高速で順次、あたかも同時に見えるような速度で命令を順次出していくような処理を並行処理。5人の人間に本当に同時に命令を出すような処理を並列処理と考える

さらに砕いた例で説明をすると、1つの脳みそで順次、5人の話を同時に聞く聖徳太子と5つの脳みそで同時に5人の話を聞く宇宙人をイメージしてもらえれば分かりやすいのではないだろうか

f:id:takamizawa46:20191207171942p:plain

Erlangにおける並行処理の方法

のちに解説をするがErlangでは並行処理のためにプロセスが採用されており、互いの内部状態はメッセージを送り合うことで更新する。このようなモデルをアクターモデルと呼ぶ

また、プロセスとは別に共有メモリを使用するスレッドという処理単位を用いて並行処理を行うものもある。知っているもので例をあげればgolangのgoroutineはスレッドを用いて処理を行う

ただ、どのようにしてアクターモデルという思想が精査され出来上がったのかが気になったので、調べてみた

アクターモデル

すでに既出でアクターモデルについて解説している優良な記事が多いので詳しい話はそちらに任せて、ざっくりと概要と説明が出来るレベルに内容をまとめておこう

アクターモデルにおいて、全てのものはアクターである。これはオブジェクト思考における、全てのものはオブジェクトであるという考え方と非常に似ている。そのため、アクターモデル並行指向オブジェクトプログラミングとも言われるようだ

アクター同士はメッセージを送り合う事が可能であり、逆に言えばやり取りの方法はメッセージを送り合う事のみに限定されている。なぜ互いのアクター同士でメッセージを送る事が可能なのかというと、それぞれのアクターはメールボックスアドレス(言うなればメールアドレス)を持っており、自分宛に受け取ったメッセージをメールボックスにスタックする
このメッセージを順次処理していくことになる。受け取ったメッセージをトリガーにして、様々な処理を行えばアクターモデルプログラミングが可能になる

またアクター自身は別のアクターを生成することが出来る
理論や歴史はwikipediaなどに情報が転がっているのでそちらを参照してほしい

ja.wikipedia.org

感想

このような概念が1970年代に、近未来に起こりうるであろう、分散コンピューティング、並行処理を予測して形作られてきていたということに驚きを隠せない。そして、一度は1990年代に衰退したアクターモデルが現代になって複雑になりすぎたデータを処理するため、再び注目されていると...。熱い...

それにしても、このプログラミングErlangが面白い。著者がErlangの作者のJoe Armstrongであるため、並行処理に関するテクニックや考え方が散りばめられていて、Erlangに興味がない人が読んでも勉強になる

また読み進めたい

引用

【第15回清流elixir+fukuoka.exもくもく勉強会】Elixirでチャットサーバーを作りながらNode間通信を試す

トピック

elixir-sr.connpass.com

Qiitaのアドベントカレンダーの執筆したり、業務が忙しすぎたり、PIDの闇にハマっていたりで、少し間が空いてしまいましたが第15回のレポートをまとめました
今回も前回同様に、kogaさんにお声がけ頂きましてfukuoka.exさんと合同でもくもく勉強会を開催しました
fukuokaex.connpass.com

僕はQiitaのアドベントカレンダーElixirNode間チャットアプリを作るってのを書こうと思って、前からやりたかったNode間通信に挑戦したので当日の内容と後日の作業をプラスでまとめたものを共有します

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

第15回の勉強会について

Nodeの基本的な操作

ElixirでのNode間通信を行う基本的な操作はプログラミングElixirの第15章にまとめられている。しかし、ただまねるだけでは面白くないので以下の記事を参考にNode間でチャットを作る簡易的なアプリケーションを作ってみようと思う

dev.classmethod.jp

その前に基本的な Nodeに関するコマンド処理を覚えておこう。まず複数のNodeを識別する必要があるためNodeに名前を付けてあげよう。名前を付ける際はiexの起動時に--snameというオプションを付与して以下のように起動する。すると、立ち上がったプロンプトに付与したNode名が表示されていることが確認できる

$ iex --sname node1
:
iex(node1@okabe-y)1>

自身のNode名を確認したい時にはNode.self()とすることでアトム形式で自身のNode名を取得することが出来る

iex(node1@okabe-y)1> Node.self()
:"node1@okabe-y"

これでNodeへの名前付けと自身の名前の確認が出来るようになった。次にもう1つターミナルを用意して、別のNodeを立ち上げてみる。こちらにはnode2という名前を付与する

$ iex --sname node2
:
iex(node2@okabe-y)1>

次に、この用意した2つのNodeを実際に繋げてみる。といっても、やり方は凄く簡単で自身のNodeに接続させてやりたいNodeの情報をNode.connect()/1を通して渡してやるだけだ。今回はnode1に対してnode2を接続させてみよう。まずはnode2のターミナルで自身のNode情報を確認する

iex(node2@okabe-y)1> Node.self()
:"node2@okabe-y"

次に、node1のターミナルで先ほど確認したnode2の情報を用いて、実際にNodeを接続させる。接続に無事に成功するとtrueが返ってくる

iex(node1@okabe-y)2> Node.connect(:"node2@okabe-y")
true

ついでに自身のNodeに今、どのNodeが接続しているのかを確認しておこう。Node.list()とすることで接続されているNodeの一覧をリスト形式で取得する事が出来る

iex(node1@okabe-y)3> Node.list()
[:"node2@okabe-y"]

無事に、先ほどの接続が完了していることが確認出来た

チャットサーバーの用意

ベースに使用しているのは先ほど紹介させて頂いた記事だが、せっかくなのでチャットルームの概念をサーバーに用意してみた。少しコードが長くなってしまったので順に解説はしていく

defmodule ClientInfo do
  defstruct name: "GUEST", room: "roomA", pid: nil
end

defmodule ChatServer do
  def start() do
    pid = spawn(__MODULE__, :receiver, [[]])
    :global.register_name(:server, pid)
  end
  def receiver(clients) do
    receive do
      # 全体通知のメッセージ
      {sender, {:announce, body}} ->
        IO.puts("[ANNOUNCE] #{body}")
        send(sender, {:ok})
        receiver(clients)
      # ルーム単位のメッセージ
      {sender, {:message, room, body}} ->
        Enum.filter(clients, fn client ->
          client.room == room
        end)
        |> Enum.map(fn client ->
          IO.puts("[MESSAGE]: #{client.room}@#{client.name} #{body}")
        end)
        send(sender, {:ok})
        receiver(clients)
      # ルームへの新規参加
      {sender, {:join, room, name}} ->
        client = %ClientInfo{name: name, room: room, pid: sender}
        add = clients ++ [client]
        IO.puts("[JOIN]: #{name} joined the #{room}")
        send(sender, {:ok})
        receiver(add)
      # ルームから退室
      {sender, {:leave, room, name}} ->
        IO.puts("[LEAVE]: #{name} left the #{room}")
        send(sender, {:ok})
        receiver(Enum.filter(clients, fn client ->
          !(client.room == room and client.name == name)
        end))
      {sender, {:status}} ->
        IO.puts("[STATUS] send server status to client")
        send(sender, {:ok, :status, clients})
        receiver(clients)
    end
  end
end

defmodule Client do
  def server_pid(), do: :global.whereis_name(:server)
  def announce(body) do
    send(server_pid(), {self(), {:announce, body}})
    receiver()
  end
  def message(room, body) do
    send(server_pid(), {self(), {:message, room, body}})
    receiver()
  end
  def join(room, name) do
    send(server_pid(), {self(), {:join, room, name}})
    receiver()
  end
  def leave(room, name) do
    send(server_pid(), {self(), {:leave, room, name}})
    receiver()
  end
  def status() do
    send(server_pid(), {self(), {:status}})
    receiver()
  end
  def receiver() do
    receive do
      {:ok} -> IO.puts("success send message to server")
      {:ok, :status, data} ->
        Enum.map(data, fn d ->
          IO.puts("[INFO]: #{d.name} joined #{d.room}")
        end)
      _ -> IO.puts("invalid message")
      after 3000 ->
        IO.puts("not reply...")
    end
  end
end

まずはチャットサーバーで扱うクライアントのデータの構造をstructを用いて定義している。今回のチャットサーバーは凄くシンプルなもので、それぞれ以下を参照している

  • name -> クライアントの名前
  • room -> クライアントが入室している部屋(今回は入室は1室のみ)
  • pid -> クライアントのPID
defmodule ClientInfo do
  defstruct name: "GUEST", room: "roomA", pid: nil
end

次にメインとなるチャットサーバーの実装がChatServerモジュールに記述してある。基本的にやっていることは凄くシンプルでElixirのメッセージパッシングの構文receiveをパターンマッチングを使って複数種類のメッセージを受け取るようにして、それぞれのメッセージを受け取った際の処理を順に記述しているだけになる

  • announce -> 接続しているクライアント全員にメッセージを通知
  • message -> 指定したルームにいるクライアント全員にメッセージを通知
  • join -> 指定したルームに新規にクライアントを参加させる
  • leave -> 指定したルームからクライアントを退室(削除)させる
  • status(デバッグ用) -> 現在チャットサーバーに接続しているクライアントの情報(誰がどこのルームに参加しているか)を返す

また、start()を呼び出すことで、メッセージ受信用のプロセスを新規で立ち上げて、:global.register_nameを使って、接続しているNode間で立ち上げた新規のプロセス情報を共有する。これはメッセージをメッセージ受信用のプロセスに送信するために行なっている(sendPIDを指定する必要があるため)

そして、最後にクライアントがメッセージを送信するためのモジュールがClientになる。それぞれの関数はsend構文をwrapして、関数単位で処理が出来るようにしているだけなので、実装しなくても同様の処理は行えるが、楽にメッセージを送信するために実装した

メッセージを送ってみる

先ほど実装したコードを適当なファイル名を付けてiexを実行したパスに配置しよう。僕は適当にmessage_server.exという名前を付けた。このファイルをそれぞれのiex上からコンパイルしてモジュールを実行可能にする

iex(node1@okabe-y)4> c("message_server.ex")
[Client, ChatServer, ClientInfo]
iex(node2@okabe-y)2> c("message_server.ex")
[Client, ChatServer, ClientInfo]

node1をメッセージ受信に用いて、node2からメッセージを送ってみる。まず、node1でチャットサーバーを起動する。その前にNode.list()を確認して、node1にnode2が接続していることを確認しておこう。Nodeが接続されていないと、:global.whereis_nameが値(チャットサーバーのPID)が取得出来ないので、メッセージを送る事が出来ない
無事に、チャットサーバーが立ち上がると:yesというアトムが返ってくる

iex(node1@okabe-y)5> Node.list()
[:"node2@okabe-y"]


iex(node1@okabe-y)6> ChatServer.start()
:yes

次に、node2の方からメッセージを送ってみる

iex(node2@okabe-y)3> Client.join("Elixirを朝まで語る部屋", "okb")
success send message to server
:ok

node1のターミナルを確認すると受信したメッセージを元に作成したログを確認することが出来るだろう

[JOIN]: okb joined the Elixirを朝まで語る部屋

せっかくなのでもう1つNodeを追加して、よりチャットっぽさを出してみよう。新たにターミナルを立ち上げてnode3という名前でiexを用意して、node1に接続させる

$ iex --sname node3
:
iex(node3@okabe-y)1> c("message_server.ex")
[Client, ChatServer, ClientInfo]

node1のターミナル

iex(node1@okabe-y)7> Node.connect(:"node3@okabe-y")
true

無事に成功したようなので、node3からnode2と同じチャットルームにjoinさせて、適当にチャットしてみる

iex(node3@okabe-y)3> Client.join("Elixirを朝まで語る部屋", "にゃーん")
success send message to server
:ok

node1

[JOIN]: にゃーん joined the Elixirを朝まで語る部屋

チャットサーバーに接続しているクライアントの確認
node2

iex(node2@okabe-y)5> Client.status()
[INFO]: okb joined Elixirを朝まで語る部屋
[INFO]: にゃーん joined Elixirを朝まで語る部屋
[:ok, :ok]

では、メッセージを送ってみよう node2

iex(node2@okabe-y)6> Client.message("Elixirを朝まで語る部屋", "こんにちは、初めまして!")
success send message to server
:ok

node1

# 送信されたというログ(フロントを実装していないため、サーバーログで確認)
[MESSAGE]: Elixirを朝まで語る部屋@okb こんにちは、初めまして!
[MESSAGE]: Elixirを朝まで語る部屋@にゃーん こんにちは、初めまして!

node3

iex(node3@okabe-y)5> Client.message("Elixirを朝まで語る部屋", "こんにちは、okbさん")
success send message to server
:ok

node1

[MESSAGE]: Elixirを朝まで語る部屋@okb こんにちは、okbさん
[MESSAGE]: Elixirを朝まで語る部屋@にゃーん こんにちは、okbさん

nod2

iex(node2@okabe-y)12> Client.leave("Elixirを朝まで語る部屋", "okb")
success send message to server
:ok

node1

[LEAVE]: okb left the Elixirを朝まで語る部屋

node3

iex(node3@okabe-y)6> Client.message("Elixirを朝まで語る部屋", "まだ朝じゃないのにいなくなったww")
success send message to server
:ok

node1

[MESSAGE]: Elixirを朝まで語る部屋@にゃーん まだ朝じゃないのにいなくなったww

無事にやり取りが出来ていることが確認出来た

今後の発展

今回のNode間通信は同一のホストで行なっただけで、まだNode間通信の真髄を味わったわけではない。それにチャットサーバーもクライアント管理が適当であったり、認証がなかったり、データ構造がいまいちだったりと改善の余地が山ほどある。しかしながら、ElixirでのNode間通信の基礎を楽しく抑えるには十分だろう

次はDockerを使って別ホストとのNode間通信を行なったり、cloudのVMと接続したりということをやってみようと思う

参考文献

Elixrのプロセスとは一体何なのか。探索の旅に出てた

今更だけど

Node間通信のチャットサーバーを作っている時にふとなぜか、「Elixirでspawnとかした後に返ってくるPIDって一体何なんだ?」と思い立った
なぜsend(target_pid, message)とするだけで対象のプロセスにdataを送ることが可能なのだろか。Node間通信をしている最終に気になってしまったので調べた結果を個人的メモとしてまとめておく

実際にPIDを見てみよう

PIDとは「Process ID」の略語でプロセスを認識するための固有IDなのかなと間違えてしまいそうだが、実際には「Process Identifier」の略語であり、プロセスの識別子、つまりはプロセスを識別するために割り振りされた値という捉え方をすることが出来る

以下のコードをiexを立ち上げて実行することでPIDの存在を確認することが出来る

iex(1)> self()
#PID<0.105.0>

この値がPID。ここに表示されたPIDは現在、立ち上がっているiexに割り振られたPIDになる。すなわち、iexもプロセスとして動いているということになる。次は新規に生成したプロセスのPIDを確認してみる

iex(3)> spawn(fn -> IO.puts("nice") end)
nice
#PID<0.109.0>
iex(4)> self()
#PID<0.105.0>

spawnを使って生成したプロセスのPIDself()で確認したPIDが異なっていることが確認できる。すなわち、新たなる別のプロセスの生成に成功しているということだ。では、実際のこのプロセスが何者なのかを追っていく

PIDを追う旅の始まり

僕がPIDに関して疑問を持ったのはNode.self()の戻り値がPIDだったからだが、先ほどself()PIDが確認出来たので、そこから追っていこう

h spawn/1としてhelpを確認したところ、spawnProcessというモジュールに実装されているそうだ
こちらがspawn/1に該当するコード。defdelegateって何じゃい。しかも、引数が2つある。どういうことだ...

@spec spawn((() -> any), spawn_opts) :: pid | {pid, reference}
defdelegate spawn(fun, opts), to: :erlang, as: :spawn_opt

defdelegateについて

まずはdefdelegateから調べてみよう。ぐぐったらElixirKerneldefdelegateを発見。見た感じ、別のモジュールに用意されている関数を自身で定義しているモジュールの関数として振る舞わせることが可能なようだ

無事にEnumに実装されているrandomの関数に処理が割り当てられているようだ。Erlangの関数も無事に呼び出せるところを見ると、先ほどのspawnに割り当てられている処理はErlangspawn_optをcallしているはずだ

defmodule Sample do
  defdelegate random(list), to: Enum
  defdelegate my_random(list), to: Enum, as: :random
  defdelegate my_sleep(time), to: :timer, as: :sleep
end

Sample.random([1,2,3]) |> IO.puts()
Sample.my_random([1,2,3]) |> IO.puts()
Sample.my_sleep(1000) |> IO.puts()

# 2
# 1
# ok

spawn_opt

次にErlangドキュメントのspawn_optを見に行こう。Erlangのドキュメントの目次のproc_libの項目からspawn_optを発見出来た

  • spawn_opt/2 -> spawn_opt(Fun, SpawnOpts) -> pid()
  • spawn_opt/3 -> spawn_opt(Node, Function, SpawnOpts) -> pid()
  • spawn_opt/4 -> spawn_opt(Module, Function, Args, SpawnOpts) -> pid()
  • spawn_opt/5 -> spawn_opt(Node, Module, Function, Args, SpawnOpts) -> pid()

ドキュメントの文言通り、optionを用いて新たなプロセスを生成するで〜とのこと。ふむふむ、この関数で間違いないだろう

Spawns a new process and initializes it as described in the beginning of this manual page. The process is spawned using the spawn_opt BIFs.

この関数はどれもpid()という値を返している。次はこのpid()を探しに行こう

Erlangのドキュメントを見たところData Typesの項目にPidの項目があるのだが、実際に内部のデータがどうなっているかという記述はなく、詳しいことはProcessを見て下さいとある。しかし、ProcessのページでもPIDの内部に触れている部分はなかった

pid()はいずこに

探しに探し、このpdfファイルの引用からヒントを得て、対象のファイルを見つけることが出来た
Erlang and Elixir for Imperative Programmers

https://github.com/erlang/otp/blob/maint/erts/emulator/beam/erl_term.h#571

現在はErlangディレクトリ構造やファイル内容が更新されていて、パスは変わっている。こちらが現在、erl_term.hが格納されているディレクトリのパスになる

github.com

そこに記述されているのがこのコメント。どうやらbinaryでデータを持たせているようだ

/*
 *  PID layout (internal pids):
 *
 *   |3 3 2 2 2 2 2 2|2 2 2 2 1 1 1 1|1 1 1 1 1 1    |               |
 *   |1 0 9 8 7 6 5 4|3 2 1 0 9 8 7 6|5 4 3 2 1 0 9 8|7 6 5 4 3 2 1 0|
 *   |               |               |               |               |
 *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *   |n n n n n n n n n n n n n n n n n n n n n n n n n n n n|0 0|1 1|
 *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *
 *  n : number
 *
 *  Very old pid layout:
 *
 *   |3 3 2 2 2 2 2 2|2 2 2 2 1 1 1 1|1 1 1 1 1 1    |               |
 *   |1 0 9 8 7 6 5 4|3 2 1 0 9 8 7 6|5 4 3 2 1 0 9 8|7 6 5 4 3 2 1 0|
 *   |               |               |               |               |
 *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *   |s s s|n n n n n n n n n n n n n n n|N N N N N N N N|c c|0 0|1 1|
 *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *
 *  s : serial
 *  n : number
 *  c : creation
 *  N : node number
 *
 */

何だこれ。全くわからん。websocketの実装をRFCで覗いたことがあるが、その時にもpayloadの内訳がこんな感じで定義されていた。電話線でのやり取りで扱われるデータも同様だ。つまりは、最大32bitの固定長のbinaryの枠に何かしらの情報を格納したものをPIDとして扱っているということだろう。この内訳が何なのかを調べていたところ、以下の質問をstackoverflowにて発見した(古い質問なので情報が古い可能性が高い)

stackoverflow.com

この回答から推測するにPID layoutは28bitから32bitのエミュレーターとなっており、そのエミュレーター<A.B.C>という、まさにElixirPIDを確認した時のように(PID<0.109.0>)構成されているということ
確かにPID layoutは最大32bitsの枠を用意している。次にA, B, Cが何を指しているのかを確認してみる。回答を翻訳すると

  • A -> 0はローカルのノード。もしくは任意のリモートノード番号
  • B(15bits) -> プロセス番号の最初の15bit
  • C(16-18bits) -> MAXPROCSに達するたびに増加するシリアル

とのこと。うーん、よく分からない。そういう風にErlangで枠を定義しているってことは理解した。つまりは、電話線やプロトコルのようにbinaryの枠にデータを定義してプロセス同士、もしくはErlang自体がやり取りをしているということだろうか?
この説明を見ただけではしっくりこなかったので流浪していたところ、Erlangの公式ドキュメントにて以下のページを見つけた

erlang.org

ページトップの文言を引用して、翻訳してみると

The external term format is mainly used in the distribution mechanism of Erlang
external term formatは主にErlangの配布機構(おそらくmessage passingのことを指す)にて使用されています

このページでビンゴだろう。それにPIDに該当するであろうNEW_PID_EXTというformatを発見することが出来た

1 N 4 4 4
88 Node ID Serial Creation

それぞれの項目に対する説明を軽く翻訳してみた

  • Node -> ATOM_UTF8_EXT, SMALL_ATOM_UTF8_EXT or ATOM_CACHE_REFを使ってエンコードされた発信元の名前
  • ID -> A 32-bit big endian unsigned integerを用いて表現。15bitsが重要で残りは0になる
  • Serial -> A 32-bit big endian unsigned integerを用いて表現。13bitsが重要で残りは0になる
  • Creation -> A 32-bit endian unsigned integerを用いて表現。同じNodeから送信される識別子は同じ作成を持つ必要がある。この値を用いてcrashしたNodeと新しいNodeを区別することが可能。値0はデバッグに用いるため、使用しないで下さい

だそうです。何となく意味は分かるが、より深い理解をするためにはErlangの深い理解が必要なようだ。ここから先はまた深い知識が必要になりそうなのでこの記事では一旦、ここまでとしておこう。現状の自分ではここでギブアップだ。来年、同じものを見た時に少しでも理解できるようになってはいたい

まとめ

今回の調査で分かった事はElixirPIDErlangで定義されている最大32bitのbinaryであり、この定義されたbinaryを使って電話線やプロトコルのようにやり取りをしているんだろうなーっていうこと

_人人人人人人人人人人人人人_
> PID完全に理解した <
 ̄YYYYYYYYYYYYYYYYYYYY ̄

参考文献

ほとんど毎日パソコン触って目の疲れがやばいのでブルーライトカットメガネを買って1週間使った感想

健康診断で目の疲れが発覚

人生で初めての屈辱を味わった。自分の取り柄はどうでも良い事を覚える事と、視力が良い事(下がらないこと)だった。しかしながら、先週、会社の健康診断があり視力を測定したところ、左目が1.5 -> 1.0に。右目が1.2 -> 0.6になってしまっていた
めちゃくちゃショックや...しかしながら日常生活を送る上では視力が下がった弊害を受けていた自覚が全くない..なぜだろうか...

思い当たる点としては平日は毎日朝10時から19時まで業務でパソコン。家帰ってから21時から23時~24時までパソコン。土日は朝10時から24時ぐらいまでパソコン触ってたりで、思ってみればパソコン触ってない日がないなぁと...

こんな生活してれば、そりゃ目も疲れるわなぁと思いつつ、この件がショックでこんなツイートをしたところ、ブルーライトカットメガネがいいというリプライをもらった

また、ベテランのエンジニアの方からも「ブルーライトカットメガネいいっすよ」とアドバイスをもらった。うーん、これはせっかくの機会だし買ってみるかと思い立ったのが丁度、先週の出来事である

地元のJINSに向かう

人生で初めてのメガネ購入。教えて頂いたJINSというお店に向かうことに...
しかしながら、到着して気づいたが地元にあったのはJINSではなくてZoffだった。「アホすぎるやろ笑」と思いつつ、お店を覗いてみることに。人生初のメガネ物色に汗が止まらなかった

3分ほど、物色を続けているとついに目的のブルーライトカットメガネを発見。価格は5000円(税込で5500円)で思っていたよりも安かった。メガネ拭き兼、持ち運び用の巾着袋も付いているので結果的に5500円で事足りた。バリエーションはフレームの色の違いとレンズの大きさの違いで用意されていて、店舗に常時置いてある種類は9種類だったと記憶している
どうやら、2, 3日営業日を待てば、好きなフレームにレンズを用意してもらえるらしいので、拘りがある方はそちらを利用すれば良いかと思う。私はメガネの似合う似合わないとか全然、気にしてなかったので即日購入可能な既製品を購入した

こちらが購入したブルーライトカットメガネ。箱から出す前に写真撮っとけばよかった...
f:id:takamizawa46:20191201123705j:plain:h550

1週間、使ってみた感想

職場に急にインテリジェンスな男が現れたと初日からざわつく。鏡を覗いてみると「悪くないじゃん」と思う一方で、フレームがもう少し太いものを買えば良かったかなぁとも思う。まぁ、見た目のことは今回は重視しないないのでヨシ!

目の疲労の感じ方の変化について

まず肝心な目の疲労についてだが、確かにブルーライトカットメガネを使うことで目の疲れ方のペースが落ちた気はする。具体的な測定値があるわけではないので、「そんな気がする」というプラシーボ効果的な面もあるので信用には足らないかもしれないが、個人的にはしっくりきている。本来の目標が達成されたのでかなり満足だ
肌が弱いが、耳のところやフレームが接触する部分がかゆくなることもなかった。しかし、一方で以下のような問題もある

ずっとかけていると世界がおかしくなる(RPG風味

レンズを通して見える世界の色が、若干黄色味がかって見えるため、メガネを外した際に目がぼやけるような感覚に襲われる。度の入ったメガネを使用したことがないので「メガネってそういうものだよ」と言われてしまえばそれまでだが、1日中、ブルーライトカットメガネをかけ続けるのはオススメしない。こんな感覚に初日に襲われてから1時間に1回はブルーライトカットメガネを5分ほど外すようにしている。こうすることで、かなりマシになった

総評

5500円で良い買い物をした。滅多に物を買うことはないが、「目の疲れを削減する」という目標は達成することが出来た。しかしながら、ずっと掛け続けていると上記のような問題もあるので、上手く使っていきたいと思う。エンジニアの方はじめ多くの方は1日中パソコンを触ることは当たり前になっているので、自身の目を大切にしたいという方はぜひ一度、買ってみて効果を体感して頂ければと思う

【第14回清流elixir勉強会】Elixirのマクロを完全に理解する会

トピック

f:id:takamizawa46:20191116231318p:plain
elixir-sr.connpass.com

第14回目となる清流elixirの勉強会を開催しました
今回からは本格的にリモート参加枠を用意した。しかし、これが思っていたよりも何倍も難しい。会場のホットな温度感や議論が始まるとリモート参加者にはその現場感を伝えることが出来ない。トライアンドエラーでよりリモート参加者の方に価値ある時間が提供出来るように創意工夫していきたい

また今回は普段より非常にお世話になっている、福岡を拠点に広く活動されているfukuoka.exさんと合同での勉強会を開催しました
今回で合同の勉強会は2回目となりました。いつも知識を共有して下さり、感謝しかありません...

fukuokaex.connpass.com

最近は普段使用している会議室がどこかしらの団体とバッティングしているようで、本当に隔週開催のペースで活動したいものの、中々、上手くいっていない 。金曜日に固定で19:30~使える会場を抑えたいが、何かに依存したくないな〜とも思いつつ、頭を悩ませている

それでも清流elixirの名前が着実に広まってきており、多くの方からお声掛けを頂いている。本当にありがたい限りです
弱い人でも勉強会は開催できる、参加できるってことを広く伝えていきたい。やるかやらないかだけの問題だと思うので...

というか、コミュニテイ人数が一気に増えてて笑うしかない(圧倒的僥倖...!
そろそろ一度は地元、岐阜でElixirの勉強会をやってもいいのかなと思っている

清流elixir-infomation
開催場所: 丸の内(愛知)
参加人数: 8 -> 12 update!!
コミュニティ参加人数 : 21 -> 36 update!
2019/11/16現在

第14回の勉強会について

今回は第13回の勉強会で涙を流すことになったElixirのマクロについて完全に理解するために、この勉強会を開催した
書籍プログラミングElixirの第20章をベースにElixirのマクロについて理解を進めていくことにした。全体の流れは私が以前、予習的にまとめた以下の記事を参照して頂ければと思う

www.okb-shelf.work

当日も同じように

  • ElixirにおけるASTについて
  • quoteとunquoteについて
  • defmacroについて
  • macroを使ってmyifを作る

という流れで理解を進めた。しかしながら、人に解説をすることで自分の理解していない粗の部分が分かった。今回は勉強会を通して自身が深く理解をした点について列挙していこうと思う

評価される順番がマクロは異なる

以前からマクロは遅延評価でっせという話は聞いていたし、何となく理解していた。なので引数からIO.puts("nice")のような値を渡しても、その瞬間にniceが標準出力されることはない
即時に評価されるのであれば、以下の処理がnicekeytrueでもfalseでも出力されるはず

iex(1)> defmodule Sample do
...(1)>   defmacro exec(arg1, key) do
...(1)>     quote do
...(1)>       if unquote(key) do
...(1)>         unquote(arg1)
...(1)>       end
...(1)>     end
...(1)>   end
...(1)> end
{:module, Sample,
 <<70, 79, 82, 49, 0, 0, 4, 172, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 137,
   0, 0, 0, 15, 13, 69, 108, 105, 120, 105, 114, 46, 83, 97, 109, 112, 108, 101,
   8, 95, 95, 105, 110, 102, 111, 95, 95, ...>>, {:exec, 2}}

iex(2)> require Sample
Sample

key=trueの時

iex(3)> Sample.exec(IO.puts("nice"), true)
nice
:ok

key=falseの時

iex(4)> Sample.exec(IO.puts("nice"), false)
nil

この結果がマクロは即時評価ではなく、遅延評価であることを表している。これが通常の関数定義に用いるdefであれば、以下のように出力されるからだ

iex(1)> defmodule Sample do
...(1)>   def exec(arg1, key) do
...(1)>     if key do
...(1)>       arg1
...(1)>     end
...(1)>   end
...(1)> end
{:module, Sample,
 <<70, 79, 82, 49, 0, 0, 4, 208, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 135,
   0, 0, 0, 15, 13, 69, 108, 105, 120, 105, 114, 46, 83, 97, 109, 112, 108, 101,
   8, 95, 95, 105, 110, 102, 111, 95, 95, ...>>, {:exec, 2}}
iex(2)> Sample.exec(IO.puts("nice"), true)
nice
:ok
iex(3)> Sample.exec(IO.puts("nice"), false)
nice
nil

あ、完全に理解した(何も分かってない

さらに評価される順序は異なる

マクロが遅延評価であることは分かったが、さらにマクロは細かく評価される時系列が異なるようだ。それを表しているのが以下のマクロ記述。これは書籍プログラミングElixirの第20章から引っ張ってきたものだ

defmodule MyMacro do
  defmacro myif(condition, clauses) do
    do_cal = Keyword.get(clauses, :do, nil)
    else_cal = Keyword.get(clauses, :else, nil)
    
    quote do
      case unquote(condition) do
        val when val in [false, nil] -> unquote(else_cal)
        _ -> unquote(do_cal)
      end
    end
  end
end

そもそも、気になっていた事があって、なんでdo_calelse_calquote doのブロック外にいるのかがよく理解出来ていなかった。quote doの内部で変数が扱いたいのならunquoteを使えばいいじゃん〜って思うわけだが...

これにはマクロの中(defmacro)でもさらに評価される順番が異なるのではないかという仮説と結果によって、なぜquote doの外に記述しているのかを理解することが出来た
以下の処理の結果を見てみると非常に興味深い。なぜか、niceは一度のみ出力されている

defmodule MyMacro do
  defmacro myif(condition, clauses) do
    do_cal = Keyword.get(clauses, :do, nil)
    else_cal = Keyword.get(clauses, :else, nil)
    IO.puts("nice")
    
    quote do
      case unquote(condition) do
        val when val in [false, nil] -> unquote(else_cal)
        _ -> unquote(do_cal)
      end
    end
  end
end

defmodule Foo do
  require MyMacro
  def main() do
    MyMacro.myif(1==2, do: IO.puts("nice"), else: IO.puts("boat"))
  end
end

Foo.main()
Foo.main()

# nice
# boat
# boat

何故なんだ...しかし、この現象はquote doの外部(上部)に記述されているコードがquote doの部分が評価される前に一度だけ、評価されていると考えると自然な結果だと理解することが出来る

一応、偶然かもしれないのでFoo.main()をめっちゃ呼び出しておく

Foo.main()
Foo.main()
Foo.main()
Foo.main()
Foo.main()
Foo.main()

# nice
# boat
# boat
# boat
# boat
# boat
# boat

つまりdefmacroの内部でも評価される順番は存在しており、現状の知識理解だと

  1. quote doの外部
  2. qiote doの内部

という順になっており、同じdefmacroの内部であっても時系列が異なることが確認出来た。userという構文や__before_compile__のような指定がマクロに指定できる事は、同じマクロであっても評価される時系列が異なるためと考えると存在している理由が分かる

またdo_cなどをquote doの外部に記述することで、 何が嬉しいかというと

  • unquote()をめっちゃ使う必要がなくなる(Keyword.get()の戻り値を何度も使用しようとすると、codeが長くなって可読性が落ちる)
  • def :nice() doのような関数定義をことを行うことがquote do内部で出来ないため、quote doの外部で宣言しておく必要がある
defmodule Sample do
  defmacro hoge(func_name) do
    to_atom = :"func_name"

    quote do
      def unquote(to_atom)(num1, num2) do
        num1 * num2
      end
    end
  end
end
  • quote doの内部にあるものは、マクロ実行時に都度、評価されるためパフォーマンスが悪くなるのではないかと考えられる(複雑な計算処理は事前に行なっておくなど)

後日談とマクロは健全

これは参加者の方から頂いた受け売りの知識だが、C言語の場合にはマクロはスコープが切られておらず、副作用を及ぼす可能性が高い(同名の変数を使用してしまうとスコープ内となるため、簡単に上書きが出来てしまう)
しかしElixirの場合はマクロがdefmacroquote doの内部に独自のスコープを持っており、お互いに影響を及ぼすことが無く、影響範囲さえ理解していれば安全に使うことが出来るようだ

闇の深淵を覗いてしまった...

今回の勉強会を通じて、自身のプログラミングの評価順序?のような概念がぶっ壊れた
pythonやjsぐらいしかまともにやってきたことがないので、コードは書かれている順に評価されていくものだと思い込んでいた

しかし、ながらElixirのマクロでは部分的に先に評価されたり、マクロの中でも評価される順序があったりと、頭がおかしくなりそうだ
非常に勉強になった。もう何も怖くない

参考文献

  • プログラミングElixir