おもこん

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

RubyGemsに登録してあるマイGem紹介

RubyGemsRubyのライブラリで、誰でも登録し、ダウンロードすることができます。 私は2つのGemを登録してあるので、ここで紹介したいと思います。 興味を持った方はぜひダウンロードして使ってみてください。 前提として、Rubyがインストールされていなければなりません。 OSは問いません。WindowsでもLinuxでも動きます(Macは手元にないので試していませんが、動くと思います)。

電卓プログラム

まず、s_calcというGemです。 これは関数電卓(scientific calculator)です。 インストールはgemコマンドで行います。

$ gem install s_calc

これには、2つの使い方があり、ひとつはコマンドとして、もうひとつはRubyのライブラリとしてです。 まず、コマンドとしてはcalcという名前で使います。

$ calc
calc > 1+2*3
7
calc >q
$

こんな感じです。 三角関数や、指数対数も使えます。 また、前回計算結果をvという変数に入れています。

$ calc
calc > 1/2
0.5
calc > v+3
3.5
calc >q
$

変数も使えます。変数はアルファベットのみからなります。

$ calc
calc > abc=100
100
calc > abc*3
300
calc >q
$

Rubyのライブラリとして使うときは

  • require "calc"として取り込む
  • c = Calc.newなどとして、Calcクラスのインスタンスを生成する
  • c.run("1+2*3")のように、計算式を引数にしてrunメソッドをコールすると、答えが返される

なお、GitHubソースコードとドキュメントがあります。

LaTeXのソースファイル管理

LaTeXで文書(とくに大きな文書)を作るための環境を整えるプログラムです。 インストールは

$ gem install lbt

でできます。

主な使い方は

  • lbt new ディレクトリ名で作られるディレクトリ下にソースファイルを書く
  • ソースファイルはLaTeX形式でもMarkdown形式でも良い。Markdownは拡張子.mdとする。これはPandocでLaTeX形式に自動変換される
  • sec1.texあるいはsec1.mdなどのファイル名でソースファイルを作る
  • lbt buildコンパイルをすると、PDFファイルができあがる

詳しくはGItHubにあるドキュメントを見てください。 自分の実感としては、ドキュメント作成が手軽になったと思います。

ファイル名に使える文字

ファイル名に使える文字はOSによって違う

Linuxでファイルを作るときにはほとんどの文字がファイル名に使えます。 厳密にいえば、OSというよりもOSの採用しているファイルシステムに依存します。 UBUNTU 23.04ではファイルシステムext4です。 ext4ではスラッシュとヌル以外をファイル名に用いることができます(ウィキペディア参照)。

これに対して、WIndowsはファイル名に使える文字が少ないのです。 次の文字はファイル名に使えません。

  • /
  • \
  • :
  • *
  • "
  • ?
  • <
  • >
  • |
  • NULL

また、WIndowsではLinuxと違い、大文字小文字を区別しません。

(注)

私はほとんどWIndowsを使っていないので、詳しくはないのですが、WSL-2というLinuxファイルシステムWIndowsでマウントする仕組みがあるそうです。 それを使うと上記のような非互換を避けることができるようです。 ただし、このことはNTFSやFATでフォーマットされたHDDやUSBメモリが上記の禁止文字を許すという意味ではありません。

USBメモリのフォーマット

現在、市販のUSBメモリのフォーマットはFAT32が多いといわれています。 また、HDDの場合はFAT32またはNTFSが多いようです。 いずれにしても、これらの製品をそのままLinuxで使うときはファイル名などの注意が必要です。

最も良いのは、上記の禁止文字をファイル名に使わないことです。 そうすれば、無用のトラブルを避けることができます。

仮に禁止文字の入っているファイル名のファイルがLinuxファイルシステムにあったとしましょう。 例えばabc?.txtのように禁止文字?が入ったファイル名のファイルです。 これをFAT32USBメモリにコピーしようとすると、ファイル名が不適当だというエラーがでてコピーできません。 ファイル名を適切に変更してコピーすればコピーは可能です。

fchangeプログラム

以上の問題を解決するために、Linuxファイルシステム上のファイルに対して、(WIndowsの)禁止文字を含むファイル名を変更するプログラムを作ってみました。 fchangeという名前のRubyプログラムです。 先程の禁止文字(ヌルを除く)をすべてアンダースコア(_)に変更します。 例えば

