おもこん

おもこんは「思いつくままにコンピュターの話し」の省略形です

徒然Ruby(43)StrScanライブラリで字句解析

電卓プログラムの記事で予告したStrScanライブラリの話です。

StrScanライブラリのドキュメント

Rubyのドキュメントに説明があります。 簡単な説明ですが、それで使い方はわかると思います。

字句解析とは

字句解析は、主にプログラミング言語で使われます。 例えば次のようなRubyプログラムを考えてみましょう。

def hello
  print "Hello world\n"
end

このプログラムを分解すると、次の表のようになります。

文字列 タイプ
def 予約語def
空白 空白
hello 識別子 hello
改行 改行
空白 空白
空白 空白
print 識別子 print
空白 空白
"Hello world\n" 文字列 Hello world\n
改行 改行
end 予約語end
改行 改行

この中の空白は、区切りという意味しかなく、それがなくてもRubyインタープリタはプログラムを解釈できます。 ですので、これらは捨てられてしまいます。 なお、改行は言語によって空白と同じように捨てられることもあります。 Rubyは改行によって、文や式の区切りを表すことがあるので、残しておきます。 空白以外の要素はインタープリタに送られます。 それらの要素は字句またはトークン(token)と呼ばれます。

トークンはタイプと値のセットにします。 タイプは「トークン・カインド(token kind)」とも呼ばれます。 タイプの部分には文字列またはシンボルを使うことが多いです。 例えば「予約語のdef」はシンボル:DEFなどです。 ここで大文字を使ったのは、トークン・カインドに大文字を使う習慣があるからです。 ここでは、シンボルでトークン・カインドを表すことにしましょう。

タイプ トークン・カインド
予約語def :DEF
改行 :NL
識別子 :ID
文字列 :STR
予約語end :END

トークンを配列の形でまとめると、

[ [:DEF, "def"],
  [:ID, "hello"],
  [:NL, "NewLine"],
  [:ID, "print"],
  [:STR, "Hello world\n"],
  [:NL, "NewLine"],
  [:END, "end"],
  [:NL, "NewLine"]
]

となります。 配列の要素はすべて「タイプ」「値」という形になります。 予約語の場合はタイプだけで用が足りるので、値は何でも構いません。 [:DEF,"def"][:DEF,nil]としても、構文解析上は問題ありません。

このように、プログラムの文字列を、その言語の要素に分解することを「字句解析」といいます。 字句解析は、文字列のパターンを見つけることですから、正規表現のあるRubyでは簡単にできます。 しかし、短い時間でパターンを発見するにはStrScanライブラリを使うのが良いそうです。

StrScanライブラリ

StrScanライブラリの使い方をまとめると、次のとおりです。

  1. strscanライブラリをrequireする
  2. StringScannerインスタンスを生成する。そのとき、引数に対象となる文字列を与える
  3. scanメソッドでパターンを比較する

具体例を示しましょう。

require 'strscan'

s = StringScanner.new("def hello\n  print \"Hello world.\n\"\nend\n")

p s.scan(/def/) #=> "def"
p s.scan(/hello/) #=> nil
p s.scan(/[[:blank:]]/) #=> " "
p s.scan(/hello/) #=> "hello"
p s[0] #=> "hello"
p s.eos? #=> false
  • 1行目:strscanライブラリを取り込む
  • 3行目:StringScannerインスタンスを生成。引数が対象文字列。
  • 5行目:scanメソッドは、文字列の先頭が正規表現/def/に一致すれば、その文字列"def"を返す。文字列検索のポインタはdefの次の文字に移動
  • 6行目:scanメソッドは、ポインタ位置の文字列が正規表現/hello/に一致しないので、nilを返す
  • 7行目:scanメソッドは、ポインタ位置の文字列が正規表現/[[:blank:]]/(空白またはタブ)に一致するので、その文字列" "を返す。文字列検索のポインタは空白の次の文字に移動
  • 8行目:scanメソッドは、ポインタ位置の文字列が正規表現/hello/に一致するので、その文字列"hello"を返す。文字列検索のポインタはhelloの次の文字に移動
  • 9行目:[0]メソッドは、前回のマッチ文字列を返す。マッチが失敗していればnilを返す。また、正規表現でカッコを使うとマッチの一部をs[1]、s[2]、・・・で取り出せる
  • 10行目:ポインタが文字列の最後まで進んでないので、falseを返す。

