Chienomi

パスワード生成器 Part2 (初級プログラミング)

プログラミング::beginners

注意 (共通)

「初級プログラミング」の記事は古いものから順に読むことを想定しており、古い記事で説明したことを再度説明していない。

わからない内容がある場合は、Chienomi内検索を使用するか、初級プログラミングの古い記事から順に読むことを推奨する。

せっかく書いてしまったので、PerlバージョンとLuaバージョンの解説もしていこうと思う。

先の記事と比べると難易度は高く、先の記事のRubyのコードをしっかり理解していることが前提となる。

See into a code.

Shebang ふたたび

#!/usr/bin/lua

さて、Shebangについておさらいしよう

  • Shebangを解釈するのは (シェル || システム)
  • 適用されるのは「実行可能なテキストファイルを実行したとき」
  • Shebang行に実行したファイルを引数として追加した形で呼び出される
  • #がコメントな処理系は実行時Shebangはコメントとして読み飛ばされる。LispやLuaはShebangを特別扱いして読み飛ばす

さて、次のようなShebangを見たことのある人も多いのではないだろうか。

#!/usr/bin/env perl

実はこれ、相当にバカバカしい。

envは環境変数をセットして引数のコマンドを実行するというプログラムである。 shの場合は

EDITOR=emacs vipw

みたいなことができるのだが、cshではこれができないので、「そのコマンドのために環境変数をセットしたい」という場合に

env EDITOR=emacs vipw

みたいにする必要があった。このためにあるプログラムである。

env(1)を呼び出しているのは$PATH上のプログラムを絶対パスではなく呼び出すためなのだろうが、それができるということはShebangを解釈する時点で$PATHが引き継がれているということである。 少なくともLinuxにおいてはShebangは$PATHを考慮されるので

#!/usr/bin/env ruby

とか書くくらいだったら、

#!ruby

で良い。ちゃんとこれで動作する。 envに対して呼び出すとenvが無駄なexecをすることになるためオーバーヘッドが増える。 頻繁に呼び出されるCGIアプリケーションなどでは結構な負担になる。

Windowsの場合はシステムがShebangを考慮することはない。 Windowsに対して実行可能なテキストファイルを実行した場合、startというプログラムの引数に渡される。つまり、

>foo.rb

とした場合、

>start foo.rb

として解釈される。startはWindowsにおける「開く」という動作において呼び出されるプログラムであり、ファイルの拡張子に対する関連付けがなされたプログラムに引数を引き渡す。

変数定義

変数の定義の必要性について、基本的には

  • 定義しなければ問答無用でその場で未定義値の変数が用意される
  • 宣言されていない変数はエラーになる
  • 変数名として名前が登場していなければエラーになる

という振る舞いが存在する。

最初の2つは割と一般的である。 PerlとLuaは名前が過去に宣言されているかどうかということは気にする必要がなく

print(foo)

みたいなのを脈絡もなく書くことができる。

Rubyの場合は3つ目の振る舞いになる。つまり、

puts foo

はエラーになる。

Traceback (most recent call last):
test.rb:1:in `<main>': undefined local variable or method `foo' for main:Object (NameError)

が、

if false
  foo = "foo" 
end
puts foo

みたいな「実際に定義されることはないけど、事前に変数であることを察知するチャンスがある」場合はOKである。

変数にはその変数が有効な「スコープ」がある。 これは、その変数を参照できる範囲である。

Rubyの場合は変数は自動的にレキシカルスコープを持ち、同一ブロック内で有効である。 ネストされたブロック内でもその変数は有効であり、外側のブロックで変数が宣言された状態でネストされたブロックでその変数の値を変更すると、外側のブロックの変数が書き換わる。

# a == nil
block1 do
  a = 100
  # a == 100
  block2 do
    a = 50
    # a == 50
  end
  # a == 50
end
# a == nil

Rubyでは$で始まる名前の変数はグローバル変数になる。

