やわらかテック

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

【レポート】第6回清流elixir勉強会inMisoca様を開催しました【競プロ@Elixir】

トピック

今回で第6回目の勉強会を開催致しました
elixir-sr.connpass.com

2019年4月から清流elixirの運営を初めて早2ヶ月
Elixirを全く知らない状態 & 新卒のクソザコでも何とかなりました
昨週、東京に行って感じたことは名古屋はまだ多くの技術がブルーオーシャン
connpass等での勉強会も開催されていないということ

Elixirは元々の知名度がアレですが、例えばReact専用の勉強会(nagoya.js的な)がなかったりして
先輩エンジニアから「名古屋でReactの勉強会あればねー」という話はよく耳にする
Haskel勉強会とかあったら普通に行きたい

こんな僕でも運営できているので、どなたでも始められると思います
自身で勉強会を運営すると顔が広くなっていろんな方と知り合えて
チャンスも生まれるし、強くなれます

本当にオススメです
なければ自分で作る。エンジニアの鑑ですね(唐突な自画自賛

また今回は開催場所に株式会社Misoca様のセミナールームを使用させて頂きました
めちゃ広くて快適な部屋を無料で使用させて頂きました
本当に有難うございます
地味にレンタルオフィスの料金が値上げしてるので非常に助かります

tech.misoca.jp

清流elixir-infomation
開催場所: 丸の内(愛知)
参加人数: 4 -> 3
コミュニティ参加人数 : 8 -> 10 update!!
20190607現在

第6回の活動内容

今まである程度、Elixirの文法の基礎部分は触ってきた
あとはElixirでの実装力を高めていき、サクサクとコードが書けるようになると良い
ということを常連参加者の方から提案があり、今回はAtCoderにある問題をElixirで解いてみることに

コードの成否判定には提案者の方が作ってきてくれたRailsのwebアプリを使った
はえーすっごい。感謝しかない。有難うございます

今回取り組んだ問題はこちらの2問

1問目

整数a,b,cと文字列sが与えられます。a+b+cの計算結果と文字列sを並べて表示する
PracticeA - Welcome to AtCoder
条件
- 1 ≤ a,b,c ≤1,000
- 1 ≤ |s| ≤ 100
入力例
1
2 3
test
出力例
6 test

2問目

1,2,3の番号のついて3つのマスからなるマス目があります。各マスには0か1が書かれています。
いくつの1があるか求めて下さい
条件
マスは必ず1か0
入力例
1000 出力例
1

Elixirでの標準入力の受け取り

これが結構難しい
以下の関数を使用することで受け取りが可能

IO.gets("")
iex(107)> IO.gets("")
111
"111\n"

しかし、このままだと改行記号である「\n」が含まれてしまうので除去したい

iex(108)> IO.gets("") |> String.trim()
111
"111"

任意の回数、入力を受け取りたい場合は自己流ですが以下を採用
第1問目が入力が3回固定なのでrangeで0..2を使っています

Stream.map(0..2, fn _ -> IO.gets("") end)
|> Stream.map(&(String.trim(&1)))
|> Stream.map(&(String.split(&1)))
|> Enum.to_list
|> List.flatten()
iex> Stream.map(0..2, fn _ -> IO.gets("") end)
|> Stream.map(&(String.trim(&1)))
|> Stream.map(&(String.split(&1)))
|> Enum.to_list
|> List.flatten()
1 2 3
4 5 6
test
["1", "2", "3", "4", "5", "6", "test"]

入力を受け取る準備が整ったので上記の2問に取り組んでみる
ちなみに、ここに到着するまで1時間ぐらいかかってしまった
競プロ経験の少なさを反省。かなり興味が湧いたので時間があればゆっくり取り組んでみたい

整数a,b,cと文字列sが与えられます。a+b+cの計算結果と文字列sを並べて表示する

先ほどの複数入力を使用する
まずは入力を受け取るところから

input_ = Stream.map(0..2, fn _ -> IO.gets("") end) |> Stream.map(&(String.trim(&1))) |> Stream.map(&(String.split(&1))) |> Enum.to_list |> List.flatten()
#input_ ["1", "2", "3", "4", "5", "6", "test"]

あとはシンプルで一番最後の値以外を取得して数値に変換して合計すれば良い
脳筋で問題を解くとこんな感じに落ち着いた

input_ = Stream.map(0..2, fn _ -> IO.gets("") end) |> Stream.map(&(String.trim(&1))) |> Stream.map(&(String.split(&1))) |> Enum.to_list |> List.flatten()
sum_ = Enum.take(input_, Enum.count(input_)-1) |> Enum.map(&(String.to_integer(&1))) |> Enum.sum()
str_ = Enum.at(input_, -1)
IO.puts("#{sum_} #{str_}")

#21 test

ブログではお見せできないが無事に全てのcaseでtrueになった

他の参加者の方からインスパイアを受けて2行にまで短縮することができた(パイプでつないでのは1行でカウント)
すごい。発想に脱帽

[str_ | nums] = Stream.map(0..2, fn _ -> IO.gets("") end) 
|> Stream.map(&(String.trim(&1))) 
|> Stream.map(&(String.split(&1))) 
|> Enum.to_list 
|> List.flatten() 
|> Enum.reverse()
IO.puts("#{Enum.sum(Enum.map(nums, &(String.to_integer(&1))))} #{str_}")

さらに突き詰めた結果、ワンライナーで書くことができた

with [str_ | nums] = Stream.map(0..2, fn _ -> IO.gets("") end) 
|> Stream.map(&(String.trim(&1))) 
|> Stream.map(&(String.split(&1))) 
|> Enum.to_list 
|> List.flatten() 
|> Enum.reverse(), do: 
IO.puts("#{Enum.map(nums, &(String.to_integer(&1))) |> Enum.sum()} #{str_}")

1,2,3の番号のついて3つのマスから...いくつの1があるか求めて下さい

こちらは入力が1つで固定なので先ほどより簡単
僕ですらワンライナーで書くことが何とかできた

Stringにクッソ便利なgraphemes()という関数が用意されており
文字列を(eg: "elixir")を1つ1つに分解してくれる(eg: ["e", "l", "i", "x", "i", "r"])
あとはこのリストから"1"以外(stringのまま)を除去してカウントすれば良い

IO.gets("") |> String.graphemes() |> Enum.filter(&(&1 == "1")) |> Enum.count() |> IO.puts()
IO.gets("") |> String.graphemes() |> Enum.filter(&(&1 == "1")) |> Enum.count() |> IO.puts()
101
2

やったぜ。
あれ、競プロっておもろい?

次回の告知

急ですが、来週(6/14(金))の19:30より福岡で活動されています
fukuoka.exさんのもくもく勉強会にリモートでジョインさせて頂くことになりました
fukuokaex.connpass.com

昨週に行ったErlang ElixirFest2019でお話を頂きまして光栄の限りです
開催場所がいつものレンタルオフィスではなく、名駅付近のレンタル会議室となりますのでご注意ください

【実装コード有り】Elixirでpandasっぽいcsvファイルを触るモジュールを作ってみた

ゆるふわな動機

pythonのライブラリにpandasと呼ばれるものがある
厳密にはC言語で書かれているのでpythonかというとそうでもないが
このpandasが提供しているAPIにread_csvというものがあり
csv形式のファイルをdf形式で読み込みpandasでお気軽にゴリゴリと編集することが可能
(shif-jisで読み込んでいるのは日本語を含むcsvのため。defaultはutf-8だったはず)

df = pd.read_csv("sample_data.csv", encoding = "shift-jis")
print(type(df))

#pandas.core.frame.DataFrame

このdfに対して

  • この条件で~
  • この条件で~
  • こういうカラムを追加して~
  • csvに保存

なんてことをよくやる
テキトーにコードを妄想するとこんな感じになる

df = pd.read_csv("sample_data.csv", encoding = "shift-jis")
df = df[df["code"] == "00"]
df = df.query('year > 2010')
df.to_csv("sample_new.data.csv", header=True, index=False)

もしくはdf.assignを使ってゴリゴリやる方法もある
こちらのqiitaの記事が素晴らしいです
https://qiita.com/piroyoung/items/dd209801ca60a0b00c11qiita.com

で、結局何が言いたいの?って話なんですけど、このデータ処理の流れってまさに
Elixirのパイプ演算じゃんってことを思う訳ですね

csv
|> Aカラムを削除
|> Bカラムに空値があるレコードを除去
|> Bカラムの100以下の数値を持つレコードを除去
|> csv形式で.csvファイルを出力

あ、いい。ここに並列処理が加わると...あとは分かるね?

とりあえず関数作った

全体コードはこちら。レポジトリ名は気分でつけました。
github.com

一番良く行う操作 ==> 特定条件での抽出 なのでこの操作を完結できる関数を用意してみた
csvライブラリを使って.csvファイルを読み込んでいる
読み込んだ時点ではデータはStream形式になっているのでStreamを可能な限り保って値を変換するようにしている
課題は並列処理の実装をしていないこと

パターンマッチを使って3種類の条件渡しに対応した

  • 条件(bool値を返す)を関数を渡すパターン(is_function)
  • 複数の値(リスト)からOR検索を行うパターン(is_list)
  • 完全一致する値を渡すパターン(上記以外)

記述は可能な限り、一般化したつもりだが
補助関数に無名関数を渡してて若干可読性が悪いような気もする
すっきりしててElixirっぽくて良いかなと思ってますが
インデントについてはオレ流ですいません

defmodule CsvEditer do
  require CsvColumn
  
  # 関数を第3引数に受け取った場合
  def filter(cdl, column_name, func) when is_function(func) do
    # 引数で受け取った関数を実行する無名関数を作成
    stream_func = fn enum, index, ope_func -> Stream.filter(enum, &(ope_func.(Enum.at(&1, index)))) end
    _filter_helper(cdl, column_name, stream_func, func)
  end

  # 複数の値(リスト)を第3引数に受け取った場合
  def filter(cdl, column_name, val) when is_list(val) do
    # 引数で受け取ったリストの値を持つかどうかを確認する無名関数を作成
    stream_func = fn enum, index, val -> Stream.filter(enum, &(Enum.member?(val, Enum.at(&1, index)))) end
    _filter_helper(cdl, column_name, stream_func, val)
  end

  # 単純に値を第3引数に受け取った場合
  def filter(cdl, column_name, val) do
    # 引数で受け取った値と一致するかどうかを確認する無名関数を作成
    stream_func = fn enum, index, val -> Stream.filter(enum, &(Enum.at(&1, index) == val)) end
    _filter_helper(cdl, column_name, stream_func, val)
  end

  # 一般化された補助関数
  defp _filter_helper(cdl, column_name, stream_func, val) do
    # 受け取ったカラムがリストのどの位置(index)にあるかを検索
    index_num = CsvColumn.val_index_in_header(cdl, column_name)
    body =
      cdl
      |> Stream.drop(1) #headerを一時除去
      |> stream_func.(index_num, val) #受け取った関数を実行
    Stream.concat(Stream.take(cdl, 1), body) #headerをつけて返す
  end
end

CsvColumnモジュール については特に難しいことはしていない

defmodule CsvColumn
  def cdl_header_stream(cdl, false) do
    # header(リストinリストのhead)をリストで返す
    cdl
    |> Stream.take(1)
    |> Enum.to_list()
    |> List.first()
  end
  # Streamのまま返す
  def cdl_header_stream(cdl, _), do: cdl |> Stream.take(1)

  # header(リスト)から引数の値を検索してindex番号を返す
  def val_index_in_header(cdl, column_name) do
    cdl
    |> cdl_header_stream(false)
    |> Enum.find_index(fn val -> val == column_name end)
  end
end

今回、検証に使用したのはこちら
よくサンプルとして広く扱われるiris(ゆり)のデータです

紹介するのを忘れていましたが、こちらがcsvを読み込む関数

defmodule CsvReader do
  def read_csv(file_path) do
    case File.exists?(file_path) do
      true ->
        file_stream =
          File.stream!(file_path)
          |> CSV.decode
          |> Stream.map(fn row ->
            {_, value_lst} = row
            value_lst
          end)
        {:ok, file_stream}
      false ->
        {:error, "#{file_path}: no such file or directory"}
    end
  end
end

以下を目標にデータをゴリゴリする

  • varietyはSetosaのみを抽出
  • sepal.lengthが6.0以下のみを抽出
  • sepal.widthが3.4もしくは3.5の値のみを抽出

では、さっそく
(ちなみにcdlというのは「csv data list」から命名している)

$ iex -S mix

#成功すればstatusとstreamの値が帰ってくる
iex> {_status, cdl} = CsvReader.read_csv("iris.csv")
{:ok,
 #Stream<[
   enum: #Function<63.126435914/2 in Stream.transform/3>,
   funs: [#Function<49.126435914/1 in Stream.map/2>,
    #Function<49.126435914/1 in Stream.map/2>]
 ]>}

# 一回データの中身を見てみる(リストinリスト)
iex> cdl |> Enum.to_list
[
  ["sepal.length", "sepal.width", "petal.length", "petal.width", "variety"],
  ["5.1", "3.5", "1.4", ".2", "Setosa"],
  ["4.9", "3", "1.4", ".2", "Setosa"],
  ["4.7", "3.2", "1.3", ".2", "Setosa"],
  :
  :
  ["4.6", "3.1", "1.5", ".2", "Setosa"]
]

数値(float)への変換はブログ記事の簡略化のため、裏作業で行なった

[
  ["sepal.length", "sepal.width", "petal.length", "petal.width", "variety"],
  [5.1, 3.5, 1.4, 0.2, "Setosa"],
  [4.9, 3.0, 1.4, 0.2, "Setosa"],
  [4.7, 3.2, 1.3, 0.2, "Setosa"]
]

準備完了。あとはパイプで遊ぶだけ

iex> csv |> 
        CsvEditer.filter("variety", "Setosa") |>
        CsvEditer.filter("sepal.length", fn x -> x <= 6.0 end) |>
        CsvEditer.filter("sepal.width", [3.4, 3.5]) |>
        Enum.to_list
[
  ["sepal.length", "sepal.width", "petal.length", "petal.width", "variety"],
  [5.1, 3.5, 1.4, 0.2, "Setosa"],
  [4.6, 3.4, 1.4, 0.3, "Setosa"],
  [5.0, 3.4, 1.5, 0.2, "Setosa"],
  [4.8, 3.4, 1.6, 0.2, "Setosa"],
  [5.1, 3.5, 1.4, 0.3, "Setosa"],
  [5.4, 3.4, 1.7, 0.2, "Setosa"],
  [4.8, 3.4, 1.9, 0.2, "Setosa"],
  [5.0, 3.4, 1.6, 0.4, "Setosa"],
  [5.2, 3.5, 1.5, 0.2, "Setosa"],
  [5.2, 3.4, 1.4, 0.2, "Setosa"],
  [5.4, 3.4, 1.5, 0.4, "Setosa"],
  [5.5, 3.5, 1.3, 0.2, "Setosa"],
  [5.1, 3.4, 1.5, 0.2, "Setosa"],
  [5.0, 3.5, 1.3, 0.3, "Setosa"],
  [5.0, 3.5, 1.6, 0.6, "Setosa"]
]

おぉ...ええやんけ
パイプ演算子のおかげで何がしたいのかが一発で分かる
問題は速度。記事の文字数が多くなったため、別記事で検証してみようと思う

ただ、可読性はあきらかに上がった(個人的にね

レッツElixir

岐阜県民だけどErlang&ElixirFest2019のために初ソロ東京に行ってきた

Erlang & ElixirFest2019とは

f:id:takamizawa46:20190602130917j:plain Erlang & Elixirが大好きな人やら興味のある人が集まる万博みたいなお祭り
年に一度開催されていらっしゃるようで今年で3度目になるそうです

Erlang & ElixirFest2019
elixirconf.connpass.com

開催場所はご想像の通り、東京です
Erlang、Elixir界隈ではかなり名のある方々が集まり、かなり技術レベルの高いカンファレンスとなっています

他の方と比べるとElixirとErlangの知見はほぼ皆無に等しいですが 岐阜に住んでて名古屋で仕事していると、こういうハイレベルな空気感に触れられる機会は、ほぼ皆無と言っていいので

  • ウン万円払ってでも!!
  • 恥をかいてでも!!

と割り切って思い切って行ってみました
ただ東京に行くのが人生二度目かつソロで新幹線乗ったのが初めてだったので、めちゃくちゃ迷いました
田舎者丸出しで恥ずかしや〜💦

内容について

以下、connpassのページから引用です

  • Keynote: Erlang/OTP と ejabberd を活用した Nintendo Switch(TM)向けプッシュ通知システム「NPNS」の開発事例 (任天堂 ネットワークシステム部 渡邉大洋)
  • 超並列高速実行処理系 Hastega 〜 Lonestar ElixirConf 凱旋帰国 (山崎 進)
  • Nervesが開拓する「ElixirでIoT」の新世界 (高瀬 英希)
  • 工場の制御をElixirで 〜ラダー・ロジックを実行する〜 (菊池 豊)
  • Elixir プロセス入門
  • ElixirでやるDDD (ドメイン駆動設計) (ne_Sachirou)
  • Phoenix1.4とVueによるサービス構築のノウハウ (山口 強)
  • ProtocolBuffer(proto3)をElixirで実装する (にく)
  • 4才から24才までプログラミングを教えた結果、高校生にElixirを教えるに至った気付き (YOSUKENAKAO.me)
  • XFLAG × スポーツ × Elixir (村山 寛明)
  • Serverless BEAM with FaaS (大原 常徳)
  • ロマサガRS における Elixir サーバー開発実践 〜生産性を上げてゲームの面白さに注力〜 (梶原 星平/関山 友輝)
  • Erlang/OTP で作るリアルタイムサーバー (清水 佑吾)
  • Erlang/OTP で WebRTC と QUIC (voluntas)
  • Enum.mapから始めるElixirデータサイエンス(piacere)

圧倒的...ッ!!! ボリューム...ッ!!
ラーメンに餃子とチャーハンと天津飯と唐揚げつけて頼んだかってぐらい
一日でこれだけの情報量を摂取できるのやばい
半分ぐらいは自分のレベル感で理解できる内容だったと信じているが
技術レベルの差にただ頷くことしかLTもあり本当に世の中は広いなと感じざるを得なかった

その反面、私の主催している「清流elixir」の常連メンバーが
elixir-sr.connpass.com

普通にErlang&ElixirFest2019に来ていて、世間は狭いなと思った
Fest行きたいなって決定打に思ったのが今回のKeynote
任天堂ErlangかElixirをswitchの通知システムに使ってるらしいよ」って話は風の噂で聞いたことがあった
ただ、その部分の任天堂さんのオープンな話(事実含めて)ってどこにも落ちてなくて
今回公の場に登壇されてお話できる範囲で詳細を初めて語って下さりました
終了後に拍手が鳴り止まず、何と色々と許可が出れば書籍化されるとかされないとか

有志の方がLTのリンクや関連スライドをqiitaの記事にまとめて下さっているので展開しておきます
qiita.com

20190601追記
keynoteでの渡邊さんのLT資料が公開されたので貼っておきます
Keynote: Erlang/OTP と ejabberd を活用した Nintendo Switch(TM)向けプッシュ通知システム「NPNS」の開発事例 (任天堂 ネットワークシステム部 渡邉大洋)
speakerdeck.com

以下、気になった&理解できているLTの内容についてざっくりとまとめてます
内容は理解できたレベルでまとめているが誤りがあれば教えてください

超並列高速実行処理系 Hastega

Elixirの並列処理ライブラリのFlowにインスパイアを受けてより速いものを作りたいという
研究者ならではの視点から開発を進められているライブラリ
GPU上でも動作し、Flowの10倍, pythonの3倍で動作するようです(化け物か

Elixir界隈では - 再帰関数 - Enum.map

を使うかでしばしば争いが起こりますが
山崎さんは後者を「Elixir Zen Style」と名付られ、こっちの方がいいじゃんと
ただ実は再帰関数の方がEnum.mapよりもパフォーマンスが良いらしいが
Hastegaを使えば、この事実がひっくり返りElixir Zen Styleで速く美しいコードがかけるとのこと

名前はFinalFantasyのヘイスガ(パーティのスピードを上げる魔法)から来ているとのことですが
実はJose(Elixirの作者)はFFをやったことがないことが判明して、会場がドワッと笑いに包まれた

僕もElixirでFFから来ていると思ってたのでワロタ
hypeやnifっていう話が前提知識がなさすぎてついていけなくなってしまったので
別の機会にインプットして記事にでもしてみようと思う

Nervesとラダーロジック

僕のイメージElixirはweb系でサーバーやらネットワークの開発がメインでしょってのがぶち壊された
IoT(ラズパイの規模から発電所)に対してもElixirはガンガン使えるぜっていうLTで
普段自分が目を向けないレイヤーの話で非常に面白かった

会場ではラズパイを動かすデモもあり、起動した際には拍手が沸き起こった(ウオオオオオ

Nervesが海外ではPhoenixと同レベルの立ち位置にいるらしく、どれだけ注目されているかが分かる
なんと2019 09/06(木) ~ 09/07(金)にかけて下呂(岐阜県やで)にてNervesの作者がやってくるそうです
岐阜はんぱないって!!

ラダーロジックというものに関しての知識は業界不一致で皆無だったが
発電所などをクラウドで管理するにはElixirは要件を満たしくれるとのこと
pythonを使って再帰で動かした際には異常があった場合に例外投げてしまって
現状どうなっているのかが分からず、管理に凄く苦労した」

「Elixirのプロセスであれば一回、killして立ち上げ直すだけだからぴったり」

おっしゃる通りです
この2つのLTからElixirの組み込み, IoT業界での伸び代を感じぜざるを得ませんでした

Elixir プロセス入門

spawnから始めり、メッセージを送受信して、Task触って、死活管理して....
個々の知識はあって穏やかな心で聞くことが出来た

Elixirの並列処理についてどの順で学べばいいかということがフロー化されており
自分のほしかったものはこれなんだよな感が半端ない

このまま清流elixirの勉強会に使えそうで非常にありがたいです

Enum.mapから始めるElixirデータサイエンス

僕が最初にElixirを選んだ理由はまさにこれ
データ処理のしやすさ(Enum)と軽量プロセスによる並列処理
並列にゴリゴリとデータ処理ができたらpythonの弱点を克服できるやんけ!!
こんなんデータ処理のためにあるみたいなもんじゃんと調べている内に先駆者であるpiacereさんを発見

LTの内容も好きな物尽くしでElixir愛がビシビシ伝わってきました

Erlang/OTP と ejabberd を活用した Nintendo Switch(TM)向けプッシュ通知システム「NPNS」の開発事例 (任天堂 ネットワークシステム部 渡邉大洋)

凄かった。さすが任天堂やで...

この一言に尽きる
詳しい話はLTを見て頂ければいいので思ったことを書く

本質的に行なっていることは自分と変わらないと思った
問題があってそれを解決するということ(ただ数字がコネクション1億とか笑えた)

1つの課題を達成するのに半年かかったこともあるとのことで
半年間の間、正解も分からない暗い森を歩き続けられるマインドが凄い

あとは会社の技術力にも脱帽
先輩エンジニアたちの知見があるからこそ成せる技。さすが任天堂

あとErlangやりたくなった。正確にはもっとErlangについて知りたくなった

総評

いい空気を吸うことができた
いくらネットで調べたら出るといえども、地方にいると技術の風を肌で感じることができない
とくにElixirっていう日本ではマイナーな部類に入る言語ならなおさら

改めて自分の技術レベルの低さを感じた
少しでも追いつけるようにインプットしなければと思う
強くなるきっかけを得られてErlang&ElixirFest2019に本当に感謝です

おまけのコーナー

休憩時間中に行われたハンズオンのコードを置いておきます

ErlangElixirFestHandsOn2019
github.com

以下を実装

(1) 1000以下のランダムな数を生成し、その時間(ミリ秒)スリープした後に、その数字を返す
(5) (1)の処理を100回、並行に実行する

ランダム数値の作成やsleepの仕方、Taskについては上記のレポジトリに記述がある
Taskについては以下の記事でも触れている

www.okb-shelf.work

defmodule HandsOn do
  def random_and_sleep(sec) do
    r_num = :rand.uniform(sec)
    IO.puts("stop: #{sec} ")
    :timer.sleep(sec)
    r_num
  end
  
  def main(sec, parallel_num) do
    Enum.map(0..parallel_num, fn _ -> Task.async(HandsOn, :random_and_sleep, [sec]) end)
      |> Enum.map(&(Task.await(&1)))
  end
end

sec = 100
parallel_num = 100
IO.inspect(HandsOn.main(sec, parallel_num))

結果

stop: 100
stop: 100
stop: 100
:
:
stop: 100
[84, 52, 86, 8, 23, 79, 29, 67, 28, 5, 17, 69, 98, 18, 98, 39, 94, 73,
 59, 78, 36, 25, 22, 77, 59, 60, 2, 86, 83, 29, 24, 97, 70, 18, 11, 89,
 34, 25, 27, 80, 61, 37, 53, 80, 26, 60, 22, 11, 43, 40, ...]

【Elixirで学ぶCS】Elixirで論理ゲートを実装するまで

なにこれ

僕は一応、理工学部の出身ではありますが建築土木が専攻でした
この業界にいながらcsについての知識が皆無
前からやらねば...やらねば..と思ってはいたが触れる機会がない & 一度挫折済み

しかし、アルゴリズムの勉強を始める中でデータ構造の重要性に気づき
データがコンピューター内部でどのように動いているか非常に気になった

良い機会だと思って「コンピュータシステムの理論と実装」を一読することに...
www.oreilly.co.jp

各説明は本を読んだ方が早いのでざっくりの説明で
実装部分をメインにブログで展開していくつもりです

ブール理論

コンピューターは一番下のレイヤーにいけば全て0と1(bool値)の組み合わせで構築されている
この組合せの数をたくさん増やす + 状態を保存(メモリやら)を使えばいろんな計算が出来るよねって話
このbool値を組み合わせて作るのが論理ゲートと呼ばれるもので

  • NOT
  • OR
  • AND

やらがある
さらにこれらの論理ゲートは全て、ベースとなる「NAND」という論理ゲートから作成することが可能
つまりはNANDさえ作れてしまえば全ての論理ゲートは実装可能となる
小さな世界から大きな世界が構築することが出来る(細胞が集まって生物になるみたいな感じ)

Elixirでの実装

NANDさえ作ってしまえば、他の論理ゲートは作成可能なのでまずはNANDから作成する

NANDゲート
NANDゲートは以下のような値を出力する関数とみなす

x y NAND(x, y)
0 0 1
1 0 1
0 1 1
1 1 0

ちょちょっと実装する
(1, 1)の時だけ0を返すようにしてある

defmodule RogicalGate do
  def nand(1, 1), do: 0
  def nand(_x, _y), do: 1
end

テストライブラリを使わずに簡単なテストを行なって
正しい値を出力しているかをチェックする

nand_test = [
  RogicalGate.nand(0,0) == 1,
  RogicalGate.nand(1,0) == 1,
  RogicalGate.nand(0,1) == 1,
  RogicalGate.nand(1,1) == 0,
]

Enum.map(nand_test, &(IO.puts(&1)))
true
true
true
true

あ、いいっすね

NOT
値を反転(0を1に, 1を0に)するのみ

x NOT(x)
0 1
1 0
defmodule RogicalGate do
  def nand(1, 1), do: 0
  def nand(_x, _y), do: 1
  def not_(x), do: nand(x, x) <-- 追加
end

テストを実行

not_test = [
  RogicalGate.not_(0) == 1,
  RogicalGate.not_(1) == 0,
]

Enum.map(not_test, &(IO.puts(&1)))
true
true

OR

x y OR(x, y)
0 0 0
1 0 1
0 1 1
1 1 1

NANDを3つ組み合わせて実装

defmodule RogicalGate do
  def nand(1, 1), do: 0
  def nand(_x, _y), do: 1
  def or_(x, y), do: nand(nand(x, x), nand(y, y)) <-- 追加
  def not_(x), do: nand(x, x)
end

テスト

or_test = [
  RogicalGate.or_(0, 0) == 0,
  RogicalGate.or_(1, 0) == 1,
  RogicalGate.or_(0, 1) == 1,
  RogicalGate.or_(1, 1) == 1,
]

Enum.map(or_test, &(IO.puts(&1)))
true
true
true
true

AND

x y AND(x, y)
0 0 0
1 0 0
0 1 0
1 1 1

すでに実装したnot_を使ってANDを実装する

defmodule RogicalGate do
  def nand(1, 1), do: 0
  def nand(_x, _y), do: 1
  def or_(x, y), do: nand(nand(x, x), nand(y, y))
  def not_(x), do: nand(x, x)
  def and_(x, y), do: not_(nand(x,y))
end

テスト

or_test = [
  RogicalGate.and_(0, 0) == 0,
  RogicalGate.and_(1, 0) == 0,
  RogicalGate.and_(0, 1) == 0,
  RogicalGate.and_(1, 1) == 1,
]

Enum.map(or_test, &(IO.puts(&1)))
true
true
true
true

いいっすね
次は作成したAND, NOT, ORを組み合わせてXORを作成してみる

XOR
(1, 0)か(0, 1)の際に1を出力させる
ORと異なるのは(1, 1 )の場合に0が返ってくるという点

x y XOR(x, y)
0 0 0
1 0 1
0 1 1
1 1 0

先ほど作成した論理ゲートをフルに使って実装する

defmodule RogicalGate do
  def nand(1, 1), do: 0
  def nand(_x, _y), do: 1
  def or_(x, y), do: nand(nand(x, x), nand(y, y))
  def not_(x), do: nand(x, x)
  def and_(x, y), do: not_(nand(x,y))
  def xor_(x, y), do: or_(and_(x,not_(y)), and_(not_(x),y))
end
xor_test = [
  RogicalGate.xor_(0, 0) == 0,
  RogicalGate.xor_(1, 0) == 1,
  RogicalGate.xor_(0, 1) == 1,
  RogicalGate.xor_(1, 1) == 0,
]

Enum.map(xor_test, &(IO.puts(&1)))
true
true
true
true

nビット論理ゲートへの対応

今まで作成したゲートをnビットの場合にも実行できるようにする
nビットといってもただ入力がリストになって複数入力されるだけで大したことはない

こんな関数を用意した
入力された2次元のリストから値を取り出して、引数から受け取った関数を適宜実行していく
notの場合はリストのサイズが1になるのでcaseで条件を分岐させている

def nbit_converter(inputs, gate_func) do
    Enum.map(inputs, fn row -> 
        case Enum.count(row) == 2 do
          true ->
            [x, y] = row
            gate_func.(x, y)
          false ->
            [x] = row
            gate_func.(x)
        end
      end
    )
  end

この関数を使ってnandなどをnビット対応にラップする
関数を引数で渡す場合には &モジュール名.関数名/引数の数 とすることで可能

defmodule NbitRogicalGate do
  def nbit_converter(inputs, gate_func) do
    Enum.map(inputs, fn row -> 
        case Enum.count(row) == 2 do
          true ->
            [x, y] = row
            gate_func.(x, y)
          false ->
            [x] = row
            gate_func.(x)
        end
      end
    )
  end
  def nand(inputs), do: nbit_converter(inputs, &RogicalGate.nand/2)
  def not_(inputs), do: nbit_converter(inputs, &RogicalGate.not_/1)
  def or_(inputs), do: nbit_converter(inputs, &RogicalGate.or_/2)
  def and_(inputs), do: nbit_converter(inputs, &RogicalGate.and_/2)
  def xor_(inputs), do: nbit_converter(inputs, &RogicalGate.xor_/2)
end

実行結果を見てみる

info = [
  [0, 0],
  [1, 0],
  [0, 1],
  [1, 1],
]

info_for_not = [
  [1],
  [0]
]

res = [
  NbitRogicalGate.nand(info),
  NbitRogicalGate.not_(info_for_not),
  NbitRogicalGate.or_(info),
  NbitRogicalGate.and_(info),
  NbitRogicalGate.xor_(info),
]

Enum.map(res, &(IO.inspect(&1)))

先ほどの表どおり、値が上手く出力された

[1, 1, 1, 0]
[0, 1]
[0, 1, 1, 1]
[0, 0, 0, 1]
[0, 1, 1, 0]

コンピューターで最も基本的なブール理論の論理ゲートの実装が何とか出来た
この論理ゲートの幾多なる組み合わせでコンピューターが動いていると考えると
考えた奴はマジで天才すぎる

【レポート】第5回清流elixir勉強会in丸の内を開催しました【再帰関数】

トピック

今回で第5回目の勉強会を僕の運営しているコミュニティで開催することができました
清流elixir

先週にはfukuoka.exを運営されているpiacereさんと Twitterで繋がらせて頂きまして多くの方に清流elixirの名を知って頂けました
本当にあざます!!!
やる気がめっさ出ました!!

清流elixir-infomation
開催場所: 丸の内(愛知)
参加人数: 3 -> 4 update!!
コミュニティ参加人数 : 5 -> 8 update!!
20190525現在

第5回の活動内容

関数型言語といったらやっぱり再帰関数でしょってことで軽いノリでこのテーマに
【自分的レシピ】elixirでの再帰関数の動かし方でも触れたように
関数型言語には一般的にfor文のような、いわゆるループ処理は用意されていない
じゃあ、どうやって書くのよ?という問いに対する答えは「再帰関数」を使おう

再帰関数ってなんぞ

関数が自分自身を呼び出す処理のこと
5回だけ「"hello"」と出力する再帰関数を作るとする
コードに落とし込むとこんな感じ

defmodule Sample do
  def hello do
    IO.puts("hello")
    hello()
  end
end

Sample.hello()

関数が自分自身を呼び出しているのが分かる
ただし、このままこのコードを実行するとまずい。無限再帰になってしまう
「いつ停止するの?」という問題がある
今回は5回「"hello"」を出力させた時点で再帰を終了させる必要がある
これは「停止性」といい、本当にこのコードが終了するかを保証する必要がある(停止性の議論)

たとえば5回で終了させたいならばこんな風に書き換える

defmodule Sample do
  def hello(counter) do
    if counter == 5 do
      :fin
    else
      IO.puts("hello")
      hello(counter+1)
    end
  end
end

Sample.hello(0)
# hello
# hello
# hello
# hello
# hello

きちんと再帰が停止した
helloの引数に追加したcounterというのはアキュムレーターと呼ばれるもので
貯蔵庫のような意味があり、状態を保管するために使用している
今回の場合は何回呼び出したか?ということをカウントするためのカウンターとして扱っている
アキュムレーターについての詳しく話はこちらで解説しているのでぜひ

ただこのコードはElixirっぽいくない上にダサいので書き換える

defmodule Sample do
  def hello(counter) when counter == 5, do: :fin
  def hello(counter) do
    IO.puts("hello")
    hello(counter+1)
  end
end

Sample.hello(0)
# hello
# hello
# hello
# hello
# hello

これでも上手く動く

defmodule Sample do
  def hello(5), do: :fin
  def hello(counter) do
    IO.puts("hello")
    hello(counter+1)
  end
end

参加者の方が面白い書き方を発見した(凄い
「helloにdefaultの値を持たせたらhelloの呼び出し時に0を渡す必要がなくなるのでは?」 fmfm...

defmodule Sample do
  def hello(5), do: :fin
  def hello(counter \\ 0) do
    IO.puts("hello")
    hello(counter+1)
  end
end

Sample.hello()

実行すると上手く動くが以下のような警告が出る

warning: definitions with multiple clauses and default values require a header. Instead of:
def foo(:first_clause, b \ :default) do ... end
def foo(:second_clause, b) do ... end

errorの意味は分かるが、修正の方法が思いつかず
再帰関数作るときはアキュムレーターにdefault引数渡すの良くなさそうということで一旦落ち着いた
詳しい原因をご存知の方は教えてください

再帰関数で色々作って遊んでみる

f:id:takamizawa46:20190525130609j:plain:w450

配列の先頭の要素を取得して出力する
headとtailを使って実装する
リストが空となった場合に停止するように関数を実装する

sample = [1,2,3,4,5]
[head | tail] = sample
defmodule Sample do
  def fetch([]), do: :fin
  def fetch([head | tail]) do
    IO.puts(head)
    fetch(tail)
  end
end

Sample.fetch([1,2,3,4,5])
# 1
# 2
# 3
# 4
# 5

配列の要素を合計する
アキュムレーターに先頭の要素をどんどん足していく

defmodule Sample do
  def sum([], accum), do: accum
  def sum([head | tail], accum), do: sum(tail, accum+head)
end

IO.puts(Sample.sum([1,2,3,4,5], 0))
#15

このままだと呼び出し時に0を渡すという操作があり
アキュムレーターの知識がない人には煩わしいのでhelper関数というものでラップする

defmodule Sample do
  def sum(lst), do: _sum(lst, 0)
  defp _sum([], accum), do: accum
  defp _sum([head | tail], accum), do: _sum(tail, accum+head)
end

IO.puts(Sample.sum([1,2,3,4,5]))
#15

これならリストを渡すだけでsumをアキュムレーターを意識せずに使用することが可能

配列の大きさをカウントする

defmodule Sample do
  def length([], accum), do: accum
  def length([_head | tail], accum), do: length(tail, accum+1)
end

IO.puts(Sample.length([1,2,3,4,5], 0))
#5

良い感じですね
言語に実装されているEnumの関数は再帰関数を使えば再現することが可能
明日から新たなモジュールを開発できる。やったぜ

悲しいお知らせ

本日の勉強会で作成した再帰関数はお察しの通り、大体はEnumの関数やらの組み合わせで作成することが可能
再帰関数を作成するまえに必ず、この処理はEnumやらを使って実装することが出来ないかを考える必要がある
再帰関数で書いてもいいけどね。スピード感を大事にしたい

嬉しいお知らせ

20190601(土)に東京で開催される
Erlang & Elixir Fest 2019に行って参ります
恥ずかしながら、東京にいくのは人生で2度目で迷わずに行けるかが心配..

清流elixirを代表して参加しようと思っていたら
いつも勉強会に来て頂ける参加者の方が全員参加するようでワロタ

こんなすげー方達の話を聞ける機会は滅多にないので楽しんできます
次回の勉強会はElixirの実装力を上げるために競プロ問題をいくつかElixirで解いてみようと思ってます