やわらかテック

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

【レポート】第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の核となる部分なのでもっと知見貯めたい
間違いなどあればコメントで指摘頂ければありがたいです

【自分的レシピ】elixirでの再帰関数の動かし方

再帰関数っていいよね

たまにelixirでも「for使いてぇ~」って邪悪な思想に染まる時がありますが
再帰関数やEnumなどを使って目標を達成できると最高の気分になりますね
再帰関数に至っては関数型言語に共通することです

自分の簡潔な言葉で「再帰関数って何なん?」という問いに答えると

関数の中で自分自身(関数)を呼ぶ関数でっせ

となります。どういうことやねん
javascriptpython使って5回、引数でもらったメッセージを出力する関数を再帰関数使って書いてみました
簡略化のため、今回は5回固定にしておきます

javascriptの場合

const greeding = (message, num) => {
  if(num===5){
    #条件に達した時に終了
    console.log("--> all finished!");
    return true;
  } else {
    console.log(message);
    #ここで自分自身(関数)を呼び出している
    greeding(message, num+1);
  }
}
greeding("hello world!", 0);

#result
# hello world!
# hello world!
# hello world!
# hello world!
# hello world!
# --> all finished!

pythonの場合

def greeding(message, num):
  if num == 5:
    #条件に達した時に終了
    print("--> all finished!")
    return True
  else:
    print(message)
    #ここで自分自身を呼び出す
    greeding(message, num+1)
    
greeding("hello world!", 0)

#result: 同じなのでカット
#for使おうぜ
for i in range(5):
  print("hello world!")
print("--> all finished!")

停止性について低姿勢で考察する

なんとなーく分かってもらえれば光栄です
僕もなんとなーくでやってるので
従来のfor文と異なるのは「ここで停止します」ということを明確に宣言してあげる必要があります
難しい本には「停止性についての議論」と記述されています

例えばpythonとかだったらif文使ってこの条件の時にreturn書いて
関数内の処理が終了するということを明確にしてあげればいいってことです
while書く時に終了条件書きますよね。あれと同じようなもんです

この停止性についてfor文だとどうなっているかというと、iteratorたるものがあって
列挙可能なデータを1つずつ取り出してきてくれます
それで、これ以上取り出せませんってなったときにNoneやらを返して処理を終了させるというものです

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

#take: 1,  rest: [2,3,4,5]
#take: 2, rest: [3,4,5]
#take: 3, rest: [4,5]
#take: 4, rest: [5]
#take: 5, rest: []
#もう残ってませんのでNone返しますわ

もっと詳しく知りたい方は「言語名 イテレーター」とかで調べてみてください
有志の猛者たちがアホほど分かりやすく解説してます

elixirにはforはないから再帰関数を使うしかない

