やわらかテック

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

【デプロイまでわずか10分】JaveScriptでcloud-functionのデプロイまでを体験した

何かサーバーレスたるものが流行ると聞く

サーバーレスって何やねん
APIなどがcallされた時に動き出して、処理が終了したらまたさよなら
常時サーバーが起動してない。呼ばれた時にパッと現れパッと消える インフラを作る必要なし。コード(関数)のみをデプロイするだけで良いと

僕はインフラの構築が一番苦手なので助かります
コード(関数)の記述に集中できるのでありがたい

CloudFunctionを使うまでの準備

今回はこちらの動画を参考に
Node.js(TypeScript)でサーバーサイドを記述しますが、正直TypeScriptの知識いらないです
Node.jsをちょろっとしか書いた事ないですが問題なく行けました

インストール

Node.jsのインストール
僕はmacを使っているのでbrewコマンドで一発です

> brew install node
> node -v
v10.9.0

その他環境でのインストールについてNode.jsのダウンロードページから
インストーラーをダウンロードして手順に沿ってインストールしていけば良いかと思います

Firebase CLIのインストール
Node.jsをインストールした際にnpmも共にインストールされています

 >  npm -v
6.4.1

npmがインストールされていることを確認した後に「firebase-tools」をインストールします
グローバルインストールしたいので -g というオプションをつけます

> npm install -g firebase-tools

この時点でローカルでの準備は終了です

firebaseにプロジェクトを追加
プランはspark(無料プラン)を選択してFirebaseのコンソールから
新規のプロジェクトを追加をクリックしてこんな感じに設定していきます

f:id:takamizawa46:20190415080809p:plain:w450
Firebaseの新規プロジェクト設定

ロケーションは適当に選んでますが無料プランなのでまぁいいでしょう
これでクラウドでの準備も整いました

Firebaseプロジェクト(ローカル)での作成

ローカルにプロジェクトを作成します Firebaseの認証が通るように、というかマナー的な感じで下記コマンドを叩きログインします

> firebase login

現時点でログインしていない場合はブラウザの新規タブが開きログインしましたで
ってな文言が表示されます

すでにログインされていた場合には

> firebase login
Already logged in as sample@gmail.com

と表示されます
ではログインもしたところで作業ディレクトリしてFirebaseのプロジェクトを開始します

> mkdir sample-functions
> cd sample-functions
> firebase init

と進めているとエラーが...

Error: HTTP Error: 401, Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.

401なので認証が通ってないっぽい。ログインしたはずなのに...
このコマンドで解消されました。再認証することを明示的に伝える必要があるのか

> firebase login --reauth

改めてfirebase initを叩く
矢印(移動)とspace(選択)とenter(決定)で目的を選択する

流れに沿って順に選択していく

? Which Firebase CLI features do you want to setup for this folder? Press Space to select features, then Enter to confirm your choices.
(どのプロジェクトをセットアップします?)
Functions: Configure and deploy Cloud Functions