abc?.txt => abc_.txt

プログラムはGitHubにアップロードしてあります。

用途が限られたプログラムですが、HDDにバックアップをとるときなどは便利だと思います。

HDDやUSBのファイルフォーマットを変更する。

HDDやUSBの使い始めのときに、ext4ファイルシステムにフォーマットしておくと上記のような苦労がありません。 その代わり、そのディスクをWIndowsでも使うとなると、単純にはext4を読むことができません。 Windowsで読み込むにはWSL-2を使えば良い(らしい)です。 上記のMicrosoftの「WSL-2でLinuxディスクのマウントを開始する」を参照してください。

もしも、あなたがWindowsを使わずLinuxオンリーならば、ext4でフォーマットするのがベストです。

結論

最も良いのは、HDDやUSBはWindowsのフォーマットで、Linuxシステム上のファイルでも禁止文字を使わないことです。 自分もそうしていたつもりだったのですが、バックアップをとる段階で禁止文字をファイルに使っていたことに気づきました。

もしも読者に同じことが起こったら、fchangeがひとつの解決法を提供します(ただし、問題が発生しても責任はとれないので自己責任でお願いします)。

単項マイナスと構文解析

追記 2023/10/16 Calcのことで訂正があります。

単項マイナスとは

単項マイナスとは、式のなかに使われるマイナスで、その数字の符号を変えるためのものです。 例えば「−2」は2の符号をマイナスに変更する演算です。 「ー」には「引き算」を表す場合もありますが、単項マイナスの演算はそれではありません。 例をあげてみましょう。

  • 「ー2」・・・単項マイナス
  • 「3−2」・・・引き算のマイナスで、単項マイナスではない

単項マイナスと引き算のマイナスは異なる演算子ですから、違う文字を割り当てれば混乱しないのですが、習慣上同じ文字を使うことになってしまいました。

単項マイナスと括弧

単項マイナスには括弧をつけるという数式記述上の習慣があります。

2+(-3)
2-(-3)
2*(-3)+6/(-3)

これは括弧なしで「2+−3」と書いたときに、それが「2+(−3)」とは分かりにくいという、人間の感覚上の問題があるからです。 実は、単項マイナスの優先順位を加減乗除よりも高くしておけば、曖昧にはなりません。

2+-3=2+(-3)=-1
2--3=2-(-3)=2+3=5
2*-3+6/-3=2*(-3)+6/(-3)=(-6)+(-2)=-8

人間と違って、コンピュータは見た目でなく、論理的に式を解析しますから、括弧なしでもきちんと式を解釈してくれます。 我々人間にとって、これは慣れの問題だと思うので、括弧なしの単項マイナスOKと決めてしまっても大丈夫なのではないでしょうか。

なお、数式の記述において単項マイナスの括弧を省略できるケースがあります。 それは数式の最初に単項マイナスがくる場合です。 例えば「−2*3+4」という式を考えてみましょう。 乗算と加算では乗算が優先するものとします。つまり、掛け算「*」は足し算「+」よりも前に計算します。 この式は2通りに解釈できますが、どちらも同じ結果になります。

-2*3+4 = (-2)*3+4 = -6+4 = -2
-2*3+4 = -(2*3)+4 = -6+4 = -2

このように解釈が分かれても結果が同じになるので、括弧を省略して良いわけです。 これは3項を足す場合に括弧がなくて良いのと同じ理由です。

1+2+3 = (1+2)+3 = 3+3 = 6
1+2+3 = 1+(2+3) = 1+5 = 6

このように加算は、左結合または右結合のいずれをとっても結果が同じになり、そのことから括弧を無しで書くことが許されます。

括弧なし単項マイナスを許容する場合のBNF

括弧なし単項マイナスを許容する式を構文解析するためのBNFを考えてみましょう。 なお、BNF構文解析については前回の記事を参照してください。

演算子の優先順位を定義しておけば、極めて単純なBNFになります。 以下のファイルをsample3.yとして保存します。

class Sample3
  token NUMBER
  prechigh
    nonassoc UMINUS
    left '*' '/'
    left '+' '-'
  preclow
  options no_result_var
rule
  expression  : expression '+' expression { val[0] + val[2] }
              | expression '-' expression { val[0] - val[2] }
              | expression '*' expression { val[0] * val[2] }
              | expression '/' expression { if val[2] != 0 then val[0] / val[2] else raise "Division by zero." end }
              | '-' expression =UMINUS { -val[1] }
              | '(' expression ')' { val[1] }
              | NUMBER
