JUS共催勉強会 なぜシェルに仕事をさせてはいけないのか? USP友の会 会員 鳥海秀一
JUS共催勉強会なぜシェルに仕事をさせてはいけないのか?USP友の会 会員 鳥海秀一
自己紹介
名前:鳥海秀一職業:金融系のSE
ここ2年間、業務ではプログラムを組んでません
所属:USP友の会(2009年4月~)
日本UNIXユーザ会(2014年12月~)
この勉強会のきっかけ
2014年12月13日~14日開催JUS シェルスクリプトワークショップ in 鳥取
シェルスクリプトワーックショップ IN 鳥取
講師
今泉光之(USP友の会)
斉藤博文(日本GNU AWKユーザ会)
斎藤明紀(鳥取環境大学)
詳しくはWEBで
https://www.usptomo.com/PAGE=20141231JUSWORKSHOP
斎藤先生が示したパネル
シェルに仕事をさせてはいけない
シェルは glue language
他のコマンドに仕事をさせる
シェルは仕事手順の制御だけ
なぜ、シェルに仕事をさせてはいけないのか?
実際に、仕事をさせてみよう!
仕事?
仕事=重めの処理
本日は、nクイーン問題を選択
nクイーン問題とは
もともとは1848年に考案された、チェス盤(8×8マス)と8個のクイーン(飛車と角行を合わせた動きをする駒)を利用したパズル
どのクイーンもお互いのきき筋にいないようにする配置を求める
8クーイン問題をn×nのマスに応用したのがnクイーン問題
2クイーンと3クイーンには解がない
nが増えると計算量が爆発的に増える。解が判明しているのは26クイーン問題まで
nクイーン問題の解の表現方法
(例)4クイーン
2 4 1 3↩️3 1 4 2↩️ と数の列で表現
nクイーン問題の解法
nクイーン問題はバックトラック法で解く問題の代表例
解法はプログラムの入門書に必ずと言って良いほど登場
バックトラック法の概説
0 1 2 3
1
2
3
4
再帰
ループ
メモ行上斜め下斜め
1
1
1
3
4
2
プログラム例(JavaScript)
function nqueens(size) { var row=[], up=[], down=[], board=[]; (function queen(n) { if (n >= size) { console.log(board.join(' ‘));
} else { for (var i = 0; i < size; ++i) { if (!row[i] && !up[n+i] && !down[n-i+size-1]) { row[i]=up[n+i]=down[n-i+size-1]=1; board[n] = i+1; queen(n+1); row[i]=up[n+i]=down[n-i+size-1]=0;
} }
} }(0));
} nqueens(process.argv[2]);
様々な言語のプログラム例
https://github.com/umidori/nqueens/tree/master/procedural
例を参考にBashでnクイーン問題の解法プログラムを作成してください
実演してみます
プログラムの作成方針
1. 4クイーン問題を解くプログラムを作成
2. 4クイーン問題をnクイーン問題に拡張
全てのクイーンの組みを出力するプログラムを作成
同一行のクイーンを排除
斜め上のクイーンを排除
斜め下のクイーンを排除
4クイーンの全ての組を出力board=() queen() { local i
if (($1 >= 4)); then echo ${board[@]} else for ((i = 1; i <= 4; ++i)); do board[$1]=$i queen $(($1 + 1)) done fi } queen 0
同一行のクイーンを排除board=() row=() queen() { local i
if (($1 >= 4)); then echo ${board[@]} else for ((i = 1; i <= 4; ++i)); do if ((!row[i])); then row[$i]=1 board[$1]=$i queen $(($1 + 1)) row[$i]=0 fi done fi } queen 0
斜めのクイーンを排除board=() row=() up=() down=() queen() { local i if (($1 >= 4)); then echo ${board[@]} else for ((i = 1; i <= 4; ++i)); do if ((!row[i]&&!up[$1+i]&&!down[$1-i+4])); then ((row[$i] = up[$1+i] = down[$1-i+4] = 1)) board[$1]=$i queen $(($1 + 1)) ((row[$i] = up[$1+i] = down[$1-i+4] = 0)) fi done fi } queen 0
nクイーン問題に拡張n=$1 board=() row=() up=() down=() queen() { local i if (($1 >= n)); then echo ${board[@]} else for ((i = 1; i <= n; ++i)); do if ((!row[i]&&!up[$1+i]&&!down[$1-i+n])); then ((row[$i] = up[$1+i] = down[$1-i+n] = 1)) board[$1]=$i queen $(($1 + 1)) ((row[$i] = up[$1+i] = down[$1-i+n] = 0)) fi done fi } queen 0
12クイーンを解いてみてください
言語別12クイーン解答時間(参考値)(Mac Mini CPU:2.3GHz Core i7 メモリ:8GB)
言語 処理時間C言語 0.14秒Java8 1.12秒C# 0.55秒
Haskell 15.0秒Ocaml 0.14秒
Perl 2.59秒Python 2.48秒Ruby 1.94秒PHP 3.08秒
JavaScript 2.11秒Gauche 1.21秒
Bash 6分24秒
なぜシェルに仕事をさせてはいけないのか?
1.処理速度が遅い
2.スクリプトが組みづらい(環境からの支援が乏しい)
シェルが遅いのはなぜか
答え 変数のアクセス効率が良くないから
1. 変数は全て「変数名=値」という文字列
2. 配列は高速なランダムアクセスを実現しない
では、どうするか?
斎藤先生が示したパネル
シェルに仕事をさせてはいけない
シェルは glue language
他のコマンドに仕事をさせる
シェルは仕事手順の制御だけ
シェルは glue language
シェルスクリプトでは、glue(のり)は主にパイプのこと
シェルスクリプトではパイプをうまく使うことが肝要
パイプをうまく使うには発想の転換が必要
例えば、nクイーン問題では後戻りしないバックトラック法という発想が必要になる
後戻りしないバックトラック法の概説
1
2
3
4
計算の進行
AWKとBashで作成してみます
プログラムの作成方針
1. 4クイーン問題を解くプログラムを作成
2. 4クイーン問題をnクイーン問題に拡張
全てのクイーンの組みを出力するプログラムを作成
同一行のクイーンを排除
斜め上のクイーンを排除
斜め下のクイーンを排除
4クイーンの全ての組を出力f() { awk '{for(i=1;i<=4;++i)print $0,i}'; }
echo | f | f | f | f
同一行のクイーンを排除f() { awk '{for(i=1;i<=4;++i)print $0,i}'; } g() { awk '{for(i=1;i<NF;++i)if($i==$NF)next;print}'; }
echo | f | g | f | g | f | g | f | g
斜めのクイーンを排除f() { awk '{for(i=1;i<=4;++i)print $0,i}'; } g() { awk '{for(i=1;i<NF;++i)if($i==$NF)next;print}'; } h() { awk '{for(i=1;i<NF;++i)if($i+i==$NF+NF)next;print}'; } i() { awk '{for(i=1;i<NF;++i)if($i-i==$NF-NF)next;print}'; }
echo | f | g | h | i | f | g | h | i | f | g | h | i | f | g | h | i
関数をまとめるf() { awk '{for(i=1;i<=4;++i)print $0,i}' | awk '{for(i=1;i<NF;++i)if($i==$NF)next;print}' | awk '{for(i=1;i<NF;++i)if($i+i==$NF+NF)next;print}' | awk '{for(i=1;i<NF;++i)if($i-i==$NF-NF)next;print}' }
echo | f | f | f | f
AWKプログラムをまとめる
f() { awk '{for(i=1;i<=4;++i)print $0,i}' | awk '{for(i=1;i<NF;++i)if($i==$NF||$i+i==$NF+NF||$i-i==$NF-NF)next;print}' }
echo | f | f | f | f
さらにまとめるf() { awk '{for(i=1;i<=4;++i){for(j=1;j<=NF;++j)if($j==i||$j+j==i+NF+1||$j-j==i-NF-1)break;if(j>NF)print $0,i}}' }
echo | f | f | f | f
nクイーンへの拡張法
1. ループ文を使用して、繰り返しを実現する
1. 変数に中間結果を格納する
2. ファイルに中間結果を格納する
2. 再帰を使用して、繰り返しを実現する
1. パイプに中間結果を通す
変数に中間結果を格納する場合
n=$1
f() { … }
tmp="" for ((i=0;i<n;++i)); do tmp=$(echo "$tmp" | f) done echo "$tmp"
ファイルに中間結果を格納する場合
n=$1
f() { … }
echo >tmp for ((i=0;i<n;++i)); do (rm tmp; f >tmp) <tmp done cat tmp rm tmp
パイプに中間結果を通す場合
n=$1
f() { if (($1==0)); then echo else f $(($1-1)) | … fi }
f $1
パイプを使ってnクイーンに拡張
n=$1 queen() { if (($1 == 0)); then echo else queen $(($1 - 1)) | awk '{for(i=1;i<='$n';++i){for(j=1;j<=NF;++j)if($j==i||$j+j==i+NF+1||$j-j==i-NF-1)break;if(j>NF)print $0,i}}' fi } queen $1
12クイーンを解いてみてください
パイプを使ったシェルスクリプトの特徴
1. 利点
1. 処理速度が速い
2. スクリプトが組みやすい
2. 欠点
1. プロセス数を意識する必要がある
2. 手続き的な発想とは異なる発想を必要とする
パイプを使うスクリプトの発想
パイプを使うには手続き型プログラミングの発想ではなく関数型プログラミングの発想が必要となる
SICPに載っているLISPによるnクイーンの解法
(define (queens board-size) (define (queen-cols k) (if (= k 0) (list empty-board) (filter↲ (lambda (positions) (safe? k positions)) (flatmap (lambda (rest-of-queens) (map (lambda (new-row) (adjoin-position new-row k rest-of-queens)) (enumerate-interval 1 board-size))) (queen-cols (- k 1)))))) (queen-cols board-size))
関数が第一級オブジェクト参照透過性遅延評価カリー化並列化
関数型プログラム言語とパイプを使うシェルスクリプトとの共通点は?
閉包性の活用
閉包性とは
抽象代数からきた言葉
集合の要素にある演算を作用させて得られた要素が、また集合の要素であるなら、要素の集合はその演算のもとで閉じているという
例えば、整数は加算、減算、乗算に対しては閉包性をもつ。整数に対するいずれの演算も、結果は整数になるため
SICPでの閉包性の説明
困ったことに多くの普通のプログラム言語にあるデータ組合せ演算は閉包性を満足せず、閉包性を活用するのは煩わしい。(中略)対の使えるLispと違って、これらの言語には合成データを一様に操作するのが容易になる汎用の糊はない。
閉包性を活用する言語の例
Lispリストに対するmap,filter,flattenなどの演算
SQLリレーションに対する8つのリレーショナル代数演算
シェルテキストに対するhead,tail,sortなどのコマンド
関数型言語の発想に慣れるには
松
竹
梅手続き的プログラムを関数的プログラムに書き換えてみる
SICP(計算機プログラムの構造と解釈)を読む
シェル芸勉強会に参加する
関数型言語としてのシェルの利点
開発環境開発環境はターミナル画面であるため超軽量
処理速度パイプを利用し、関数的に解いた方が速度的に有利
記述順と実行順の一致シェルスクリプトでは記述順と実行順が一致する
言語別12クイーン解答時間(参考値)(Mac Mini CPU:2.3GHz Core i7 メモリ:8GB)
言語 手続き的プログラム 関数的プログラムC言語 0.14秒 ーJava8 1.12秒 2.78秒C# 0.55秒 2.17秒
Haskell 15.0秒 5.36秒Ocaml 0.14秒 3.53秒
Perl 2.59秒 34.4秒Python 2.48秒 11.9秒Ruby 1.94秒 17.0秒PHP 3.08秒 23分03秒
JavaScript 2.11秒 16分35秒Gauche 1.21秒 29.8秒
Bash 6分24秒 11.0秒
シェルでは記述順と処理順が一致
普通、関数的なプログラムでは右から左に処理が進む
Perlを使った4クイーンの解答例(右(この場合は下)から左(この場合は上)に処理が進行)
print map{join(" ",@$_)."\n"} map{$l=scalar(@x=@$_);map{[@x,$_]}grep{$y=$_;!grep{$y==$x[$_]||$l-$_==abs($y-$x[$_])}0..$l-1}1..4} map{$l=scalar(@x=@$_);map{[@x,$_]}grep{$y=$_;!grep{$y==$x[$_]||$l-$_==abs($y-$x[$_])}0..$l-1}1..4} map{$l=scalar(@x=@$_);map{[@x,$_]}grep{$y=$_;!grep{$y==$x[$_]||$l-$_==abs($y-$x[$_])}0..$l-1}1..4} map{$l=scalar(@x=@$_);map{[@x,$_]}grep{$y=$_;!grep{$y==$x[$_]||$l-$_==abs($y-$x[$_])}0..$l-1}1..4} []
Bashを使った4クイーンの解答例(左(この場合は上)から右(この場合は下)に処理が進行)
echo | awk '{for(i=1;i<=4;++i){for(j=1;j<=NF;++j)if($j==i||$j+j==i+NF+1||$j-j==i-NF-1)break;if(j>NF)print $0,i}}' | awk '{for(i=1;i<=4;++i){for(j=1;j<=NF;++j)if($j==i||$j+j==i+NF+1||$j-j==i-NF-1)break;if(j>NF)print $0,i}}' | awk '{for(i=1;i<=4;++i){for(j=1;j<=NF;++j)if($j==i||$j+j==i+NF+1||$j-j==i-NF-1)break;if(j>NF)print $0,i}}' | awk '{for(i=1;i<=4;++i){for(j=1;j<=NF;++j)if($j==i||$j+j==i+NF+1||$j-j==i-NF-1)break;if(j>NF)print $0,i}}'
まとめ
シェルに仕事をさせてはいけない
仕事をさせる場合はパイプを使う
パイプを使いこなすには発想の転換が必要
発想の転換にはシェル芸勉強会が最適
ここで、シェル芸問題をひとつ
nクイーンの数列(例えば、2 4 1 3)を次のようなアスキーアートに変換してみましょう。+---+---+---+---+ | | | Q | | +---+---+---+---+ | Q | | | | +---+---+---+---+ | | | | Q | +---+---+---+---+ | | Q | | | +---+---+---+---+
解答例1
awk '{ printf "+";for(i=1;i<=NF;++i)printf "---+";print ""; for(i=1;i<=NF;++i){printf "|";for(j=1;j<=NF;++j) if(i==$j) printf " Q |"; else printf " |";print ""; printf "+";for(j=1;j<=NF;++j)printf "---+";print ""}}'
解答例2
while read i; do a=($i); printf $(printf "+%0${#a[@]}s\\\n%${#a[@]}s\n" | sed "s/ /|%${#a[@]}s\\\n+%0${#a[@]}s\\\n/g") | sed "s/0/---+/g;s/ / |/g" | eval sed "$(eval echo {1..${#a[@]}} | sed 's#[0-9][0-9]*# -e "$((${a[&-1]}*2))s/...|/ Q |/&"#g')"; done