シーザー暗号 (1)
Haskellは頭を使いたくて暇な時に時間を潰すには最適な遊び道具です。
私は便利ツールや使い捨てスクリプトを書くのは専らRubyですが、
おもちゃとしてはHaskellの方が好きです。
ちょっとしたパズル感覚で頭を捻りながら解いていくのが楽しいからです。
今日はシーザー暗号プログラムを書いてみました。
文字列のアルファベットを一定の数だけずらすものです。
単純な例ですがHaskellの特徴がよくわかって良いかなと思います。
1| import Char
|
2| caesar :: Int -> String -> String
3| caesar n msg
4| = map chr $ map (\x -> shift n (ord x)) msg
|
5| shift :: Int -> Int -> Int
6| shift n x
7| = let (a, m) = case x of
8| _ | x `elem` [largeA..largeZ] -> (largeA, n)
9| _ | x `elem` [smallA..smallZ] -> (smallA, n)
10| _ -> (0, 0)
11| in case m of
12| 0 -> x
13| _ -> ((+ a) . (`mod` alphas) . (+ m) . (flip (-) a)) x
14| where (largeA, largeZ) = (ord 'A', ord 'Z')
15| (smallA, smallZ) = (ord 'a', ord 'z')
16| alphas = 26
実行結果です。
Main>caesar 3 "hello, world!"
"khoor, zruog!"
Main> caesar (-3) "hello, world!"
"ebiil, tloia!"
Main> caesar (-3) $ caesar 3 "hello, world!"
"hello, world!"
以下コード解説。
1: chr,ord関数を使うためにCharモジュールをインポートしています。
2-4: ずらす数と暗号化対象の文字列を引数に取って、シーザー暗号を施した文字列を返す関数ceasarの定義です。
2: 関数の型指定です。曖昧でなければ型推論が行われるので省略することもできます。
4: 関数本体。ordは文字を数値に変換する関数でchrは数値を文字に変換する関数です。
文字列は文字のリストなので、全文字を数値に変換→ずらす→全数値を文字に変換 という流れです。
「ずらす」の部分がshiftという関数に委譲されています。
「map 関数 リスト」でリストの全要素に対して関数を適用したリストを返します。
「\x -> ...」はラムダ式です。schemeの(lambda (x) ...)に当ります。
「$」は右側の関数適用を優先する演算子です。括弧で囲むのが冗長で面倒だと感じる人には嬉しいかも。
「f $ g x」と「f (g x)」は等価です。ちなみに「(f . g) x」も等価です。
5-16: 文字をずらす関数の定義です。
ずらす数と対象の文字を数値に変換した値を引数に取り、ずらされた文字に対応する数値を返します。
7-13: レキシカルスコープです。
「let x = value in ...」でScheme の(let ((x value)) ...)に当ります。
変数定義の左辺をタプルにすれば多重代入が可能です。
7-10: パターンマッチ構文のcaseです。C系のswitch-caseみたいなものです。
でもここではパターンマッチの部分は使わないで条件節で分岐させてます。
caseの文法は以下のようになっています。条件は省略可能です。
case 値 of
パターン | 条件 -> 値
パターン | 条件 -> 値
パターンに「_」を指定すると必ずマッチします。パターンにマッチし、かつ条件が真であれば「->」以降がCaseの値となります。
パターンが「_」で条件がない場合はifのelseと同じ働きをすることになります。
ifを使うの素直かもしれませんが、見た目が汚なくなるので敢えてcaseを使ってます。
ifだとこうなります。どちらも微妙に冗長なのでもっと綺麗に書く方法がないですかねぇ。
Lispのcondがあれば良いのだけど。
if x `elem` [largeA..largeZ] then (largeA, n)
else if x `elem` [smallA..smallZ] then (smallA, n)
else (0, 0)
caseの値がタプルになっていて、これがletで宣言した変数に多重代入されます。
8: [0..5]とやると[0,1,2,3,4,5]に展開されます。これはRubyと同じです。
あと[0..]等とやると無限リストも扱えます。
elemはリストの要素かどうかを返す述語関数です。
elem 0 [0..5] -- True
elem 6 [0..5] -- False
Haskellでは前置記、中間置記を制御できるようになっています。
前置記関数を中間置記として使うにはバッククォートで囲みます。
0 `elem` [0..5] -- True
逆に中間置記の関数を前置記として使うには括弧で囲みます。
3 + 2 -- 5
(+) 3 2 -- 5
13: ここが肝です。部分適用した関数を関数結合演算子「.」で複数繋ぎ合わせています。
Haskellでコード書いているとラムダ式が冗長に見えてきてしまうので、
仮引数を記述しない部分適用関数の連結という抽象的でわかりにくいコードになりがちかもしれません。
この辺がパズル的です。如何に短く美しく書くか。
flipは引数を入れ替える作用をします。
わかりにくいのですが部分適用では必須かと思われます。
何故、引き算だけflipを行う必要があるかというと、(- x)だと-xとして解釈してしまうためです。
(+ x)だと部分適用してくれるんですけど…なんか微妙な仕様です。
14-16: whereもletと同じくレキシカルバインドですが、変数定義が本体の後に来ます。
「let x = value in ...」と「... where x = value」は等価です。
RubyやPerlにある制御構文の倒置表記と同じ感じですね。
たった16行のコードなのにやたら説明長くなってしまいました。
# つかこんな説明でわかる人いるのだろうか…
以上、Haskellの宣伝でした。