end

---- header

---- inner

def lex(s)
  @tokens = []
  until s.empty?
    case s[0]
    when /[+\-*\/()]/
      @tokens << [s[0], s[0]]
      s = s[1..-1] unless s.empty?
    when /[0-9]/
      m = /\A[0-9]+/.match(s)
      @tokens << [:NUMBER, m[0].to_f]
      s = m.post_match
    else
      raise "Unexpected character."
    end
  end
end

def next_token
  @tokens.shift
end

---- footer

sample = Sample3.new
print "Type q to quit.\n"
while true
  print '> '
  $stdout.flush
  str = $stdin.gets.strip
  break if /q/i =~ str
  begin
    sample.lex(str)
    print "#{sample.do_parse}\n"
  rescue => evar
    m = evar.message
    m = m[0] == "\n" ? m[1..-1] : m
    print "#{m}\n"
  end
end

Raccを使ってコンパイルします。

$ racc sample3.y

すると、sample3.tab.rbというRubyのソースファイルができます。 これは、BNFに沿って計算するRubyプログラムです。 実行してみましょう。

$ ruby sample3.tab.rb
Type q to quit.
> 1+-2
-1.0
> 1--2
3.0
> 2*-3
-6.0
> -2*-3
6.0
> q
$

単項マイナスは高優先順位の演算子なので、乗除の計算に先立って計算しています。 括弧のない「1−−2」のような式は、違和感がありますが、文法的には曖昧さなしに定義できているわけです。

calcの場合

追記 2023/10/16

GitHubのCalcにバグがあり、修正しました。 それに伴い以前ここに書いた記事にもあやまりがありましたので、訂正して下記のように直しました。

GitHubに登録してあるCalcの場合も単項マイナスが他の演算子に優先するようになっています。

このBNFの中から、四則と単項マイナスの部分だけを取り出して、若干の手直しを加えると次のようになります。

  expression  : expression '+' expression { val[0] + val[2] }
              | expression '-' expression { val[0] - val[2] }
              | expression '*' expression { val[0] * val[2] }
              | expression '/' expression { if (val[2] != 0.0) then val[0] / val[2] else raise("Division by zero.") end }
              | '-' primary  =UMINUS { -(val[1]) }
              | primary
primary     : '(' expression ')' { val[1] }
              | NUM

BNFの完全な形については上記のリンクをたどり、racc/calc.yを参照してください。

徒然Ruby(44)Raccパーサ・ジェネレータ

パーサ・ジェネレータとは

パーサを日本語で「構文解析器」といいます。 「器」とといっても、「うつわ」ではなく「器械」、より適切にはアプリケーションまたはプログラムのことです。 つまり、構文解析をするプログラムです。

構文解析」とは、「プログラム言語などの構文」を「解析」することです。 簡単な例として、足し算を考えてみましょう。

1+2
11+34
123+456

3つの足し算の例があります。 出てくる数字は違うものですが、どれも「数字+数字」というパターンは変わりません。 3番めの文字列は7つの文字からなっています。

1, 2, 3, +, 4, 5, 6

この7文字はバラバラではなく、最初の3つがひとまとまりで、「123」という数字を表しており、その後「+」という演算記号、「456」という数字が続きます。 そこで、この7文字をタイプと値のセットで、それぞれ次のようにまとめましょう。

数字, 123
+, 何でも良い
数字, 456

数字には値が必要ですが、演算記号には値がありませんから、2番めの「値」のところは何でも良いのです。 ここでは、同じ演算記号の文字列"+"にしておきましょう。 また、「数字」というタイプは、Numberの最初の3文字をとって、シンボル:NUMで表すことにします。 すると、「123+456」という7文字の文字列は

[[:NUM, 123], ["+", "+"], [:NUM, 456]]

という配列にすることができます。 これを字句解析といいます。 配列の要素の[:NUM,123]を字句またはトークンといいます。 トークンのタイプを表す:NUMなどをタイプとかトークン・カインドといいます。 配列の2番めの要素「123」は値とか、意味上の値(semantic value)などといいます。 字句解析は、構文解析には含まれないというのが一般的な考え方です。

それでは、この構文はどういうものでしょうか。 さきほど述べたように、どの式も「数字+数字」の形です。 そこで、この式(expression)は