StrScanライブラリを使った字句解析

では、そきほどのRubyコードの字句解析プログラムを作ってみましょう。

require 'strscan'

s = StringScanner.new("def hello\n  print \"Hello world.\n\"\nend\n")

token = []
until s.eos?
  if s.scan(/[[:blank:]]+/)
    ; # throw away space/tab characters
  elsif s.scan(/\n/)
    token << [:NL, "NewLine"]
  elsif s.scan(/def/)
    token << [:DEF, "def"]
  elsif s.scan(/end/)
    token << [:END, "end"]
  elsif s.scan(/"([^"]*)"/)
    token << [:STR, s[1]]
  elsif s.scan(/[[:alpha:]][[:alnum:]]*/)
    token << [:ID, s[0]]
  else
    raise "Unexpected character."
  end
end
p token

until文で、ポインタが文字列の終端になるまで繰り返します。 if-elsif文で順に空白、改行、予約語def、予約語end、文字列、識別子、その他に一致するかを見て、それぞれに対応するアクションを行います。 空白は無視され、「その他」には「Unexpected character.」の例外を発生させます。 これを実行すると次のような結果を得ることができます。

[[:DEF, "def"], [:ID, "hello"], [:NL, "NewLine"], [:ID, "print"], [:STR, "Hello world.\n"], [:NL, "NewLine"], [:END, "end"], [:NL, "NewLine"]]

なお

  • このプログラムでは文字列内のバックスラッシュ記法や式展開はサポートされず、ごく単純化した形になっています
  • Rubyの字句解析が目的であれば、Ripperライブラリがまさにそのためのライブラリです

実例

StrScanライブラリを用いた実例として、GitHubにCalcレポジトリがあります。

ファイルracc/calc.yの57行から77行のlexメソッドが電卓プログラムの字句解析部分になります。 Calcがどのような機能を持っているかはREADME.mdファイルを参照してください。

この字句解析は、Racc(パーサ・ジェネレータ)で使うことを前提にしています。 Raccの場合、トークン・カインドは次のような形になります。

  • 識別子をIDで表すことにすると、トークン・カインドはシンボル:IDにする
  • 演算子、例えば和の記号+などは、その文字列自身をトークン・カインドにする。注意すべきは、それは文字列"+"であって、シンボル:+ではない

次回はパーサ・ジェネレータのRaccについて投稿予定です。

筑波山でハイキング

筑波山とは?

筑波山を知らない人は少ないとは思いますが、一応簡単に紹介を。

筑波山茨城県にある山で、標高877mとさほど高くはないが、平らな関東平野の中に突き出ているので、登山としては結構大変です。 あまり気楽に登り始めると、途中できつくなるかも・・・

主なルートは3つあり、

  • 御幸ケ原コース
  • 白雲橋コース
  • おたつ石コース

また、歩かなくても、ケーブルカーまたはロープーウェイで山頂付近まで行くことができます。

今回は白雲橋コース

タイトルは「ハイキング」と書きましたが、結構険しいです。 特に最後の方の岩場が疲れる。 そこには、数々の巨岩があり、岩を見る楽しさもあります。

弁慶七戻り

この岩には「弁慶七戻り」という名前がついています。 今にも落ちてきそうな巨岩に、さすがの弁慶も怖気づいた、という意味の名前でしょう。 この岩の下がコースなので、どうしてもここを通らないといけません。 怖いですね。

女体山山頂