># つかこんな説明でわかる人いるのだろうか…
まぁ、シーザー暗号のアルゴリズムは知ってるんで説明無くても大体分かる感じかな。
未知の言語でも処理内容が分かれば大体読める感じですな。私は。
ま、なんでそんなことできるんだとかいわれると、やっぱり解析経験ですかねぇ。
とりあえず、
>ちょっとしたパズル感覚で頭を捻りながら解いていくのが楽しいからです。
ということならば、局長は解析にはまる資質があるかも?とか思うような思わないような。
ま、解析の主な作業はアセンブリ(最適化済み)からCあたりの高級言語のソースを
汲み上げる作業な訳ですが。その後更に理論的に何やってるのかを見ると。
にしても私的に読みにくいなぁと思う。やっぱCはやってることが一目でわかっていいなぁと思う今日この頃。
ちなみにやってることが分かるというものの最高峰はアセンブリな訳で、そこら辺別の意味に取られると誤解が出そう。
まぁ要するに、ここって実際どう動いてるのよみたいなのが全然見えない感じなので読みにくいかなぁと。
とりあえず、例えば配列の全ての要素に対して処理をする構文とか、
配列の特定の(指定された)要素に対して処理をする構文みたいなので、
処理される順番とかそういうのがわからないってのは私は好きじゃない感じです。
perlのforeachに配列投げた時も、本当に配列の順番どおり処理するのか分からないんで
$iを0から回してますわ。順番気にするときは。
ま、実験してみりゃ良いんですがね。面倒なのと、仕様変更で変わったりしたら嫌なのとで。
活用形は Haskell, Haskeller, Haskellest でよろしいですか? Haskell もよいですね。最近、そういったたぐいの言語の美しさが分かってきました。8 命令しか持たないチューリングマシンを記述できる Brainfuck という言語もいとおかしですよねw
>> wiz
> ということならば、局長は解析にはまる資質があるかも?とか思うような思わないような。
どうだか…
資質以前に興味が沸かないというのが正直なところですが。
> にしても私的に読みにくいなぁと思う。やっぱCはやってることが一目でわかっていいなぁと思う今日この頃。
この辺の感覚は正反対なんですよねぇ。
私が言語に求めるものって
1. ルールが少ないこと(統一性)
2. 冗長度が低いこと(抽象化能力が高く無駄なく書けること)
3. 素直に記述できること(脳内イメージがスムーズにコードに写せること)
でして、それぞれ私的一番は、
1. Scheme
2. Haskell
3. Ruby
アセンブリは主に2が激しく×です。
>>yu-bon
冗談だとは思うけどHaskellをBrainfuckなんかと一緒にしないでくださいw
Haskellはやはり冗長性のなさという点でかなり極まってる気がします。
ただ冗長性がないことが必ずしも良いとは言えないんですよねぇ。
実際Haskellのコードって読みにくいし。
アセンブリで書いてると2の理由で激しく面倒になりますな。私的にはアセンブリは3は合格ですが。ニヤニヤ
1は少ないような多いようなって感じですかねぇ。表面的には非常に制約が大きい言語なんだけど、
本質的には最強の自由度を持っているんで。
まあ要するに、文法の制約はかなりきついけど、自己書き換えとかとんでもないことまで出来ちゃうと。
個人的に冗長で気になるところといえばやはりループの前処理かな。
たまにループの前処理でループ内と似たようなことを書かないといけないことがあったりして
そういう時はなんとか無駄をなくせないかと考えてみたりする。
ただ、個人的にループの中に(無駄な)ifを入れるというのはタブーなのでできん。
繰り返し1回辺り1clockの損失とTLB(table lookside buffer)を1個消費するんで。
ま、そこまで考える必要は無いんだけどな。普通。
>冗談だとは思うけどHaskellをBrainfuckなんかと一緒にしないでくださいw
冗談というか単に趣があるという話のようで。まぁ良いんですが。
>Haskellはやはり冗長性のなさという点でかなり極まってる気がします。
>ただ冗長性がないことが必ずしも良いとは言えないんですよねぇ。
>実際Haskellのコードって読みにくいし。
perlみたいに冗長性を削りすぎると訳わかんないレベルにもなるしねぇ。
見た感じバイナリファイル開いたのかと思えるプログラムってのも考え物ですなぁ。
>冗談というか単に趣があるという話のようで。
個人的にはBrainfuckなんて暇人の受け狙いとしか思えないので… Whitespaceとかね。
別に批判するつもりではないですが正直な感想として。
>perlみたいに冗長性を削りすぎると訳わかんないレベルにもなるしねぇ。
perlの削り方って文字数減らしてるだけのような…
覚えることはむしろ多くなっているので本質的には冗長性を削っていないと思うんですね。
Hakellの削り方は綺麗ですよ。体系的で合理的ですから。