expression : NUM '+' NUM

と表すことができます。 式(expression)とは数字(NUM)の次にプラス('+')がきて、その次にまた数字(NUM)がくるというトークンの列のことである、ということです。 この記法をBNFバッカス・ナウア・フォーム)といいます。

この構文はRaccが理解できる形にしています。

  • NUMにはコロンをつけない(:NUMとはしない)
  • +はシングルクォートで囲む。

足し算のトークン・カインドを新たに決めることもできますが(例えばPLUSとして、字句解析では[:PLUS, "+"]とする)、演算子はそれ自体をトークン・カインドにすることが多いです。 そのことによって、トークン・カインドの種類が多くなるのを防ぎ、プログラマーの負担を軽くする狙いがあります。 演算子をそのままトークン・カインドにするときは、

  • BNFの中ではシングルクォートで囲む
  • 字句解析のトークン・カインドはシンボルではなく文字列を使う(:+ではなく'+'にする)

とします。シンボルでないことには注意してください。

このように式(expression)を定義すると、最初の3つの足し算はすべてこのBNFに沿っていることがわかります。 このBNFで表されたものを、文法(grammar)といいます。

Rubyなどのプログラム言語はもっと複雑な文法を持っていますが、その文法に入力(ソースファイル)があっているかどうかは確認ができます。 あっていなければ「Syntax Error」(文法上のエラー)となります。

例えば次のような式はさきほどの足し算の文法からはSyntax Errorになります。

1+2+3
10-6
-20

この3つはエラーが発見される時点が違います。 最初から順に見ていって、エラーになるところを●で表すと

1+2+●3
10-●6
-●20

黒丸の左側の要素を見た時シンタックスエラーが確定します。

少し複雑な文法

それでは、我々が普段使う足し算、引き算の計算をBNFにしてみましょう。

この計算では、数字、+、ー、(、)の5つのトークンが必要です。 具体例を見てみましょう。

1-2+3
(1-2)+3
1-(2+3)

最初の例は後回しにして、2番めを見てみます。 これは、はじめに1と2の2つの数字を引き算し、その結果と3を加える、というもので、木構造(ツリー)で表現すると

(1-2)+3

となります。 3番めは同様に、

1-(2+3)

です。

1行目の「1-2+3」には括弧がないので、上の図のどちらのツリーになるか、曖昧さが残ります。 通常は頭から計算するので、1-2を先に計算するのですが、構文解析上はそれを明示する必要があります。 その方法については後ほど述べることにします。

この例では1回の計算の結果が次の式の中で使われています。 つまり、「数字+数字」ではなく「式+式」のような場合があるということです。 そこで、BNFを少し変えてみます。

expression : expression '+' expression | expression '-' expression

縦棒(|)は「または」という意味で、その左側のパターンでも右側のパターンでも良いということです。 このBNFでは、expressionをexpressionで定義しているので、これだけでは構文を表したことになりません。 前は式を「数字+数字」としていました。 少なくとも数字が式の定義に出てこなければいけないでしょう。 そこで、次のように変えてみます。

expression : expression '+' expression
           | expression '-' expression
           | NUM

このように、定義は2行以上に渡っても良いことにします。 式は「式+式」または「式ー式」または「数字」のいずれかに一致すれば良い、ということです。 これならば、「1-2+3」は式であることがわかります。

  • 1」は「数字」したがって「式」でもある
  • 1-2」は「式ー数字」だが、数字は式でもあるので、「式ー式」になる。これはBNFの2行目のパターンなので式とみなせる。
  • 1-2+3」は「1-2」が式なので、「式+3」すなわち「式+数字」だが、数字は式だから「式+式」となり、BNFの1行目のパターンに一致するから式である。

しかし、実は違う方法で式であることも説明できます。

  • 1-2+3」は「2+3」が「数字+数字」、すなわち「式+式」だから、式となる
  • 1-2+3」は「2+3」が式なので、「数字ー式」となり、数字は式だから「式ー式」となり、BNFの2行目のパターンに一致するから式である。

1−2+3が式であることはBNFで確認できるが、その方法は少なくとも2通りあるということです。 しかも、その計算結果は異なり、それぞれ2と−4になります。 ですから、BNFの解釈が2通りというのはとてもまずいことなのです。 また、この2通りは、さきほど木構造で表した2通りと同じことだとわかります。 ですから、解釈が1通りになるには括弧を加える、というのはひとつの方法です。

