単項マイナスと構文解析
単項マイナスとは 単項マイナスと括弧 括弧なし単項マイナスを許容する場合のBNF calcの場合
アプリ作成の記事でminitestを使いました。 今回はminitestについて、また一般にテストについて、私の考えを書こうと思います。
一般にテストはプログラムのバグを除くために広範に使われている手法です。 開発に従事している方は既に良くご存知のことと思います。 私は「仕事として」つまり給料をもらって開発をしたことがありません。 おそらくプロの開発の方は、私よりもっとテストの経験も知識もお持ちだと思います。 そういう方は、この記事を読む必要はないかもしれません。
また、テストには「テスト駆動開発」とか「振る舞い駆動開発」などというのもあって、ひとつの開発手法にもなっています。
そのような手法では、まずテストを書いてからアプリを書く、といわれます。 ですが、私はどちらが先でも良いのではないかと思っています。
テストを書くときは、あまりプログラムの細かい部分に立ち入るよりも、その振る舞い(behavior)が期待通りかをテストするようにします。 そうすることで、そのプログラムの内部構造を変更、改良したのちも同じテストプログラムでテストすることができます。 つまり、テストは内部構造まで立ち入らず、振る舞いのみにフォーカスすべきです。
また、テストではその規模も問題になります。
この記事ではユニットテストを扱います。
minitestはテスト用のフレームワークですが、それを使わずに自分でテストプログラムを作ることもできます。
例えば、あるメソッドが数字を表す文字列を引数に取り、その数値を返す、というものだとしましょう。
メソッド名をto_number
としておきましょう。
今回は先にto_number
を作ってから、そのテストプログラムを作ることにします。
def to_number(s)
s.to_i
end
引数の文字列をto_i
メソッドで整数にして返す、というメソッドです。
おそらく、皆さんはすぐにこのプログラムの欠点を見つけたことでしょう。
問題ありありのプログラムですが、テストの説明には適しています。
テストプログラムを考える上でのポイントは「メソッドの振る舞い」をテストするということです。 この場合、「引数が与えられたら、その引数の表す数値を返す」というのが振る舞いですが、もう少し詳しく考えないといけません。 引数と返し値の組み合わせを「引数=>返し値」で表すことにします。
??と書いたところは仕様が決められていなかったので、そう書きました。 テストを考えただけで、仕様の不十分さも分かりました。 これもテストを導入するメリットの1つです。 つまり、振る舞いに対するテストを書くためには、振る舞いをより完全に定義しなければならなくなります。
今回は??のところはnilを返して、エラー処理は呼び出し側に任せることにしましょう。
テストはこんな感じです。
def to_number(s)
s.to_i
end
# テストプログラム
def test_to_number
a = to_number("0")
print "0 にならずに #{a} が返された\n" unless a == 0
a = to_number("-0.1")
print "-0.1にならずに #{a} が返された\n" unless a == -0.1
a = to_number("1.23e10")
print "1.23e10にならずに #{a} が返された\n" unless a == 1.23e10
a = to_number("abc")
print "nilにならずに #{a} が返された\n" unless a == nil
a = to_number(100)
print "nilにならずに #{a} が返された\n" unless a == nil
end
# 実行してみると
# $ test_to_number
#=> -0.1にならずに 0 が返された
#=> 1.23e10にならずに 1 が返された
#=> nilにならずに 0 が返された
#=> nilにならずに 100 が返された
最初のテストだけ通って、残りの4つは通りませんでした。 テストを通ることを「成功(サクセス)」通らないことを「失敗(フェイル)」といいます。 またテストを通ることを「パスする」ともいいます。
to_number
は引数について場合分けして、それぞれに対して適切な値を返さなければなりません。
メソッドto_number
を書き直してみましょう。
def to_number(s)
return nil unless s.is_a?(String)
case s
when /\A-?\d+\Z/
s.to_i
when /\A-?\d+\.\d+([Ee]\d+)?\Z/
s.to_f
else
nil
end
end
正規表現を使って、文字列が数字にマッチする場合に数値に変換しています。 結構複雑なことをしなければならないのですね。 これでテストは通るようになります。
ちょっと脇道にそれますが、このように文字列が数値を表すかどうかをチェックするのは一種の字句解析に当たります。 rubyはプログラムを読む時に「字句解析」をして、その文字列をキーワード、識別子、区切り記号、数値などに分解したのちに構文解析、実行に移ります。 そこで、rubyの字句解析を使って数値を取り出すこともひとつの方法として考えられます。 ripperという標準添付ライブラリはRuby プログラムを解析するためのライブラリです。 メソッドRipper.lexは字句解析をし、字句(トークン)の配列を返します。
require 'ripper'
p Ripper.lex("123 -1.2e5")
#=> [[[1, 0], :on_int, "123", END], [[1, 3], :on_sp, " ", END], [[1, 4], :on_op, "-", BEG], [[1, 5], :on_float, "1.2e5", END]]
配列の中身の詳細はRubyのドキュメントを参照してください。 注目すべきは、各配列の2番目のシンボルです。
これを利用すると、正規表現を使わずに「整数」「浮動小数点数」「その他」に文字列を分類できます。
to_number
をRipper.lexを使って書き直してみましょう。
require 'ripper'
# メソッドの再々定義
def to_number(s)
return nil unless s.is_a?(String)
a = Ripper.lex(s)
unless a.size == 1 || (a.size == 2 && a[0][1] == :on_op && a[0][2] == "-")
return nil
end
case a[-1][1]
when :on_int
s.to_i
when :on_float
s.to_f
else
nil
end
end
Ripperを使うことはあまりないでしょうが、Ruby同様の字句解析をしたいときはRipperが便利です。 本題とはあまり関係ないことを書きましたが、Ripperも面白いと思ったので、この機会に紹介しました。
to_numberメソッドをテストするminitestのプログラムを作ってみましょう。 次のプログラムはto_numberに対して8つのテストをします。 to_numberメソッドはこのプログラムの前に挿入しておくこととします。 (require_relativeメソッドで取り込んでも良いです)。
require 'minitest/autorun'
class TestToNumber < Minitest::Test
def setup
end
def teardown
end
def test_to_number
[["0",0],["12",12],["-5",-5],["12.34",12.34],["2.27421e5",2.27421e5],["-23.56",-23.56],["abc",nil], [123,nil]].each do |s,v|
if v == nil
assert_nil to_number(s)
else
assert_equal v, to_number(s)
end
end
end
end
要点を次に記します。
test_
をつける。
ここではto_number
メソッドのテストなのでtest_to_number
とした。
ここでは、2重配列を使い、その8つの要素に対してeachメソッドを用い、テストする。
配列のそれぞれの要素は["0",0]
のように、数字の文字列(to_number
の引数)と数字(to_number
が返すオブジェクト)となっている。
最初の例では”0”を引数にしてメソッドを呼び出したとき、0が返されることが期待されている。
期待通りならテストは通過(パス)し、そうでなければ失敗(フェイル)となる。assert_equal nil, (テストで得られたオブジェクト)
」とすることもできるが、これは古い書き方で現在は推奨されていない。
assert_nil
が推奨される書き方になっている。
テスト通過の場合は何も表示されず、失敗のときは期待された(expected)オブジェクトと実際の(actual)オブジェクトが表示される。実行してみます。
$ ruby example23.rb
Run options: --seed 7828
# Running:
.
Finished in 0.005369s, 186.2654 runs/s, 1490.1234 assertions/s.
1 runs, 8 assertions, 0 failures, 0 errors, 0 skips
テストは成功したので、フェイルの表示が出ません。
ドットはテストするメソッド(この場合はtest_to_number
)の個数分だけ表示されます。
一番下の行の「8 assertions」は8つのassert_・・・
のテスト(アサーションと呼ぶ)が行われ、失敗0エラー0だったことを示しています。
なお、失敗は「期待した値と実行結果の値が異なること」、エラーは「プログラムのエラー(間違い)」で、違うものを表します。
このテストはトップレベルのメソッドを対象としました。
もしこのメソッドがトップレベルで定義されたインスタンス変数を使っているときは、その定義をsetupで行わなければなりません。
(トップレベルで行う定義をそっくりコピペすれば良い)。
トップレベルのインスタンス変数はmain(トップレベルのオブジェクト)のインスタンス変数になります。
これに対して、minitestのテストはMinitest::Test
クラスのサブクラスに定義されたメソッドで行われるので、そこで参照されるインスタンス変数は「そのサブクラスのインスタンスの変数」になります。
したがって、mainのインスタンス変数は参照できないことに注意してください。
これを解決するには、さきほど述べたように、setupメソッドでインスタンス変数を定義しておきます。
次のプログラムはテストするまでもない簡単なものですが、標準出力のテスト例として取り上げます。
@n = 0
def inc
@n += 1
p @n
end
inc
inc
inc
メソッドincはインスタンス変数@nを1つ大きくし、標準出力に出力します。 トップレベルでincを3回呼ぶと、
1
2
3
と、呼び出すごとに1つずつ大きくなって表示されます。
このテストは次のようになります。
class TestInc < Minitest::Test
def setup
@n=0
end
def teardown
end
def test_inc
assert_output("1\n") {inc}
assert_output("2\n") {inc}
assert_output("3\n") {inc}
end
end
大事なことはsetupでインスタンス変数@nの初期化をしなければなりません。
前項で述べたように、トップレベルのインスタンスがmainであるのに対し、test_inc
メソッドのインスタンスはTestIncクラスのインスタンスです。
したがって、トップレベルのインスタンス変数を参照することはできません。
setupでの@nの初期化は必須で、これがないとincの@n += 1
を実行するときに、@nが未定義で参照されるためにnilになり、nilには+メソッドがないのでエラーになります。
これはエラーであってフェイルではありません。
このテストではassert_output
メソッドを使っています。
引数が期待される結果、それをブロックの実行の標準出力と比較して異なるとフェイル・メッセージが表示されます。
自作のテストプログラムで標準出力をテストするには、出力を捕まえる(出力先を変更する)必要があり、大変ですが、minitestを使えばassert_output
を使うだけですみます。
minitestのアサーションは豊富です。 minitestのドキュメントに詳細がありますので、参考にして下さい。
単項マイナスとは 単項マイナスと括弧 括弧なし単項マイナスを許容する場合のBNF calcの場合
パーサ・ジェネレータとは 少し複雑な文法 四則(加減乗除)計算のBNF Racc で実装 クラス定義、BNFの記述部分 ヘッダー、インナー、フッター コンパイルと実行 演算子の優先順位と結合における左右の優先順位 まとめ
StrScanライブラリのドキュメント 字句解析とは StrScanライブラリ StrScanライブラリを使った字句解析 実例
lbtというgemを作って公開してみた lbtはどんなgemか ファイルの配置 lbt.gemspec Rakefile gemのビルド RubyGems.orgへのアップロード 補足・・rake/gempackagetaskサブライブラリについて
文字列のエンコーディングに頭を悩ませることはほとんどなくなりました。 なぜなら、どのアプリ、システムもUTF-8を使うようになったからです。 Rubyでもエンコーディングの問題が起こることはまず無いでしょう。 ですが、今回はエンコーディングの考え方を整理してみたいと思います。
Fiberを書いたときから、次はスレッドを書こうと思っていましたが、時間がかかってしまいました。 その理由は、期待したとおりのスレッドの効果がなかったためです。 今回はそのことを書きますが、これはRubyのスレッドの抱えている問題なのか、自分のやり方が悪いのかははっきりしていません。
Fiberは「ノンプリエンプティブな軽量スレッド」とRubyのマニュアルに記載されています。
今回はRubyプログラムから自動的にドキュメントを作成するRDocについて書きたいと思います。 私はこのことについて、エキスパートではありません。 この記事も、初心者の体験談だと考えてください。
Ruby/Gtkの記事を先日書いたときに、「これはかなり使える」という手応えを感じたので、WordBook(Railsで作った単語帳プログラム)のGTK 4版を作りました。 プログラムは「徒然なるままにRuby」のGitHubレポジトリに置いてあります。 レポジトリをダウンロードし、ディレクトリ_example/...
今回はGTK 3とGTK 4をRubyで使うライブラリについて書きたいと思います。
今回もRubyとGUIのトピックです。 Glimmerを取り上げます。
Rubyはグラフィックについて弱い印象があります。 しかし、グラフィックはデバイスに関することなので、言語そのものには直接の関係はないはずで、あるとすればライブラリです。 今後グラフィック関係のgemが開発されることに期待しましょう。
Rails7におけるシステムテストについて書きます。
前回作ったWordbook(リソースフル)のテストを書いてみます。 RailsのテストはminitestをRails用に拡張したものです。
今回はRailsの慣例に沿った形でWordbookを作り直します。
今回はWordBookの検索と削除についてです。
今回はRailsにおけるデータの作成と保存、そして変更について説明します。 そのベースになるモデルとデータベースの話から始め、appendとchangeの動作について詳しく説明します。
一般に、HTMLは文書の構造を表し、CSSはその体裁(見栄え)を表します。 Railsは最終的にCSSを含むHTML文書を出力するので、この2つについての理解が必須です。 この記事ではとくにCSSの人気ライブラリであるBootstrapを紹介します。 BootstrapはJavascriptも含んでいます。
Rubyの最も人気のあるアプリケーションであるRuby on Railsを取り上げようと思い、書き始めました。 予想してはいましたが、相当な分量になってしまいました。 そのため、何回かに分けて記事にすることにします。 また、対象となる読者のレベルをどうしようかと考えましたが、「徒然Ruby」が基礎的な内容から始ま...
Rubyのライブラリ管理システムのRubygemsとコマンドgemおよびbundlerについて説明します。
minitestについて連続して2回書いてきました。 「minitestはドキュメントが少ない」という人がいますが、私も同感です。 例えば、モックとスタブの説明も少ないです。 そこで、今回はmock.rbのソースコードを参考に、モックの私的ドキュメントを書いてみました。 あくまで私個人の考えであり、minites...
今回もminitestの話です。 mockとstubに焦点をあて説明します。
アプリ作成の記事でminitestを使いました。 今回はminitestについて、また一般にテストについて、私の考えを書こうと思います。
今回はメソッドの呼び出し制限ついて説明します。 呼び出し制限にはpublic、private、protectedの3つがあります。
今回は特異メソッド、特異クラス定義、名前空間、モジュール関数について説明します。
2023/10/29 追記:この記事は新しく書き直しました。 古い記事で使っていたGitHubのCalcが大幅にアップデートされたためです。 そこで、この記事に合うようなプログラムsimple_calcを新たに作りました。 このプログラムは本レポジトリの_example/simple_calcにあります。
if〜elsif〜・・・〜else〜endは皆さん良く使うでしょうか? これは場合分けで良く使われる方法です。 これと同様の制御構造にcase文があります。 Cのswitch文に似ていますが、より強力な機能を持っています。 if-else-endよりも高い能力があるといえます。
Procオブジェクトを生成するメソッドlambdaについて説明します。
今回はブロックを一般化したオブジェクトProcを説明します。
ブロック付きメソッドの作り方を説明します。
モジュールには名前空間とミックスイン(Mix-in)の2つの機能があります。 ここではミックスインについて説明します。
クラスの親子関係
Rubyの演算子とその再定義について書きます。
今回からクラスとインスタンスを定義、生成する方法を説明します
Kernelモジュールのメソッドはどこでも使うことができます。 そのメソッドの中には便利で有用なものが多いです。
ここでは私が便利だと思ったメソッドを紹介します。
実数
今回はシンボルとハッシュについて説明します。
文字列は最も使うオブジェクトのひとつです。 特にウェブ・アプリケーションでは、コンテンツだけでなくHTMLのタグやCSSを含めすべてが文字列です。 Rubyは文字列オブジェクトのメソッドが充実しており、またパターンマッチのための正規表現も充実しています。
配列は、どのプログラミング言語にもあると思います。 複数の要素を一括して扱うことができるのが配列です。 Rubyの配列はメソッドが充実しているので、プログラムを効率的、機能的に書くのに役立ちます。
今回の目標はインスタンスです。 インスタンスを説明するために、ローカル変数と文字列オブジェクトを事前に扱います。
今回はメソッド定義です。 メソッド定義はRubyの核心ですが、今回はトップレベルに限って説明します。 この限定によって、内容はかなり易しくなっています。
ブロックはRubyの特長です。 ブロックのおかげで記述が非常にすっきりと分かりやすくなります。 今回はブロックをイテレータの本体として使う方法を説明します。
ここではRubyの最も基本的なオブジェクトである整数について説明します。
「徒然なるままに」をネットで調べてみると、「することもなく、手持無沙汰なのにまかせてという意味」とありました。 まさに、自分の現状を言い当てた言葉。 しかも、ブログに書くネタもなかなか思いつかない日々。