Perlの場合は宣言しない変数はグローバルで、プログラム中どこでも同じ名前の変数なら同じ変数を指す。 宣言にはmy, local, ourの3つがあるなかなかややこしい仕様。 myがレキシカルスコープを持つ変数で一般的。myで宣言された変数のスコープ内でその変数を変更すれば、その変数が変更されるが、ネストしたブロック内でさらにmyで同名の変数を宣言すると、外側にある同名の変数は隠蔽される。

# a == undef
{
  my a = 100
  # a eq 100
  {
    my a = 50
    # a eq 50
  }
  # a eq 100
}
# a == undef

Rubyの場合変数の明示的宣言という書き方がないので、このようにネストしたレキシカルスコープを持つ変数は作成できない。

Luaの場合は何も宣言しなければグローバル、localをつけて宣言するとレキシカルスコープを持つ変数になる。 サンプルコードではLuaだけグローバル変数にしているが、これはLuaでは比較的小さなプログラムの場合わざわざ宣言しないほうが一般的なようだから。

この手の動作として珍しいのはJavaScriptである。 JavaScriptも明に宣言した場合はレキシカルスコープを持ち、そうでなければグローバルスコープを持つのだが、JavaScriptにおけるvar変数のスコープは「宣言された関数全体」なのである。 そして、その関数内で宣言されているのであれば、宣言する前であってもその関数では有効である。

しかしそれが気に食わないという人もいて、最近はletという変数宣言も追加された。 こちらはスコープがブロックで、ネストしたレキシカルスコープを持つ変数を作れるようになっている。 登場が2015年と新しく、ブラウザで動作するJavaScriptでは処理系を選べないので、今すぐ使うのは結構危険だと私は思っている。

Shift

pwlength = ARGV.shift&.to_i || 64
pwlength = 64 if pwlength < 1

このRubyのコードと

my $plen = int(shift);
$plen = 64 if $plen lt 1;

このPerlのコードはほとんど同じ。これは、

pwlength = ARGV.shift.to_i
pwlength = 64 if pwlength < 1

というRubyコードと等しい。

Luaの場合はそもそもLuaに配列がないため、shiftがないという珍しい言語である。 そのため、shift相当の処理をするのが面倒で、「残る引数を使って…」みたいな処理がないということを踏まえて

pwlength = tonumber(arg[1])
converter_symbols = arg[2]

Shiftせずに両方の引数を一気に用意するという方法をとった。

if not(pwlength) or pwlength < 1
then
  pwlength = 64 
end

nilの場合と、0以下の場合のデフォルト処理である。

ちなみに、LuaはRuby同様に珍しく、未定義値がnil

文字列を文字の配列にする

Rubyの場合は、each_charで一文字ずつ繰り返すEnumratorを用意でき、to_aで配列化できるため一気だったが、Perlの場合はそれがないので