expression : expression '+' expression
           | expression '-' expression
           | '(' expression ')'
           | NUM

これだと、「(1−2)+3」や「1−(2+3)」が文法的に合うようになります。 解釈が2通り出てしまう「1−2+3」は防げませんが、そういうものは

  • シンタックス・エラーだということにする
  • 解釈が2通りでたら「左側の演算から優先的に行う」という条件をつけて1通りに強制する

のどちらかを採用すればよいのです。

また、次のようにBNFを変更すると、常に計算は左から行われるようになります。

expression : expression '+' primary
           | expression '-' primary
           | primary
primary    : '(' expression ')'
           | NUM

「1−2+3」がこれでどのように解釈されるか考えてみましょう。 なお、primaryは日本語で一次因子とかプライマリーと呼ばれます。

  • 「1−2」は「数字ー数字」で、数字=>プライマリー=>式だから、「式ープライマリー」なので式になる
  • 「1−2+3」は「式+3」すなわち「式+数字」になり、それは「式+プライマリー」だから「式」になる

2+3を先に計算する方は上記のBNFには合いません。

  • 「2+3」は「数字+数字」=>「式+プライマリー」になるので「式」になる
  • 「1−2+3」は「数字ー式」=>「式ー式」になるが、「式ープライマリー」ではない。したがって、式にはならない

このBNFは計算の第二項が「式」ではなく「プライマリー」であるとして、プライマリーが「括弧で囲まれた式」か「数字」のみということで、括弧なしで右から計算することを取り除いています。

以上をまとめると、計算の順序の曖昧さを防ぐ方法は2つあり、

  • BNFで示す
  • 演算子+とーが2つあった場合、左から計算すると定義しておく

ということになります。

四則(加減乗除)計算のBNF

今回の最後に四則計算の文法定義のBNFを作ってみましょう。 足し算引き算のところで、演算子が2つあったとき、左から計算するか、右から計算するかの2通りの解釈が生まれる場合がありました。 似たものに、乗除と加減の優先順位の問題があります。

1+2*3

ここでは、Rubyと同じように掛け算はアスタリスク*)で表します。 この計算では、「2かける3」を先にやることになっています。 ですから、

1+2*3 = 1+6 = 7

となります。 これは小学校で習いますね。 ところが、コンピュータはそういうことを習っていませんから、BNFなどで、演算の優先順位を定義しなければなりません。 ところで、「1+6」の1や6を「項(term)」と呼びます。 また、「2*3」の2や3を「因子(factor)」といいます。 BNFだけで演算の優先順位を表現するには式、項、因子を分けて定義します。

expression  : expression '+' term
            | expression '-' term
            | term
term        : term '*' factor
            | term '/' factor
            | factor
factor      : '(' expression ')'
            | NUMBER

このBNFは「2+3+4」のような括弧のない式は左から順に計算するようになっており、曖昧さが発生しません。 このBNFでは、単項マイナス、すなわち因子の符号を変えるマイナス(例えば「−2」のようなマイナス)はサポートしていません。

Racc で実装

このBNFをRaccのソースファイルに取り入れて、コンパイルしてみましょう。 Raccのソースファイルは拡張子.yにするのが習慣です。 大きく4つのパートに分かれます。

  • クラスの定義、BNFの記述部分。この部分はRaccによってRubyプログラムにコンバートされる
  • (header)クラス定義の前にコピーされるRubyプログラム
  • (inner)クラスの中のコンバートされる部分の前に挿入されるRubyプログラム
  • (footer)クラス定義のあとにコピーされるRubyプログラム。メインプログラムを書く場合はここに書く

クラス定義、BNFの記述部分

ここがRaccのソースプログラムのメインです。 スーパークラスは書かないようにします(class A < BのBの部分は書かない)。

  • token の後にトークンを宣言しておく。これは無くても良いが、トークンの綴間違いなどをチェックできる。例えばtoken NUMのようにする
  • optionsの後にオプションを指定できる。options no_result_varが良く用いられる。その意味はドキュメントを参照
  • rule以降(classのendまで)にBNFを記述する

具体例を次に示します。

class Sample1
  token NUMBER
  options no_result_var