白雲橋コースは女体山山頂に通じます。 筑波山には女体山と男体山がありますが、女体山の方が少し高いです。

女体山御本殿

この写真は女体山の頂上にある御本殿です。 そのすぐ先に頂上があります。

筑波山山頂

山頂からは、筑波の平野が一望できます。 また、ロープーウェイもここから見ることができました。

ロープーウェイ

白く小さく見えるのがロープーウェイです。 わかりますか?

男体山にも挑戦

女体山から尾根伝いに男体山まで行くことができます。 距離は、そうありません。 男体山には展望台があって、ここからも関東平野を一望することができます。

男体山山頂から10分ほど下ったところにケーブルカーの駅があります。 帰りはケーブルカーで戻りました。 膝が痛くなり始めていたので、助かりました。

最後に筑波神社にお参り

最後に筑波神社に行きました。

筑波神社

この神社の前で、結婚式をあげた方が記念撮影をしていました。 筑波の神様の前で式を挙げるのも良いものですね。

ちなみに筑波神社から女体山までのタイムは2時間半ほどでした。 これはかなり遅いですが、険しい山道ですので、自制気味に登った結果です。

麓のホテルでお風呂に入ることができます。 汗を流してさっぱりし、疲れも癒やすことができました。

電卓プログラムにRaccとStrScanライブラリを使用

今回の電卓プログラムは、最近投稿した計算結果が分数になる電卓とは別のプログラムで、小数を用いる普通のタイプの電卓です。

電卓プログラムCalc

このプログラムCalcは以前GitHubに置いてあったものですが、最近アップデートしました。 このアップデートは大きなもので、ほとんど書き換えといって良いものです。 ポイントは、Rubyの標準添付ライブラリのRaccとStrScanを使ったことです。

Raccとは?

Raccは有名なコンパイラコンパイラYaccRubyバージョンで、Rubyに同梱されています。 コンパイラコンパイラは「コンパイラを作成するコンパイラ」という意味ですが、古い言葉です。 今ではパーサ・ジェネレータの方がよく使われます。 これは「構文解析プログラムを生成するプログラム」という意味です。

RubyのドキュメントにはRaccの項目がありますが、ほとんど何も書かれていません。 作者のウェブサイトにドキュメントがあるので、それが参考になります。

Raccについては、別にブログ記事を書きたいと思いますので、ここでは説明を略します。

StrScanとは?

StrScan(ライブラリ名は小文字)は文字列を高速にスキャンするライブラリです。 StringScannerクラスのインスタンスを作成するときに、対象となる文字列を引数に与えます。 そのインスタンスに対してscanメソッドを使い、スキャンします。 例がRubyのドキュメントにあるので、それをご覧になれば良くわかると思います。

これを使うと、字句解析プログラムを簡単に作ることができます。 もともとRubyには正規表現があるので、字句解析プログラムを作るのは簡単ですが、このクラスはそれを高速に行うことができます。 このライブラリについても後日別に記事を投稿しますので、詳しいことはそちらをご覧ください。

電卓プログラムについて、再び

電卓プログラムはCalcという名前で、GitHubに登録してあります。

Raccを使ったので驚くほど簡単に、短くなりました。 今まで構文解析Rubyで書いていたのに比べると、格段に楽です。 Raccのソースファイルはたったの84行です。

Raccをどのように使うかは、このレポジトリのraccフォルダの下にあるdoc.mdを見てください。

また、このアップデートからは、gemのスタイルを取るようにしました。 しかし、Rubygemsには登録していません。 すでに同名のgemが登録されているので、別名にしないと登録できないのです。 電卓プログラムはいろいろなものが既に普及しているので、Rubygemsに入れるまでもないということで登録を見送りました。

ただ、この電卓は三角関数などもサポートしているので、便利ではあります。 興味がでたら、ぜひ使ってみてください。

計算結果が分数になる電卓

電卓の答えは小数になる