my @csym = split(//, shift);

である。副作用のある式(ここではshift)を値として取らずにいきなり関数の引数にするのはPerlっぽいコード。Rubyも比較的割と書き方をする。

文字列を何もないところで分割すると1文字ずつ分かれる、というトリッキーさもPerlっぽい。

for c in converter_symbols:gmatch(".") do
    table.insert(converter, c)
end

Rubyで書くと

converter_symbols.scan(/./) do |c|
  converter.push(c)  
end

になる。要は、「パターンにマッチした文字に対してイテレータを回す」というもので、イテレータの中で配列(Lua的にはテーブル)に要素を追加していっているわけである。

ちなみに、全然書き方が統一できていないが、

converter_symbols:gmatch(".")

というのは、

string.gmatch(converter_symbols, ".")

と等しい。だったら、

converter:insert(c)

とかけそうなものなのだけど、これは効かない。なぜだかはわからない。

候補文字の配列を作る

Rubyの便利さが光る。 やっていることはあまり変わらないのだが、デフォルトのほうを見てみよう。

Rubyでは

('!' .. '~').to_a

である。 範囲で文字を指定すると、この場合はコードポイント(1バイトの数をそのまま文字表現として解釈したもの)の33である!から126である~までの文字からなる範囲を表現でき、to_aでそれを配列に変換できる。

さすがにこれはだいぶ特殊で、Perlの

@converter = map { chr  } (33 .. 126);

というコードのほうがだいぶまとも。 Perlのmapはかなり見づらいが、

map block list;

であり、Rubyでは

converter = (33 .. 126).map {|i| i.chr }

になる。 Perlの範囲は「範囲」ではなく、その範囲で示されたものが即座に展開されてリストになる(条件式として使われる場合を除く)。 で、map$_に各要素を代入するので、chr($_)で整数値である$_を文字にしたものになる。

mapはイテレータを回して返り値からなるリスト(配列)にするものなので、これで候補文字のリストが出来上がる。

Luaはこんな便利なものはないので

for asc=33,126 do
  table.insert(converter, string.char(asc))
end

という感じで、33から126までイテレータを回して、ひとつずつ文字に変換してはテーブル(配列)に追加していく、という手順である。これはRubyでは

(33 .. 126).each do |asc|
  converter.push asc.chr
end

というコードになる。 この手のコードとしては書きやすい部類に入り、Perlでは3パートforループを使用して

for(my $asc=33; $asc <=126; $asc++) {
  push(chr($asc), @converter);
}

というコードになる。 もっとも、リストに展開してしまうのがアリならPerlでも

foreach my $asc (33 .. 126) {
    push(chr($asc), @converter);
}

というイテレータは書ける。

Perlは

if (0 < @csym) {
  @converter = (("a" .. "z"), ("A" .. "Z"), (0 .. 9), @csym);
}

というコードがなかなか強烈。

配列の長さの求め方がRubyは

array.length

とまぁすごく普通で、Luaは

#array

とまぁ納得できる感じだが、Perlは 「スカラーコンテキストでリストを評価すると長さが返る」という仕様である。だから実は

if(0 < @csym)

というと、0と比較できるのはリストではなくスカラーなのは明らかなので、@csymはこのリストの「長さ」になる。

ちなみに、$#csymと書くと「リスト@csymの最後の要素のインデックス」になる。この場合、返ってくる数は1つ小さい。

そして、Perlはリストの要素はスカラー値と決まっていて、リストの値としてリストが入らない。リストの中にリストを入れるとどうなるかというと、リストが展開される。つまり、勝手に平坦化される。 だから、

@converter = (("a" .. "z"), ("A" .. "Z"), (0 .. 9), @csym);

というコードは、範囲もリストに展開されるということを踏まえると「リストの中に4つのリストを書いている」のだが、平坦化されるのでこのリストの要素を全部並べたリストが出来上がる。

Rubyでは

converter = [("a" .. "z").to_a, ("A" .. "Z").to_a, ("0" .. "9").to_a, csym].flatten

に相当するコードで、なかなかのきわどさである。

ループの構造

Rubyの場合は、「パスワードの長さ分繰り返す。無効な値の場合はそのループをやり直す(redo)」という方式である。

pwlength.times do |i|
  redo if #...
  #...
end

Perlの場合もほとんど同じ。繰り返し回数のために3パートforループを使っている。 Perlでは$outlenという名前にしたが、別に$iにしても良い。

for (my $outlen = 0;$outlen < $plen; $outlen++) {
    redo if #...
    # ...
}

ところがLuaの場合はnextredoも、ループ制御はなにもない。ループを抜けるbreakだけだ。

そこで、予めカウンタ変数を用意しておき、そのインクリメント処理をifの中に入れるという方法を取った。 ついでに、unlessもないのでifの条件式もnotで囲む。

outlength = 0
while outlength < pwlength
do
  -- ...
  if not(c < convlength)
  then
    -- ...
    outlength = outlength + 1
  end
end

「ある段階でこれ以上処理を続ける必要がないフローを除外する」という方法と、「適用されるフロー全体をifで囲む」という方法は基本的に書き換えられるもので、適切性や好みによって使い分けることになる。 Luaの場合はそもそも前者の方法は書けないので、後者の書き方でなければならない。

1バイト読み

RubyはIO#readbyteで1バイト読むと数値として返るが、PerlやLuaにはないので「1バイト読んで、読み込んだ(バイナリの)文字列を数値に直す」という作業が必要になる。

c = urandom:read(1)
c = c:byte()

とはいえ、どちらも比較的簡単にできる部類である。

むすびに

今回は「書き換え」をテーマに、その言語に備わっている機能や、その言語の文化的あるいは文法的側面から適切な記述を行うということを解説した。

前回と比べるとだいぶ難しい内容だが、言語に依存することなく「これを意味する表現方法はどのようなものがあるか」ということを考えるようにすると、プログラミング脳が強化されることだろう。

おまけ PerlとLuaについて

私が普段Rubyばかり書いているので、「Rubyしか書けない人」みたいなイメージがつくといけないな、という発想からPerlとLuaでも書いてみたのだが、書くと思うところは色々あった。

Perlは相当に古い言語であり、基本的には「スーパーsedとスーパーawkのハイブリッド言語」みたいな言語であった。 で、これにシェルスクリプトちっくなグルー機能をもりもり追加されて到達したのがPerl4で、それに、よりモダンで本格的なプログラミング要素を追加して誕生したのがPerl5で、「今から新しくPerlが成し遂げた革命をもう一度やろうぜ」的に作られたのがRaku(Perl6)である。

だからPerlの文法というのは結構シェルっぽい。 Perlの悪口を口汚く言う人はUnixの経験があまりなさそうな、そうでなくともシェルスクリプトにあまり慣れていなさそうな印象を受ける。 Unixシステムにおいては日常的な作業にPerlはすごく重宝するし(それこそスーパーsedやスーパーawkとして)、Unixerしての基本教養だ。

例えばPerlはリストが配列ではなく(平坦な)リストだが、これはシェル(Bash, Zsh)と同じである。 例えばZshなら

ary1=(foo bar baz)
ary2=(quux quuux quuuux)
ary=($ary1[@] $ary2[@])
# ary -> (foo bar baz quux quuux quuuux)

ということになる。また、文字列と数値で比較する演算子が違う、というのも、test(1) (あるいはシェル組み込みのtest)に従ったものだと考えられる。

だが、それ故にちょっと「うっ…」となる部分もある。私としては「仮引数がない」というのはPerlに戻ってくるといつも「うっ」となる。

function foo($i) {
  print($i);
}

みたいな書き方はできない。

function foo( {
  my $i = [0];
  print($i);
}

と書くしかないのである。これは、関数定義ならまぁ許せるのだけど、コールバック用の匿名関数だとすっごく面倒に感じる。

また、文字列や数値といった型は常にダイナミックに変換されるという点も、テキストを読み込むときにはすごく便利なのだけど、スカラーとリストの扱いも含めた「全部コンテキスト依存」という振る舞いは、「型のことなんて考えなくていい、わーいやったぜー」というメリットよりも、「コンテキストを心を砕いて気にしなければいけない」というコストのほうが高くて割とげんなりする。

Perlは振る舞いや機能などに結構古さを感じることが多くて、使っていくと「えぇぇ…」と思うことが多い。珍しく もうちょっと上手いことやってくれないかなぁ、という気持ちになる。特に、UTF-8の先駆者なのにUnicodeの扱いがちょっと微妙なところとか。

Luaは誕生自体は1993年でPythonとかRubyとかと同じ頃だけども、注目されるようになったのはもっと最近。

Luaはそもそも「ミニ言語」の類で、ターゲットは組み込み用途である。処理系が小さくて移植も簡単、しかも動作コストが低く高速というのは、本当に組み込みでは強い。さらにいえば、書き方としても簡素でとてもよろしい。

Wikipediaには文法はPascalに似ていると書いてあるけれども、私はPascalの経験もあるけれど最高に辛かった記憶しかないので(それでも完全に挫折した関数型プログラミング言語よりはマシだが)Pascalより全然書きやすいよ、と言いたい。 なにより、私はLuaで「ちゃんと」コードを書いたのは今回が初だったけれど、プログラミングの基本的な概念がわかっていれば泥縄式でも特に困ることはなかったから、結構習得しやすい部類だと思う。

むしろ私は「Zshっぽいな…」と思った。

ミニ言語の傑作といえばJavaScriptで、JavaScriptとは結構似ている。配列と連想配列が同一型で、配列として使うと速くなる、という点も似ている(最新のJavaScript実装では配列はちゃんと別の型になっていたりするけど)。ただ、JavaScriptが無茶な書き方を許す汎用性の高い単純なルールなのに対し、Luaの場合はあくまで簡素に書かせる。 全体的に、JavaScriptよりもずっとずっとミニだな、と感じる。

ミニ言語だから機能は少ない、という点には結構配慮が必要で、多言語で書いたコードを丸写し、みたいなことはできない。 また、場合によってはLuaの機能で実現できる設計にら改める必要がある場合もある。

でも、Luaは筋のいい(JavaScript同様に)ミニ言語の特徴を備えていて、機能自体は少ないけれど、やりたいことをやるための汎用性がある。 コードの中でユーティリティ関数でも書いてあげれば使い勝手はぐっと上がる。

なにより、(今回は使用していないが)Luaではクロージャとして機能する関数があり、かつその関数がファーストクラスオブジェクトであるという点が非常に大きい。 JavaScriptもそうだけど、これがあるとないとでは大違いである。活用幅がものすごく広いのだ。 逆にクロージャがない言語というのは限界を感じることが多くて、Zshプログラミングではクロージャがないので割とどうしようもない(evalを使って無理やり実装したりする)ことが結構あるし、C++だとクロージャがなくてすごくしんどい思いをすることが結構ある。

個人的な考えだけれども、クロージャの有り難みがわかる、というのはプログラマとしてはひよこレベルを卒業した感がある。

ちなみに、Perlもちゃんとクロージャはあるのだけれど、「匿名関数へのリファレンス(ポインタみたいなもの)がスカラー値だから変数に格納できるよ、デリファレンスすると呼び出しになるよ」というのは、なんか喉から手を突っ込むみたいな気持ちになる。 Rubyは2.0からfunc.()みたいな感じで呼び出せるようになって、完璧ではないながらも結構気持ちよく使えるようになった。まぁ、Rubyはカッコが省略できるから仕方ないね、って感じもある。

Luaはミニ言語だから機能も少なくて、単純にその言語で書く、ということだけ考えると「サイコー!!使いやすい!!」とは全くならない。特に大規模なプログラミングだと機能不足がたたって割としんどそうだな、という印象もある。 が、組み込みで使う、という選択肢の上であれば書きやすいし簡素だし全然アリである。そして、「速い言語が欲しい」という点においても、他の選択肢(JavaScript, Java, C, C++, Rust, Nim)と比べて考えると、「…うん、アリだな」という気持ちになる。

特にコード自体が小さいが、インスタンス数が多く、処理量も多い、みたいなときにはLuaはサクッとかけて軽くて速い、という条件になるので大変素晴らしい。 私の奥義であるところのorbital designとも非常に相性が良さそうだ。

また、Luaが「幸せな言語だな」というのも思う。

どの言語もだいたい人気が出てくると、他の言語を好む人からの横槍が入り、それを取り入れると迷走する、というのがいつものことである。 それによって言語の特長や魅力を見失ってしまう。 振り回された言語の代表格といえばPHPで、JavaScriptも近年はJavaScriptらしくない変更が続いている。

そして、ここまで考えた上で思うのは、「Rubyやっぱいいなぁ」ということである。 他の言語でできる書き方がだいたいRubyでもできる。逆に、Rubyでできる書き方が他の言語でそのままできることは割と少ない。 Ruby機能豊富すぎて書きやすすぎてやばい。惚れ直す。

おまけの宣伝

Mimir Yokohamaではプログラミングの学習に適したサービスを提供している。

家庭教師サービスは(条件は限定されるが)こうした内容をより踏み込んだものや、体系化された知識を授業として受講可能だ。

また、開発サービスに”Educational Code”オプションがあり、そのオプションはオーダーメイドのソフトウェアに、この記事のような解説がコメントとして入ったコードを入手することができる。

もちろん、これらの組み合わせも可能だ。