これまでの流れを踏まえて考えれば、for文のないelixirでもループ処理は書けそうな気がしてきた(適当
関数内で自分自身(関数)を呼び出して、終了条件を書いてあげればいいんでしょ?

その通り。察しがいいですね
その前にelixirというか関数型言語での便利な構文について少し触れておきます
ゆるく理解するElixirのデータ構造体と簡単なパターンマッチング例でも若干触れてますが
配列の先頭要素を取り出す際に

lst_data = [1,2,3]
[head | tail] = lst_data

IO.puts(head) #result: 1
IO.puts(inspect tail) #result: [2,3]

こんな書き方をしています
構文に関しての名前は特になくて著書によれば「ヘッドとテイル」とあるので、そう呼びます
ヘッドとテイルと言ってるんですけど変数名は実は何でもOK

[a  | b] = lst_data

配列の要素が残り1つになった場合には

[head | tail] = [1]

#head: 1
#tail: []

tailには が与えられています
この空の配列に対してヘッドとテイルを使うとerrorになります

[head | tail] = []
#(MatchError) no match of right hand side value: []

一度まとめておくと

  • 配列に対してヘッドとテイルを使用することで先頭要素と残りの要素(配列)に分割することが可能
  • 配列の残り要素が1つの場合にはheadに要素が、tailには空の配列が与えられる
  • 空の配列に対してヘッドとテイルを使用するとerrorになる

で、何かって?このヘッドとテイルが何かiteratorに似てるなーって思いませんか?
Noneやらが空の配列になっただけじゃんって考えるとそっくりじゃないですか!!

準備は整った

ここまで話を膨らませるともう何となく書けちゃいそうですね
データを列挙して、終了する条件を記述してあげればOKってことなので
ヘッドとテイルの場合には空の配列になった時に終了させてやればよさそうですね
配列に受け取った文字列を全て出力するという処理を書いてみましょう
elixir

defmodule Output do
  def show_msg([]), do: :ok
  def show_msg([head | tail]) do
    IO.puts(head)
    show_msg(tail)
  end
end

msg_lst = [
  "hello okb",
  "goodbye okb",
  "I love you"
]

Output.show_msg(msg_lst)
#result
# hello okb
# goodbye okb
# I love you

詳しいことは別の記事に書こうと思いますが
elixirでは関数の引数に対してマッチングをすることが出来ます

def show_msg([]), do: :ok

という関数では第1引数が空の配列の場合に実行されます
それ以外の場合(配列に要素が存在している)には

def show_msg([head | tail]) do ... end

こちらの関数がヒットします
elixirでは同名の関数何個あってもOKで、引数を使って条件分岐が可能です
その上、引数内でヘッドとテイルやパターンマッチが使えてしまうという...超強力です

つまりはヘッドとテイルを使って配列の要素を取り出しつつ
restが空の配列になった時点で終了になるという動き方をします

#take: 1,  rest: [2,3,4,5] -> show_msg([head | tail]) --> continue
#take: 2, rest: [3,4,5] -> show_msg([head | tail]) --> continue
#take: 3, rest: [4,5] -> show_msg([head | tail]) --> continue
#take: 4, rest: [5] -> show_msg([head | tail]) --> continue
#take: 5, rest: [] -> show_msg([]) --> finished!!

配列じゃない場合どうするの?

再帰関数を使うにあたって「停止性」を保証してあげればいいので
基本的に考え方は同じです
ifやらでマッチさせる対象が配列から数値や文字列に変わるだけです

指定回数「hello!」を出力する

defmodule Output do
  #numが0の時にヒットする
  def show_hello(num) when num === 0, do: :ok
  def show_hello(num) do
    IO.puts("hello !")
    #次の再帰に渡す引数を-1(いずれ0になる)
    show_hello(num-1)
  end
end

Output.show_hello(10)

#result
# hello !
# hello !
#:
#:
# hello ! 10回出力されて終了

文字列を先頭から1文字ずつ取得して表示していく
かなり強引ですが文字列でも余裕でいけます

defmodule Output do
  def first_str(str) when str === "", do: :ok
  def first_str(str) do
    first = String.slice(str, 0..0)
    rest = String.slice(str, 1..String.length(str)-1)
    IO.puts("--> first: #{first}")
    first_str(rest)
  end
end


Output.first_str("abcdefg")
--> first: a
# --> first: b
# --> first: c
# --> first: d
# --> first: e
# --> first: f
# --> first: g

まとめ

再帰関数を記述際に考えるべき重要な点は
この処理は停止するかどうか ということです
停止性を実現するためにはifなどを用いて条件によって終了させてあげれば良いです
加えて再帰関数では条件を作成した後にその条件を満たす場合が発生するように
引数の値が変化するようにしてあげる必要があります

  • 配列: 要素が1つ取得される(ヘッドとテイル)
  • 数値: +1される -1される など
  • 文字列: 先頭の1文字が取得される 1文字増える etc...

全ての引数の値が全て同じであると再帰が無限になってしまうので気をつけましょう

まとめのまとめ

  • 停止性を保証する(条件を作成する)
  • 次の再帰に全ての引数で同じ値を渡さない

この記事の続編として再帰関数には不可欠なアキュムレーターについてまた触れます
長くなってすんません。やっぱり再帰関数っていいよね

現役エンジニアが考察するエンジニアに転職したいならProgateは控えるべきって話

著者のバックグラウンドについて

現在(2019年4月)、僕はエンジニアとして名古屋の企業で働いています
一応、理工学部の出身ではありますが環境工学(土木/建築)を専攻してました(ゆうて真面目に勉強してない)
つまりはプログラミングに関しては大学3年生までド素人だったわけで...
ただパソコンは子供の頃から好きでずっと触ってましたが
office使ったり、ニコ動見るぐらいです。MADクソみてました

こんな状況だったわけですが、一応エンジニアとして就職することができました
まぁ転職ではないですが一新したキャリアで就職しましたので、ある意味転職したようなもんですね(適当

ざっくりとまとめると

  • キャリアチェンジした経験がある
  • 現役のエンジニア(2年目

という立場から今回のテーマに触れていきます

言うまでもないけどProgateって何?

オンラインでプログラミング言語フレームワークを学習することの出来る
プラットフォームな学習サイトです
Progateの特徴は以下の通り

  • 環境構築(プログラムを始めるまでの準備)が一切不要
  • 登録後にブラウザのみで学習が可能
  • スライド形式のコース授業
  • めっちゃ安い(月980円は破格)

何よりもすぐに始められるのがかなりウケてる印象です
僕も元Progaterで当時は結構なレベルまでやりこみました
htmlとcssjQueryに関しては2週か3週やった遠い記憶があります

約2年前の僕のアカウント
Progateの成績

僕はProgateに対しての不満は1mmもなくて、むしろお世話になりましたし
出来栄えや学習のしやすさに関しては文句なしで最高の出来だと思います

「じゃあ、何が不満でこんな記事書いてんの?」って怒られそうですが、ちゃんと不満はあります

エンジニアに転職したいって面接くる人

僕は人のことを偉そうに言えないんですけど、すっごい多いんですよ。こんな人達

  • 元営業でエンジニアに転職したくて勉強中
  • エンジニアになってお金稼ぎたい
  • 大学生で文系だけどプログラミング始めました

うん。まぁ、その気持ちには何の問題もないし、むしろ良いことですね
新しいことを始めようとしたりチャンレンジしたり素晴らしいことですよ

じゃあ何が問題かって?

こういう人達の多くが
Progateしかやってない&やった気になってる
ってことです

「Progateでhtms/cssを勉強してます!」
pythonをProgateでやりました」
railsをやりました。Progateでやりました」

甘い。甘すぎる

やった気になってるのが最悪な状態

Progateやってますって誇ってるのって、エンジニア界隈をカレー作りに例えていえば
皿にカレーをよそってます。カレー作りは任せてください
という状態とほぼ同等です

一番最後の工程だけを触って、満足感を得ているのと何ら変わりません
カレーを作って食べるためには

  • どんなカレーを作るかを決める
  • 手順を考える
  • 材料を準備する
  • 具材を切ったり、煮込んだりする
  • 皿によそって食す

この過程があってこそ。てゆうかこの過程を経験することが最も重要
プログラミングも全く同じです

  • プログラミングで達成したい目標を定める
  • 手順(フロー)を考える
  • 環境を構築する
  • コードを書いていく
  • リリースする

Progate上がりの人ってこの過程をプログラミングの視点で考えることが出来ない人が多いです
僕も経験したことですが、Progateのrailsコースが終わってから
自分の環境でrails使ってオリジナルのブログを作ろうとした時に
自分の手が一切動かないことに気づきました

そして、気づけばProgateのスライドを漁っているわけです
それじゃあ、いつまでやっても強くなれない...

結局はある程度自分で1から作るべき

先ほどのブログを1から自分で作ろうとするとまぁしんどいですよ

ほとんど知識ない状況で完成品を考えて...
rubyをインストールして、railsをインストールして...
controllerが...、viewが....って考えて...
訳の分からないエラーが出て、ググって対応して...

これだけの苦労を乗り越えて初めて、実務で役立つ能力が身に付きます

  • 設計力
  • デバック力
  • 言語に対する知識 etc...

もしくはオリジナルな改造をしてみるなど、自分の考えが反映されていない作業は意味ないです

このレベルに達してないProgateだけをやってる人は面接では話になりません
Progateを何週もやっても、あんまり意味ないので自分の作品を作った方が何十倍も良いです

Progateは悪か。正しい使い方は?

Progateは教材としては素晴らしく、よく出来ています
プログラミングという従来、壁があったものを気軽に始めることができます
だからやりたくなるし、やった気になる

ただ深入りは要注意です。Progateは基礎の部分を学ぶだけにした方が良いです
ここで全てが学べるという考えは止めましょう
全てを学びたいなら公式のドキュメントを読めって話

まとめ

Progateは素晴らしい。ただ、深入り要注意で

  • エンジニアに転職したい
  • スキルを磨きたい

のであれば、自分の考えが反映されるコードを書きましょう

以上