皆さんよくご存知のことですが、電卓で割り算するとその答えは(割り切れなければ)小数になります。 物差しで長さを図るときには、2.3cmのように小数になり、分数にはなりません。 ですから、電卓が小数を使うことは、理にかなっているといえます。

数学では小数が問題になる

ところが、数学では小数が問題になります。 というのは、割り切れないときに小数はその数を正確に表すことが難しいのです。 例えば「サンブンノイチ」

 \displaystyle \frac{1}{3}=0.3333 ...

右辺をどこかで四捨五入すると誤差が生じます。 したがって、その数を正確に表すには無限小数を使わなければなりません。

では無限小数を使えば問題がなくなるかというと、そうではありません。 これでも困ったことが起こります。 それは、0.9999...という無限小数です。 これは1に等しいことが知られています。

 0.9999 ... = 1

そうなると、ひとつの数字が、1と0.9999...という2つの表現を持つことになります。

これらの問題は小数を用いることからきているのです。 それを避けるために数学では分数を主として用います。

分数の計算を分数でしたい

そうなると、分数の計算を分数のまましたくなります。 いつもは小数で良いけれど、時々は分数も使いたい。 でも電卓は分数には対応していない。

そこで、分数を使えるプログラムをさがすと、以前紹介したmaximaなどがそれにあたります。 それで十分ではあるのですが、Rubyで分数をサポートする電卓を作ってみました。

ポイントは式の構文解析

分数の計算自体はRubyのRationalクラスがサポートしています。 ですので、プログラム中で分数計算するのは何も問題ありません。 ポイントは入力された文字列を構文解析して、プログラムに「式として理解させる」ことです。 そのために、例えば1/3+1/2という式を

     +
   /   \
  /     /
 / \   / \ 
1   3 1   2

図のように木構造のデータに直し、それを計算していきます。 ごく簡単に要点をまとめると

この3つになります。

GitHubのレポジトリ

プログラムをGitHubのMath_Programsというレポジトリにアップロードしてあるので、詳細はそちらをご覧ください。 なお、該当のプログラムは

lib/math_programs/acr.rb

です。

このプログラムはgemの形式になっているので、gem buildでビルドして、gem installでインストールすると、コマンドラインから使うことができます。 実行してみましょう。

$ mp23 acr 1/2+1/3
5/6

このように、答えが分数になります。 このレポジトリには他に不定方程式、分子が1になる分数の和で表す問題、拡張テンパズルも入っています。 これらは以前このブログでとりあげたので、そちらも参考になると思います。

拡張テンパズル

YoutubeのQuizKnockを見ていて、面白い問題を人間とコンピュータ(のプログラムを作る人間)が勝負していました。 動画はもちろん面白かったのですが、このパズルを解くプログラムを自分でも作ってみたくなりました。

このパズルは例えば次のようなものです。

数字1, 2, 3, 4, 5と四則演算(加減乗除のこと)をつかい、その式の計算結果が20になるような式を見つけよ。

答えは複数ある可能性がありますが、ひとつ見つければ良いことにします。 例えば、

 3\times 5-1+2+4=20

はひとつの答になっています。

パズルの名前

このパズルは有名なのでしょうか?またその呼び名はあるのでしょうか? ネットを探してみると、「テンパズル」または「メイクテン」というのが、これに似ています。

ただし、テンパズルは与えられる一桁の数字が4個で、計算結果は10に固定されています。 計算結果が10ということが「テン」パズル、あるいはマイク「テン」の元になっているようです。

拡張テンパズル

このパズルの条件を拡張します。

  • 計算式に使う数字は4個でなくても良い。2個以上なら良いものとする
  • 計算式に使う数字は二桁以上でも良い。要するに自然数なら良い
  • 計算結果は10でなくても良い

これをここでは「拡張テンパズル」と呼ぶことにします。 拡張テンパズルを解くプログラムをRubyで作ってみました。 GitHubにすでにアップロードしてあります。