? Select a default Firebase project for this directory:
(標準の設定を選択しますか?)
[don't setup a default project]

? What language would you like to use to write Cloud Functions?
(言語は何を使いますか?)
TypeScript

? Do you want to use TSLint to catch probable bugs and enforce style? (Tslint(構文チェックライブラリ)を使用しますか?)
Yes

? Do you want to install dependencies with npm now?
(npmの依存関係をインストールしますか?)
Yes

これでプロジェクトの準備は完了です
標準でpackage.jsonにfirebase-adminのfirebase-functionsバージョンが記載されていますが
最新でない場合があるそうなので

> cd functions
> npm install firebase-admin firebase-functions

と叩き、確実に最新バージョンにしておきましょう

いよいよデプロイする(もうデプロイする)

index.ts(もしくはindex.js)にすでにサンプルのコードが用意されているのでこちらを使用します
リクエストを受け取ったら"Hello from Firebase!"を送信する超シンプルな関数です

import * as functions from 'firebase-functions';

export const helloWorld = functions.https.onRequest((request, response) => {
 response.send("Hello from Firebase!");
});

これコードの準備は終了したので後はクラウドのプロジェクトにデプロイするのみです このコマンドを叩くだけです。あっさり

> firebase deploy
> firebase deploy --project projectID(Firebaseの設定に記載されている)

僕の場合、「--project projectID」がないとエラーになったので--projectを付与していますが
firebase deployでもデプロイは通るみたいです

 ❯  firebase deploy --project cloud-function-114514

=== Deploying to 'cloud-function-114514'...

i  deploying functions
Running command: npm --prefix "$RESOURCE_DIR" run lint
:
:
✔  Deploy complete!

Please note that it can take up to 30 seconds for your updated functions to propagate.
Project Console: https://console.firebase.google.com/project/cloud-function-114514/overview

わちゃわちゃとよしなにやって頂いてデプロイが無事に完了したようです
一番最後の行にURLが記載されており、今デプロイしたコードがAPI化されこのパスから確認することが可能です

対象パスに飛んでみると

Hello from Firebase!

と表示されているでしょう
エンドポイントまでを自動生成。マジで便利すぎる...
たった10分ばかしでAPIの作成からデプロイまでが終了しました
技術の進歩は恐ろしいですね

次の記事ではFirebaseのDatabaseを使ってもう少しレベルアップしたAPIの作成をやってみます

新卒がconpassで新規コミュニティ作って勉強会開いてみて得られた知見

なんで新規コミュニティを作ったのか

元々は社内で勉強会をちょろっとやってたんですが、中々上手くいかず...
一番しんどかったのは毎回リソースを用意して体験してもらうっていう

「主催者」 -> 「参加者」

一方的な関係が正直なところ苦手です
僕の理想では

「主催者」 <-> 「参加者」

お互いが情報を共有できるような勉強会なんですよね
何か学校の授業みたいでしんどい...
興味のないことを一方的に話されたり、説明されるのしんどい...
ということもあって続かず仕舞いになりました

ただ、前から自分のコミュニティを持ちたいなってことはずっと思ってました
そんな中、たまたまelixirという言語と出会いぼっちでずっと勉強してたんですが

  • 1人でやるのつらい
  • 同じものに興味がある人と繋がりたい
  • 共にインプット、アウトプットしたい

と思いました

色々、方法を考えたんですけど極力お金を削減したかったので
無難に無料で使えるconnpassを選択しました

一番時間がかかったのはコンセプトを決めること

難しかったのは「コンセプト」を明確にすることだったかなと思います
上でちょこっと触れてますが僕が作りたい勉強会ってのはこういう関係

「主催者」 <-> 「参加者」

これを文字に落とし込んで納得してもらうにはどうすればと結構悩みました
プラスでどうすればこのコンセプトが実現できるだろうかと
ざっくりプラン(要件定義)を考えないとおそらく数回で詰みます

清流elixirのコンセプト(20190414現在)

僕自身もelixirについて現状(2019/03/10)全く知識がないので(プログラミグelixirを半分読んだぐらい)
こちら(開催側)がリソースを用意して勉強してもらうという一般的な勉強会コミュニティやもくもく会ではありません
「勉強会って敷居が高いし、前提知識が必要じゃん...」と懸念する心配はなく、共にゼロから学ぶことを目標にしています
すでにelixirにお詳しい方は僕のような雑魚にアウトプットしてさらに強くなって頂ければと思います
開催者と参加者が講師と生徒のような関係ではなく互いにジャンキーに知識を共有しましょう!!

強調したかった点

  • 自分自身もほとんど初心者であること
  • 一方通行の勉強ではないこと(参加者にもアウトプットの可能性がある)
  • 初心者さよならフィルターを作らない
  • 強い人が来ても問題ない

勉強会自体はまだ2回しか開催できてませんが
自分では出来としては悪くなかったと思ってます
このコンセプトを元に実際に勉強会をやって反省した点は
「最初の動き出し部分はある程度決めておく必要があること」です

毎回、集まって勉強会をやるにしても何をテーマとしてその日のゴールを何にするのかは
最低限決めておく必要があると思いました
そうしないといつまでたっても勉強時間がスタートしないです

ダメな例: パイプ演算子を触る(どう触るのかを決める必要がある->何がゴール??)
goodな例: パイプ演算子を触って配列データをいじくる(何がゴールかがある程度決まっている)

2回目からは

  • 前回こういうの分からなかった
  • ここの部分をもっと知りたい

というフィードバックがもらえるので気持ちは楽です
そういった意味で初回の勉強会のテーマの重要性を身に染みて感じます

コンセプト & 初回テーマ が動きやすさを決める要因でしょう

勉強会を開くまでのステップ

こんな感じでコンセプトさえ決めてしまえばあとは簡単です
connpassでグループを作成してイベントをグループ主催で開催するのみです
公式ページのこちらに詳しく記述されているのでこの手順に沿って文字を打ってくだけ

一番困るのは開催場所だと思います
自前のスペースや施設があれば問題ありませんが、僕のようなものにはそんなものありません
いつも僕が使っているのは
instabaseというサービスです

全国各地のレンタルスペースをクレカ1枚で予約可能です
地域にもよりますが愛知だと1時間500円行くか行かないぐらいで割とリーズナブルで
朝昼の時間帯はもっと安いです

おめでとうございます。この時点で

  • 自分のコミュニティの作成
  • 勉強会の開催

については完了しています
あとは...

人はどうやって集めるのか

ここの知見は現状ほとんどないので偉そうな事は言えませんが1つ実感したことがあります
SNSの効果は絶大だということです

以前のブログを運営していた時は恥ずかしいからtwitterに記事をupしたぜって通知ツイートはやってなかったんですけど
試しにconnpassで勉強会開きましたってツイートすると
めっちゃ見られてる... 僕のこの時点でのフォロワーは250人ぐらいです

f:id:takamizawa46:20190414111345p:plain:w400
実際のツイートアクティビティ

このツイートがあるかないかだけで300人近くの人に見てもらえるかどうかが変わると考えると凄い

知り合いにも声をかけられました
「何か勉強会開くらしいじゃん?」って感じで
本当にみんな意外と見てるんだなと痛感

SNSの力は本当に偉大です
もっと集客できるようなノウハウは身につけていきたいです

まとめ

「割とノリで何とかなる」この一言につきる
年齢とかキャリアとか関係なしにやるかやらないかですね

やってれば自然と見てもらえるし人も来るということに気づきました
迷ってるならやった方がいいです

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

トピック

f:id:takamizawa46:20190413153340j:plain:w500
第2回清流elixir勉強会
昨日、無事に第2回の勉強会を開催させて頂きました
第1回目では方針を決める時間が長く、手を動かす時間が全くなかったのですが
今回の勉強会では「パイプ演算子を触る」というテーマで
普通に真面目に勉強しました(そこそこ真面目に

第1回では参加者は僕を含めて2人だったのですが、 今回、新たな方に参加して頂けまして3人での勉強会となりました

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

第2回の活動内容

Enumについて

パイプ演算子を使ってデータをごちゃごちゃやりましょうっていうテーマで
どれだけelixirがデータ処理に対して強力なのかを体験しました

合わせてEnumモジュールについての知識が必要になりましたのでざっくりと解説しました
優しく始めるelixirのEnumと簡単な使用例の方でそこそこ詳しく解説してますので
参考にして頂ければ幸いです

f:id:takamizawa46:20190413153432j:plain:w400
Enumと列強可能なデータ構造について

Enumモジュールが使えるデータ構造は以下の3つ

  • リスト(list [])
  • レンジ(range n..n+m)
  • マップ(map %{})

このEnumとパイプ演算子の組み合わせがどれだけヤバいかを触ることに

パイプ演算子について

numbers = [1, 2, 3, 4, 5]

res = func1(numbers)
res = func2(res)
res = func3(res)
res = func4(res)
print(res) #最終的な目標値 

こんな感じである関数の戻り値を使って、次の関数の引数に何回も渡すことってわりとある気がします
ただ毎回resに戻り値いれて、次の関数の引数にいれてって単純にめんどくさいし、どこで何か起きてるのか後に分かりにくい

ただelixirにはパイプ演算子という強力な構文が用意されており、上記のようなことが簡単に行える

f:id:takamizawa46:20190413154645j:plain:w400
パイプ演算子について

numbers = [1,2,3,4,5]
res = Enum.map(numbers, fn ...)
        |> Enum.map(fn ...)
        |> Enum.map(fn ...)
        |> Enum.map(fn ...)
IO.inspect(res)

まるで工場のベルトコンベアのように順を追うことができて何がしたいのかがパッと見れば分かる(川の様との意見も

実際に試してみた

最初はシンプルな問題に取り組んでみた

data: 1...100(range)
1. 全ての要素を8倍
2. 500以上の要素を削除
3. 残りの要素を足し合わせる

data = 1..100
data
  |> Enum.map(fn x -> x * 8 end)
  |> Enum.filter(fn x -> x <= 500 end)
  |> Enum.sum() 

#result: 15624

ここではelixirの文法を確認しつつも、特に詰まる部分はなかった
しかし、あえてsumを使わずにreduceを使って実装してみることに(マゾ
reduce使えばできることってだいたいEnumに実装されているのでreduceの活躍できる場所は中々難しいです
reduceの詳細は優しく分かるEnumのreduce関数と簡単なサンプルで解説しています

data = 1..100
data
  |> Enum.map(fn x -> x * 8 end)
  |> Enum.filter(fn x -> x <= 500 end)
  |> Enum.reduce(fn x, accum -> x + accum end)

#result: 15624

提案を頂き、ついでにreduceで最大値も求めることに

data = 1..100
data
  |> Enum.map(fn x -> x * 8 end)
  |> Enum.filter(fn x -> x <= 500 end)
  |> Enum.reduce(fn x, accum -> if x < accum do accum else x end end)

#result: 496

続いて文字列にチャレンジ

data = ["apple", "banana", "peach", "grape", "orange", "strberry", "pineapple", "raspberry"]
1. 頭文字を大文字にする
2. "a"を除去する
3. "py"を単語の最後に結合する

ここで初めてStirngモジュールが登場し全員で公式ドキュメントを漁る
先頭の文字列を大文字に変換してくれるString.capitalizeたるものを発見しアガる

data
  |> Enum.map(fn str -> String.capitalize(str) end)

#result:
# ["Apple", "Banana", "Peach", "Grape", "Orange", "Strberry", "Pineapple", "Raspberry"]

つづいて"a"を文字列から削除するが、空文字に置き換えてしまえばいいことに気づき再びアガる

data
  |> Enum.map(fn str -> String.capitalize(str) end)
  |> Enum.map(fn str -> String.replace(str, "a", "") end)

#result:
#["Apple", "Bnn", "Pech", "Grpe", "Ornge", "Strberry", "Pinepple", "Rspberry"]

ここで重要なことに気づく。Aが消えてねーじゃん!!!! なんで先に大文字に変換したねん〜
ゴリ押しで解決する

data
  |> Enum.map(fn str -> String.capitalize(str) end)
  |> Enum.map(fn str -> String.replace(str, "a", "") end)
  |> Enum.map(fn str -> String.replace(str, "A", "") end)

#result:
#["pple", "Bnn", "Pech", "Grpe", "Ornge", "Strberry", "Pinepple", "Rspberry"]

最後に文字列の連結演算子 "a "<> "b" = "ab" をつかって"py"を付与する

data
  |> Enum.map(fn str -> String.capitalize(str) end)
  |> Enum.map(fn str -> String.replace(str, "a", "") end)
  |> Enum.map(fn str -> String.replace(str, "A", "") end)
  |> Enum.map(fn str -> str <> "py" end)

#result:
#["pplepy", "Bnnpy", "Pechpy", "Grpepy", "Orngepy", "Strberrypy", "Pinepplepy", "Rspberrypy"]

1mmも意味のない単語が出来上がる...

残念ながら時間切れ

マップを使った問題に取り組み始めた所でレンタル会議室の時間切れ...
結局ここまでしか触れませんでした(反省

マップを触るには先にパターンマッチ知りたいねという話になり
次回はパターンマッチについて勉強しようと思います 今回の勉強会では記事にしてない部分を含めて

  • Enumモジュールの触り方
  • パイプ演算子について
  • 公式ドキュメント最高だよね
  • _(アンダースコア)について
  • ifの書き方

等を共有しました
ワイワイ楽しく出来たと思います
次回はまた2週間後(4月下旬)に開催予定です

よろしければぜひconnpassからご参加ください

【サンプルコード有り】ElixirのOptionParserの入門と使い方

OptionParserとCLI

最近はこってりした記事を書いてばかりなので
久々にあっさりした記事を書こうと思います

CLIって何

ここらへんは自己満程度の部分でメインはOptionParserの使い方のところなので
そんなもん知っとるわという方は飛ばしてください

Command Line Interface
(ターミナルとかiterm2とかpowershellやら)

いわゆる黒い画面です

cd ____
ls -a

とかをやる画面です。最近の悩みはcdと叩いた後に必ずlsも叩いてしまうこと

OptionParserって何(2回目

elixirでコマンドライン解析をよしなにやってくれるモジュールです
コマンドラインに入力された引数を解析して対象のモジュールの関数に渡すことが可能です
escriptたるものを使用してbuildすることでプロンプト(iex)を実行していない状態でも対象の関数を呼び出す際に
OptionParserを使うことで引数をよしなに渡すことができます

いつものiexからの実行

#まずプロンプト(iex)を立ち上げないといけない
$ iex -S mix
iex> Sample.say_hello("hello world")
hello world
:ok

escriptでbuildしてOptionParserで引数を渡して実行

#そのままコマンドラインから実行可能
#第1引数にメッセージ, 第2引数に出力する回数
$ ./project_name "hello world" 2
hello world
hello world
:ok

OptionParserの使い方

個人で色々試していてOptionParserの動き方がよく分からなかったので
自分のメモがてらまとめておきます

基本的な使い方は以下の通り

#第1引数に渡したい引数をリストで渡す
#strictには第1引数に与えた要素がそれぞれ何の型なのかを記述する(下で触れる)
OptionParser.parse([], strict:[])

["--string"]のように「--」を付与することでbooleanとして判定させることができる
["string"]の場合はstringの判定になる模様

戻り値は3つのリストを含むタプルになっている

{[ ], [ ], [ ]}

解析に成功したデータは第1のリストに格納される 解析されなかったデータは第2のリストに格納されて返ってくる
第3リストには不正な型の宣言があった場合にnilvalueにもつmapが返ってくる

非推奨パターン(strictを指定しない)

boolean形式の場合は解析が通るが非推奨だと怒られる
stirng形式の場合には解析が通らずに第2リストに格納されている

#boolean形式で渡す
iex(n)> OptionParser.parse(["--debug"])
warning: not passing the :switches or :strict option to OptionParser is deprecated
{[debug: true], [], []}

#string形式で渡す
iex(n)> OptionParser.parse(["debug"])
warning: not passing the :switches or :strict option to OptionParser is deprecated
{[], ["debug"], []}

ちゃんとstrictを書くパターン

boolean

#boolean形式であることを明示的にstricrtで伝える
OptionParser.parse(["--debug"], strict: [debug: :boolean])

#警告もなく無事に解析を通った
{[debug: true], [], []}

string

#string形式であることを(ry
iex(5)> OptionParser.parse(["debug"], strict: [debug: :string])

#おや...
{[], ["debug"], []}

#boolean形式以外の場合にはリストに2つ以上の要素が必要になる模様(キーが必要っぽい)
iex(6)> OptionParser.parse(["--debug", "B'z"], strict: [debug: :string])
{[debug: "B'z"], [], []}

integer

#integerで(ry
#errorになってしまう
OptionParser.parse(["--debug", 10], strict: [debug: :integer])
** (FunctionClauseError) no function clause matching in Integer.count_digits/2

#文字列のまま渡してあげる必要があるようで勝手にintegerに変換してくれる
iex(7)> OptionParser.parse(["--debug", "10"], strict: [debug: :integer])
{[debug: 10], [], []}

たくさん渡す

#B'zがうまく解析されず第2リストに行かれてしまう
iex(14)> OptionParser.parse(["--debug", "singer", "B'z"], strict: [debug: :boolean, singer: :string])
{[debug: true], ["singer", "B'z"], []}

#これだと通った
iex(15)> OptionParser.parse(["--debug", "--singer", "B'z"], strict: [debug: :boolean, singer: :string])
{[debug: true, singer: "B'z"], [], []}

他のオプションについて

aliases

ざっくりと機能を見てみると、割り当てのためのオプションパラメーターであることが分かる
aliasesって通称みたいな意味があるのでそのまんまですね

#aliasesを使うことで-dが--debugに変換されて解析されている
ex(15)> OptionParser.parse(["-d"], aliases: [d: :debug],  strict: [debug: :boolean])
{[debug: true], [], []}

文字列や数値で試してみたがうまく解析されなかった
個人的には「-h」を「--help」ってやつぐらいしか使い道が思いつかない(booleanのみ)
そもそも文字列やら数値は省略して渡さないのでいいのか...

switches

strictとは違いswitchesは解析した際にマッチしなかった引数の型を設定できるっぽい
strictの場合は解析できなかったものは無視して処理されるため解析できなかった引数は第2リストで返ってくる
stictは厳しめでswitchesは優しめ程度に覚えておけばいい気がする
現状ではあまり使い分けの方法は思いつかない。switchesにすると自由度高すぎるから注意ぐらいかな

OptionParser.parse(["--debug"], switches: [debug: :count])
{[debug: 1], [], []}

elixirで並列処理を使ってファイルを同時に開き特定の文字を検索する

あいかわらず長いタイトル

どういうことかというと

./file/file1.txt
./file/file2.txt
:
./file/file99.txt

file(n).txtには単純に文字が書いてあるだけです
こんな感じですかね

ppppppcatpppppppppppppp
ppppppcatpppppppppppppp
ppppppcatpppppppppppppp
ppppppcatpppppppppppppp

対象のディレクトリ内のファイルを全てを並列処理で各プロセスで開き
特定の文字列を開いたファイルから検索し、何個含まれているかをカウントする
このファイルの場合であれば「cat」を探すとして結果としては「4」が返ってくる
これを並列処理でゴリゴリっとして複数ファイルに対してカウントを行い

[4, 5, 20, 45, 2]

ってことがしたい
この知見があればcsvファイルを並列で読み込んでゴリゴリ加工していくなんてことが気軽にできるはず...
elixirの強さが生きてきますね

プロセスを生成する

正直なところ、他の言語で並列処理をほとんど触ったことがなくて(pythonで少しだけ)
すんませんが並列処理に関する知識・知見はほぼゼロです
プロセスやらスレッドやらを生成してゴリゴリということは何となく分かってます
elixirではerlangでサポートされているプロセスを使ってるようで

  • 軽い
  • 速い
  • 負荷が少ない
  • すべてのCPUで使用可能

とのことで強すぎるわ
また、elixirではプロセスを作るのがアホほど簡単でデフォルトのリミットを外してさえあげれば
低スペックなPCでも10万とか100万って数のプロセスを余裕で生成できます

プロセスの生成の方法は色々ありますが最もシンプルな

spawn(module, function, [arguments])

に触れておきます
spawn使うとプロセスを生成してくれます
はい。これだけです

サンプルとして引数にはこんな感じで渡す

defmodule Sample do
  def greet(msg) do
    IO.puts(msg)
  end
end

#モジュール名と関数名(アトムで渡す)
#関数に渡したい引数は第3引数に配列に内包して渡す
spawn(Sample, :greet, ["ossu"])

#result:
#ossu
#PID<0.42.0>

spawnはPIDという値をリターンします
ずっとProcessIDの略語だと思ってたんですけど
elixirの公式ページによると「Process Identifier(プロセス認識子)」だそうです

sendとreceive

elixirの並列処理ではアクターモデルたるものを採用しています
アクターモデルってのをクソ程ざっくり説明すると

アクターモデル
それぞれのプロセス同士で何も共有しない
お互いにメッセージ送り合って色々やるで~

これはいいですね。
今あのプロセスはこの状態で...あっちのプロセスはこういう状態で...
ってなことを一切考える必要がありません
お互いのプロセスでメッセージを送受信するだけでいいので

しかもプロセス同士でメッセージ送受信するのもクソ簡単です
メッセージの送信

pid = spawn(Sample, :greet, ["オラオラオラオラ"])
send pid, {self(), "send message!"}

spawn関数で生成したPIDを使用して
{送信元, メッセージ}を用意してあげればok
またself()もしくはself(elixir1.8ではwarning)を使うことで現在のプロセスのPIDを返してくれます
AプロセスからBプロセスへメッセージを送信ってことをしているわけです

メッセージの受信

defmodule Sample do
  def greet do
    receive do
      {sender, message} -> IO.puts(message)
    end
  end
end

spawnの中で指定したモジュールの関数内にreceiveを使う
さらにreceiveブロックの中でパターンマッチを行いメッセージを受信する

送信元にメッセージを送り返す

defmodule Sample do
  def greet do
    receive do
      {sender, message} ->
        send sender, {:catch, "catch: #{message}"}
    end
  end
end
 
pid = spawn(Sample, :greet, [])
send pid, {self(), "send message!"}

receive do
  {:catch, message} -> IO.puts(message)
end

モジュール内の関数でrecieveした後にsendを再び使って送信元に再びメッセージを送り返します
さらにこのモジュール内の関数からsendで送信したメッセージを送信元で受信することができます

特定の文字をカウントする関数

ここはメインの部分ではないのでざっくりと
再帰関数を使ってファイル内の文字列を置き換える毎にaccumlatorに+1して
置き換えられなくなった時点(false)で終了し、最終的なaccumlatorをリターンします

defmodule FinderStrInFile do
  def total_count(target_str, file_path) do
    File.read!(file_path)
      |> _total_count(target_str, 0)
  end

  defp _total_count(file_str, target_str, accum) do
    key = String.contains?(file_str, target_str)
    case key do
      true ->
        String.replace(file_str, target_str, "", global: false)
          |> _total_count(target_str, accum+1)
      false -> accum
    end
  end
end

FinderStrInFileモジュールでのメッセージの送受信

プログラミングelixirの第14章をリスペクトしながら送受信の部分を作成していきます
送信されたメッセージをやりとりするためにモジュール内にfind関数を用意します

def find(scheduler) do
    #いつでもいけます
    send scheduler, { :ready, self() }
    receive do
      { :find, file_path, target_str, client } ->
        IO.puts("--> #{file_path}")
        send client, { :answer, total_count(target_str, file_path), self() }
        #再帰呼び出し
        find(scheduler)
      { :shutdown } -> exit(:normal)
    end
  end

こん関数はcallされた時に送信元に{ :ready, self() }(アトム, 現在のPID)を送信します
こちら側はいつでもファイル探せる準備できてますよ〜ってことを知らせるためにメッセージを送るわけです

:findについて

{ :find, file_path, target_str, client } ->
  IO.puts("--> #{file_path}")
  send client, { :answer, total_count(target_str, file_path), self() }
  find(scheduler)

もちろんメッセージの受信部分が必要になるのでreceive節も用意しています
送信元から{ :find, ... }のデータを送ることでデータの内の文字のカウントを開始します
文字のカウントが終了した際には送信元に{ :answer, #結果, pid }を返し
処理が終了したことと、集計の結果を送信します
また、上記の処理が次回以降も行われるようにパターンマッチの中でfind自信をcallして再帰させています

:shutdownについて

{ :shutdown } -> exit(:normal)

もう処理する必要がなくなった際に処理を停止させるために{ :shutdown }を用意してあります
:findのパターンマッチではfind自信をcallして再帰していましたが
:shudownの方ではもう処理を継続させる必要がないのでプロセスの終了を行います

#引数でプロセスの終了の仕方のモードを変更可能
exit(:normal)

ようやく処理を行うメインのモジュールの全体が完成しました

defmodule FinderStrInFile do
  def find(scheduler) do
    #処理開始の準備ができたことを送信元に返信
    send scheduler, { :ready, self() }
    receive do
       
      { :find, file_path, target_str, client } ->
        IO.puts("--> #{file_path}")
        send client, { :answer, total_count(target_str, file_path), self() }
        find(scheduler)
      { :shutdown } -> exit(:normal)
    end
  end

  def total_count(target_str, file_path) do
    File.read!(file_path)
      |> _total_count(target_str, 0)
  end

  defp _total_count(file_str, target_str, accum) do
    key = String.contains?(file_str, target_str)
    case key do
      true ->
        String.replace(file_str, target_str, "", global: false)
          |> _total_count(target_str, accum+1)
      false -> accum
    end
  end
end

Schedulerの作成

この部分はプログラミングelixirの14章をほぼオマージュしています
プロセスの生成と対象モジュールにデータを送受信の管理部分を担当させます

Schedulerには2つの関数を用意します

  • run関数: プロセスを生成する
  • schedule_processes関数: メッセージの送受信の管理を行う

run関数はシンプルで受け取った数値の合計分、プロセスをEnum.mapを使って生成するだけです
spanwの戻り値であるPIDをschedule_processesの第1引数に渡して並列処理を開始させます
ついでに受け取った引数も渡します

def run(num_processes, module, func, file_list, target_str) do
    #受け取った数値分のレンジを生成
    (1..num_processes)
      #Enum.mapを使って数値分のプロセスを生成
      |> Enum.map(fn(_) -> spawn(module, func, [self()]) end)
      |> schedule_processes(file_list, target_str, [])
  end

schedule_processes関数は少し複雑ですが、別に難しいことはなんもやってません
あるのはreceive節のみでホンマにメッセージの送受信管理をしてるだけです
schedule_processesで管理しているメッセージは

  • :ready
  • :find
  • :shutdown
  • :answer

の4つです。すでにfind関数で触れているものなので各説明はしませんが
find関数から送信された:readyの合図を受信して処理を開始します
第2引数にファイルのパスがリストに格納されており、この第2引数に対して
ガード節を用いてパターンマッチさせています
リストから要素を取り出せる限り、schedule_processesを再帰させて:findを送信し続けます
空になった時点で:shutdownを送信して処理を終了するようにします

defmodule Scheduler do
  def schedule_processes(process, file_list, target_str, accum) do
    receive do
      #準備完了のメッセージを受信。リストが空でなければマッチ
      { :ready, pid } when file_list != [] ->
        #ヘッドとテイルを使って先頭の要素を取得
        [ head | tail ] = file_list
        #対象のモジュールに:findを送信
        send pid, { :find, head, target_str, self() }
        #再帰を行う
        schedule_processes(process, tail, target_str, accum)

      #準備完了のメッセージを受信。リストが空ならマッチ
      { :ready, pid } ->
        #対象のモジュールに終了を伝えるために:shutdownを送信
        send pid, { :shutdown }
          #ここが正直よくわかってない
          if length(process) > 1 do
            schedule_processes(List.delete(process, pid), file_list, target_str, accum) 
          else
            IO.inspect(accum)
          end
      #:answerを受け取りaccumlatorに結果を追加する
      { :answer, total_count, _pid } ->
        schedule_processes(process, file_list, target_str, [total_count | accum])
    end
  end
end

プログラミングelixir第14章を参考に書きましたが、この部分がまだよく分かってない...
Enum.mapとspawnで生成したPIDのリストを下記のようにしていくのは分かる
なぜなのか。並行して調べてるので何か分かり次第、追記します

[#PID<0.374.0>, #PID<0.375.0>, #PID<0.376.0>, #PID<0.377.0>]
[#PID<0.375.0>, #PID<0.376.0>, #PID<0.377.0>]
[#PID<0.375.0>, #PID<0.376.0>]
[#PID<0.375.0>]
#何の意味があるのかよく分からない。バグの対処とあるが....
if length(process) > 1 do
  schedule_processes(List.delete(process, pid), file_list, target_str, accum)
else
  IO.inspect(accum)
end

最後の最後に実行部分の作成

ファイルの数だけプロセスを生成してFinderStrInFileのfind関数を呼び出すようにします

#ファイルを読み込みパスを生成
files = File.ls!("./data")
  |> Enum.map(&("./data/" <> &1)) 

#ファイルの数分プロセスを生成するためにカウント
make_process_num = Enum.count(files)

#ファイルパスとファイル数。対象のモジュールと関数と引数を一気に渡す
result = Scheduler.run(make_process_num, FinderStrInFile, :find, files, "cat")

実行結果

IO.inspect(result)

#result:
#並列にIO.putsが実行されている!!
# --> ./data/str3.txt
# --> ./data/str2.txt
# --> ./data/str1.txt
# --> ./data/str5.txt
# --> ./data/str4.txt
# [20, 2, 5, 10, 20]
# [20, 2, 5, 10, 20]
# [Scheduler, FinderStrInFile]

無事にファイルの文字数がカウントされている!!!!!
# result = [20, 2, 5, 10, 20]

まとめ

とりあえず、やりたいことはできたと思う
TaskとAgentたるものを使用することでもっと楽にできるらしいです
アホほど長たらしくなってしまい、すんません
並列処理はelixirの核となる部分なのでもっと知見貯めたい
間違いなどあればコメントで指摘頂ければありがたいです