はじめに
注:自分が初学者なのであって、初学者向けのドキュメントとして書いたのではないです。タイトルも紛らわしかったのでちょっぴり変更(ひらがな1文字)しました。
Elixir 本を読んだり集まりなどに行ったりしたので、あとは書いてみるしかないかなと。何を題材にすればよいのか思いつかなかったが、ちょうど Ruby で書いたパスワード生成プログラムがあったので、真似て書いてみることにした。
なお、Ruby も通りすがりの勉強しかしてないので模範的ではないことに注意。
$HOME/sh/looppw.rb 2>/dev/null | $HOME/sh/selpw.rb
パイプの前の looppw.rb がランダムな文字列を組み合わせてパスワードの候補をじゃんじゃん生成し、パイプの後ろの selpw.rb が望む条件をチェックしてOKなのが来たらそれを出力して終わるという bsh スクリプトである。
#! /usr/bin/ruby
strl = "abcdefghkmnprstuvwxyz"
strs = "\#$%;,.?/"
str = strl + strl.upcase + strs
basechars = [*2..8, *str.split("")]
loop {
puts [strl.split("").sample(1), basechars.sample(7)].join
}
コマンド間で文字列定数を引き渡したりしていないので、今見るとちょっと汚い。
#! /usr/bin/ruby
Strn = "2345678"
Strl = "abcdefghkmnprstuvwxyz"
Stru = Strl.upcase
Strs ="\#$%;,.?/"
def suffice(str)
return Strl.include?(str[0]) \
&& (Strn.split("") & str.split("")).length >= 2 \
&& (Strl.split("") & str.split("")).length >= 3 \
&& (Stru.split("") & str.split("")).length >= 2 \
&& (Strs.split("") & str.split("")).length == 0
end
until suffice(passwd = gets) do
# do nothing in the loop
end
print passwd
目標
以下の関数を作る。既存のコマンドとはちょっと仕様が違う。
-
入力:ほしいパスワードの文字数
-
出力:パスワードの文字列
-
英大文字、英小文字、数字、特殊文字、をそれぞれ1文字以上は含むこと
-
紛らわしい文字を使わないこと
- 0 と o と O
- 1 と i と I と j と J と l と L
- 9 と q と Q (口頭で伝えるときに発音が紛らわしい)
-
使う機能
できるだけ多くの機能を使ってみる。
- 遅延評価を使った表現
- 無限リスト、パイプ、フィルタ
- 複合型
- タプル、リスト、マップ
- 高階表現
- 無名関数を引数として渡す
- 再帰呼出し
- パターンマッチ、ガード
defmodule MakePassword do
def mkpwd(n) do
str_map = _base_str()
str = str_map.strl <> str_map.stru <> str_map.strs <> str_map.strn
_random_stream(str)
|> _filter(0, n, str_map)
|> elem(0)
|> to_string
end
def _filter(stream, times, str_n, str_map) do
str_list = Enum.take(stream, str_n)
mapl = String.split(str_map.strl, "", trim: true)
mapu = String.split(str_map.stru, "", trim: true)
maps = String.split(str_map.strs, "", trim: true)
mapn = String.split(str_map.strn, "", trim: true)
nl = Enum.count(str_list, fn t -> t in mapl end)
nu = Enum.count(str_list, fn t -> t in mapu end)
ns = Enum.count(str_list, fn t -> t in maps end)
nn = Enum.count(str_list, fn t -> t in mapn end)
if nl > 0 && nu > 0 && ns > 0 && nn > 0 do
{str_list, times}
else
_filter(stream, times+1, str_n, str_map)
end
end
def _random_stream(str) do
Stream.unfold(nil, fn(s) -> {_random_char(str), s} end)
end
def _random_char(str) do
Enum.random(String.split(str, "", trim: true))
end
def _base_str() do
str_lower = "abcdefghkmnprstuvwxyz" # do not use "ijloq"
str_upper = String.upcase(str_lower)
#str_special = "\#$%;,.?/"
str_special = "-/"
str_number = "2345678" # do not use "019"
%{ :strl => str_lower, :stru => str_upper, :strs => str_special, :strn => str_number }
end
end
実行例
$ iex
Erlang/OTP 21 [erts-10.0.5] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]
Interactive Elixir (1.7.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> c "mkpwd.ex"
[MakePassword]
iex(2)> MakePassword.mkpwd(5)
"UY/7z"
iex(3)> MakePassword.mkpwd(9)
"su7-K3EKG"
iex(4)> MakePassword.mkpwd(16)
"8hmZzuCKdgr-HCKR"
考察
一応、目標は達成した。しかしながら、つくり上げるのに精一杯で、反省点多し。
- 無理やり機能をてんこ盛りに使ったのでそれなりに不自然
- 関数で必要な値は全部引数にして引き回したけどちょっとくどい感じ
- タプルとリストとマップが頭の中でゴチャゴチャになる
- 作るのにいっぱいいっぱいでろくにコメントが入れられなかった
- 変数名に工夫がない
- 文字列をいろいろな型に変換して処理しているので見通しが悪い
- 再帰呼び出しを無理やり使ってるけど、コンパイラが tail recursion してくれるのを意識しながら書くという本末転倒な使い方をしてる
- パターンマッチとガードはうまく使える場所を見つけられなかった
Qiita でドキュメントを残す
Elixir ついでに Qiita に投稿というのもはじめてやってみる。それに依存してマークダウンも使ってみた。
参考文献
[プログラミングElixir] (https://www.amazon.co.jp/dp/4274219151/)
[Elixir v1.7.3] (https://hexdocs.pm/elixir/)