(9/29 追記)

このレポジトリのプログラム全体が大きく変更されました。 そのため、以下の記事とレポジトリの説明文書が異なる部分があります。 その場合は、レポジトリ内の文書の方が正しいです。 また、変更後のプログラムはgem形式になっていて、requireするときは、ライブラリ全体を対象に行います。

require 'math_programs'

下記のプログラムのrequire_relative文は、このように置き換えるなどしてみてください。

(追記 終わり)

プログラム名の「e10p.rb」がextended TenPuzzleからきています。 このプログラムはライブラリであり、メインプログラムを作る必要があります。 demo.rbがそのような例になっているので、参考にしてください。

プログラムの実行

プログラムを実行するためのメインプログラムの例を示します。

require_relative "e10p.rb"

numbers = ARGV.dup.map{|s| s.to_i}
sum = numbers.pop
puzzle = E10P.new(numbers, sum)
print "#{puzzle.solve}\n"

これを実行するときに、引数に「式に使う数字」と「合計」を空白区切りで指定します。 例えば、式に使う数字が11, 20, 33で、合計が42とすると、

$ ruby main_program.rb 11 20 33 42
33-(11-20)

このように答えが表示されます。

テンパズルもやってみましょう。

$ ruby main_program.rb 2 3 5 8 10
5*(8-2*3)

面白いのは、分数が一度出てくるような例があることです。

$ ruby main_program.rb 1 1 9 9 10
9*(1+1/9)

プログラムの仕組み

例 2,4,5,6から10を作る

答えの一例は

 5\times(2\times 4-6)

です。 これは木構造で表すと

  *
 / \
5   -
   / \
  *   6
 / \
2   4

となります。 一番下の階層は2*4になっています。 この結果は8ですが、これから、この問題は、8と5, 6の3つで10になる計算を見つければ良いことになります。 もちろん、最初から2と4を組み合わせれば良いことは分かりませんから、4つの数字から2個をとる順列を生成し、虱潰しに探していきます。

数字がひとつ減ったので、今と同じ方法を用いると、次は数字が2つになります。 このようにして数字を減らしていって、計算結果が10になる場合を見つければ良いことになります。 これは、再帰呼出しです。

GitHubのプログラムでは、solve_realというメソッドがそれに当たります。

この問題はプログラム化が結構難しいと思いますが、Rubyのクラスを導入して木構造を作ると短くまとめることができます。

数式処理アプリケーションMaxima

Maximaとは

Maximaは古く歴史のあるアプリケーションですが、現在でも開発が行われています。 いわゆる「数式処理」をするアプリです。 電卓やPCの計算は数字に対するもので、整式などは扱えません。 例えば、次の式を展開する計算はできません。

 (x+1)(x+2)

このような計算をすることを「数式処理」といいます。 Maximaは数式処理をするアプリです。 詳しくはドキュメントを参照してください。 ここでは簡単な例のみ取り扱います。

2つのMaxima

Ubuntu Softwareというアプリケーションを開き「Science」ボタンからMaximaを探すと

の2つがあります。

どちらもMaximaなのですが、WxMaximaはGUIに対応しています。 1番目のMaximaは端末からCUIベースで操作します。

Ubuntu Softwareから簡単にインストールすることができます。

Windows版もあります。 ダウンロードサイトの説明を見てダウンロードしてください。

式の展開と因数分解

Maximaはコマンド、引数、セミコロンがまとまりになります。 ここでは2つのコマンドを試してみましょう。

  • expand: 引数で与えられた式を展開する
  • factor: 引数で与えられた式を因数分解する

式は、四則(+-*/)と累乗(^)が可能です。  x^2+2x+3は「x^2+2*x+3」と書きます。

まず、Maximaを起動して、展開、因数分解した画像を見てください。

Maxima

CUIベースなので、指数が同サイズのフォントで1行上に現れるので見にくいです。 しかし、きちんと展開、因数分解ができていることがわかると思います。