rule
  expression  : expression '+' term { val[0] + val[2] }
              | expression '-' term { val[0] - val[2] }
              | term
  term        : term '*' factor { val[0] * val[2] }
              | term '/' factor { if val[2] != 0 then val[0] / val[2] else raise "Division by zero." end }
              | factor
  factor      : '(' expression ')' { val[1] }
              | NUMBER
end

BNF加減乗除をサポートする以前示したものと同じですが、その後ろに波括弧で囲まれた部分があります。 これはアクションと呼ばれるものです。 アクションの左側のBNFに一致したときに、そのアクションが実行されます。 配列valは左のBNFの要素に対応するアクションが実行されたときの結果です。 例えば

  factor      : '(' expression ')' { val[1] }
              | NUMBER

この2行目にはアクションがありませんが、その場合のアクションは{ val[0] }、すなわち最初のBNFの要素のアクションの結果を返す、ということになっています。 NUMBERは字句解析で返された数字に一致しますので、その値が返されます。 1行目は括弧で囲まれた式です。このBNFには、左括弧、式、右括弧の3つの要素があります。val[1]は2番めの要素である式のアクションの結果です。 したがって、この括弧で囲まれたfactorの値は、括弧で囲まれた式の値になります。

このようにして、それぞれの式に一致したときに、その計算がアクションとして行われます。

なお、定義するクラスをモジュールの中に入れたいときは

class MName::Sample1

のようにします。MNameはクラスを入れるモジュールの名前です。 Raccでコンパイルすると

module MName
  class Sample1
... ... ...
  end
end

このようになります。

ヘッダー、インナー、フッター

ヘッダーに良く書かれるのはrequire文です。 このサンプルプログラムでは特に記述するものはありません。

インナーには字句解析関係のプログラムを書きます。 また、Raccで生成したパーサが字句を一つずつ読み込むためのメソッドはnext_tokenというメソッド名に決まっています。 それを実装します。具体例を示します。

def lex(s)
  @tokens = []
  until s.empty?
    case s[0]
    when /[+\-*\/()]/
      @tokens << [s[0], s[0]]
      s = s[1..-1] unless s.empty?
    when /[0-9]/
      m = /\A[0-9]+/.match(s)
      @tokens << [:NUMBER, m[0].to_f]
      s = m.post_match
    else
      raise "Unexpected character."
    end
  end
end

def next_token
  @tokens.shift
end

メソッドlexが引数の文字列を字句解析するプログラムです。 ここでは、StrScanライブラリを使っていませんが、大きなプログラムでは高速な字句解析のためにStrScanを用いるほうが良いです。 その場合は、headerのところにstrscanライブラリをrequireしてください。 演算子は、その文字列をタイプと値にして@tokensにプッシュします。 数字(0以上の整数)はタイプを:NUMにしてその数字(Floatオブジェクトに直したもの)をプッシュします。 IntegerでなくFloatにしたのは、割り算で小数点以下まで出したかったからです。 Integerオブジェクトにしても良いのですが、その場合は割り算で小数点以下は切り捨てられます。 それ以外の文字は例外を発生させます。

メソッドnext_tokenは字句解析の配列からひとつづつ取り出して返します。

フッターの部分にはメインプログラムを書きます。 メインプログラムから、構文解析をするにはdo_parseメソッドをコールします。 そのコールの前にはlexメソッドによる字句解析は終了していなければなりません。 以下の例では一行入力しては、その計算をして表示します。

sample = Sample1.new
print "Type q to quit.\n"
while true
  print '> '
  $stdout.flush
  str = $stdin.gets.strip
  break if /q/i =~ str
  begin
    sample.lex(str)
    print "#{sample.do_parse}\n"
  rescue => evar
    m = evar.message
    m = m[0] == "\n" ? m[1..-1] : m
    print "#{m}\n"
  end
end

特に難しいことないと思います。 ソースプログラム(sampe1.y)の全体も以下に示しておきます。

class Sample1
  token NUMBER
  options no_result_var
rule
  expression  : expression '+' term { val[0] + val[2] }
              | expression '-' term { val[0] - val[2] }
              | term
  term        : term '*' factor { val[0] * val[2] }
              | term '/' factor { if val[2] != 0 then val[0] / val[2] else raise "Division by zero." end }
              | factor
  factor      : '(' expression ')' { val[1] }
              | NUMBER
end

---- header

---- inner