なお、乗算記号(*)とセミコロンを忘れないようにしてください。 セミコロンを忘れると、次の行に入力が繋がりますので、そこでセミコロンを打つと計算をしてくれます。 終了するときには

quit();

とタイプしてください。

次にWxMaximaで展開と因数分解した画像です。

WxMaxima

こちらはGUIベースなので多少見やすいと思います。

終了はメニューからできます。 Maximaのようにquitコマンドを使うと、GUIの背後で動いているmaximaが終了してしまうので注意してください。

まとめ

Maximaは豊富な機能を持っているので、興味のある方はウェブサイトを調べてみてください。 自分は展開や因数分解のチェックで使うことが多いです。 私は整式の手計算のミスが多いので、助けられています。

数式処理ソフトでは、一時期Mathematicaがウェブで語られていましたが、こちらは有料です。 現時点でMathematicaウェブサイトを調べたところ、デスクトップ版を家庭・趣味用で購入すると387ドル(145.97円×387=56490.39円)です。 円安なので高くつきますね。 他に教育用などの別のライセンスがありますから、購入を検討する人は調べてみてください。

Maximaは無料で使うことができるので、手軽だと思います。

WindowsにImageMagickとRMagickをインストール

Windowsで画像処理を行うため、ImageMagickをインストールします

ImageMagickのインストール

  • ImageMagickのウェブサイトをブラウザで開く
  • タイトルバーのダウンロードボタンをクリック(他に広告のダウンロードボタンもあるが、それは違うので要注意)
  • 下の方にWindowsインストーラがある。「ImageMagick-7.1.1-15-Q16-HDRI-x64-dll.exe」をクリックしてダウンロード。 PCの能力に応じて別のインストーラを選択してもよい
  • インストーラの指示に従ってインストール。 途中で「Install development headers and libraries for C and C++」にチェックを入れる。 これはRMagickを後にインストールするときに必要なため

テスト

  • 「ドキュメント」フォルダを開き、右クリックから「ターミナルで開く」をクリック
  • 「magick logo: logo.gif」と入力。ドキュメント・フォルダにlogo.gifというファイルができる。
  • フォルダ画面からlogo.gifをダブルクリック。ImageMagickのロゴが表示される

以上が確認できれば正常にインストールできています。

RMagickのインストール

RMagickはRubyのGem(ライブラリのこと)で、RubyImageMagickを使うためのものです。 使い方はオリジナルのImageMagickとは違い、Rubyプログラムに適した形になっています。 詳細はRMagickのドキュメントを参照してください。

ターミナルからgemコマンドでインストールします。

> gem install rmagick
Temporarily enhancing PATH for MSYS/MINGW...
Building native extensions. This could take a while...
Successfully installed rmagick-5.3.0
Parsing documentation for rmagick-5.3.0
Installing ri documentation for rmagick-5.3.0
Done installing documentation for rmagick after 2 seconds
1 gem installed
>

RMagickのテスト

エディタで次のプログラムを作成し、test.rbのファイル名で保存します。 保存先のディレクトリはどこでも良いのですが、一応ドキュメント(Documents)にしておきましょう。

require 'rmagick'
include Magick

img = Image.new(800,600) do |options|
    options.background_color = 'blue'
end
img.write("blue.png")

端末から実行します。 まず、カレント・ディレクトリがドキュメントになっていることを確認しておいてください。

> ruby test.rb
>

同じフォルダにblue.pngができているはずです。 それは800x600サイズで全面青色の画像ファイルです。 それができていれば、RMagickはきちんと動作しています。

RMagickの応用

RMagickを使ってプロジェクターの画面解像度に写真サイズを合わせるプログラムが、 「プロジェクター用スライドの画像について」にありますので、参考にしてください。

この他にもいろいろなことができるので、RMagickのドキュメントを参考に試してみると良いと思います。