def lex(s)
  @tokens = []
  until s.empty?
    case s[0]
    when /[+\-*\/()]/
      @tokens << [s[0], s[0]]
      s = s[1..-1] unless s.empty?
    when /[0-9]/
      m = /\A[0-9]+/.match(s)
      @tokens << [:NUMBER, m[0].to_f]
      s = m.post_match
    else
      raise "Unexpected character."
    end
  end
end

def next_token
  @tokens.shift
end

---- footer

sample = Sample1.new
print "Type q to quit.\n"
while true
  print '> '
  $stdout.flush
  str = $stdin.gets.strip
  break if /q/i =~ str
  begin
    sample.lex(str)
    print "#{sample.do_parse}\n"
  rescue => evar
    m = evar.message
    m = m[0] == "\n" ? m[1..-1] : m
    print "#{m}\n"
  end
end

コンパイルと実行

コンパイルは「racc ソースファイル名」で行います。

$ racc sample1.y
$ ls
sample1.y sample1.tab.rb

sample1.tab.rbがRaccで生成されたRubyプログラムです。 このファイルをエディタで見ると、BNFが様々な配列とメソッドに変換されているのがわかります。 実行してみましょう。

$ ruby sample1.tab.rb
Type q to quit.
> 1-2-5
-6.0
> 2*3+4*5
26.0
> (10+25)/(3+4)
5.0
> q

計算は正しく行われていますね。

演算子の優先順位と結合における左右の優先順位

BNFにあいまいさがあるときに、その解決方法として

  • BNFをあいまいさの残らない形に変更する
  • 演算の優先順位を指定する

の2つが考えられます。 後者には2つあり、例えば

  • 乗除は加減より優先的に計算する。例えば「1+2*3」では「1+2」よりも「2*3」を先に計算する。 このように演算子の間に優先順位をつけることにより、曖昧さを解決できることがあります。 四則では、
    • 乗除の優先順位は同じ。つまり「1*2/3」は「1*2」と「2/3」のどちらを先にするかの取り決めはない。この順については別に定めが必要
    • 加減の優先順位は同じ。
    • 乗除は加減に対して優先順位が高い
  • 同レベルの演算があったときに、左から先に計算する(左結合)または右からさきに計算する(右結合)を定める。 通常は加減乗除とも同レベルの演算は左から先に計算する。 右結合は少ないが、無いわけではない。たとえば累乗は右結合である。累乗を「^」で表すと「2^3^4」は「2^81」のことになる。

Raccでは、これらの優先順位を指定することができます。 優先順位をprecedenceといい、Raccはこの最初の4文字を用いて

  • prechigh: 優先順位が高い
  • preclow: 優先順位が低い

この2つ間に演算子を書き、同じ行には同じ優先順位、上の方が優先順位が高い、とします。

また、左結合はleft、右結合はrightで指定します。 以上のルールを用いると、BNFが単純でもパーサを生成することができます。

Raccのソースファイルの最初の部分を書き換えてみましょう。 なお、footerのインスタンス生成時のクラス名はSample1からSample2に変更が必要です。

class Sample2
  token NUMBER
  prechigh
    left '*' '/'
    left '+' '-'
  preclow
  options no_result_var
rule
  expression  : expression '+' expression { val[0] + val[2] }
              | expression '-' expression { val[0] - val[2] }
              | expression '*' expression { val[0] * val[2] }
              | expression '/' expression { if val[2] != 0 then val[0] / val[2] else raise "Division by zero." end }
              | '(' expression ')' { val[1] }
              | NUMBER
end

3行目から6行目が計算順を示す部分です。 このおかげで、BNFは短く、すっきりしたものになりました。

まとめ

この電卓プログラムのソースコードはわずか57行です。 これをRubyで書いたら、そんな短いコードではすみません。

Raccは構文解析が必要なプログラム、例えばインタープリタコンパイラ、電卓などを書くときの大きな手助けになります。 言語はCで書かれることが多いですが、Cの場合はパーサ・ジェネレータのBisonを使うことが多いです。 BisonもRaccのようなプログラムです。

Raccを使った電卓プログラムをGitHubにあげてありますので、参考に見てください。

また、Raccのドキュメントはこちらをご覧ください。

徒然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について投稿予定です。

電卓プログラムに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になる分数の和で表す問題、拡張テンパズルも入っています。 これらは以前このブログでとりあげたので、そちらも参考になると思います。