単項マイナスと構文解析
単項マイナスとは 単項マイナスと括弧 括弧なし単項マイナスを許容する場合のBNF calcの場合
Rubyはグラフィックについて弱い印象があります。 しかし、グラフィックはデバイスに関することなので、言語そのものには直接の関係はないはずで、あるとすればライブラリです。 今後グラフィック関係のgemが開発されることに期待しましょう。
そのような状況の中で、現時点でグラフィックやグラフィック・ユーザ・インターフェース(GUI)のRubyをめぐる状況を調べていました。 今回はShoesというライブラリを取り上げます。
2022/11/15の時点で、Shoesには安定版のバージョン3.3と開発版のバージョン4がありますが、安定/開発だけではない違いがあります。 バージョン3はCを使っていましたが、バージョン4はJRubyのgemとなっています。つまり、Javaベースです。 また、その開発は現時点では活発とはいえません。 Shoes3の最後のコミットが2020年1月9日、Shoes4が2019年4月5日です。
今回はShoes4を試してみました。 PC環境はUBUNTU22.10です。 解説記事というよりは、使用記です。
Java(open-jdk)がインストールされているかを確認します。
$ java --version
openjdk 11.0.17 2022-10-18
OpenJDK Runtime Environment (build 11.0.17+8-post-Ubuntu-1ubuntu2)
OpenJDK 64-Bit Server VM (build 11.0.17+8-post-Ubuntu-1ubuntu2, mixed mode, sharing)
このようにコマンドラインからjava --version
でバージョン表示がされれば、すでにインストールはできています。
なお、これはUBUNTUでのデフォルトのバージョンです(OpenJDK-11JDK)。
もっと新しいバージョンもあります(最も高いバージョンは2022/11/15の時点でOpenJDK-20-JDK)ので、それを使いたい場合はaptでインストールします。
先程のコマンドでバージョンが表示されなければJavaをaptでインストールしてください。
私の場合はrbenvを使ってRubyをインストールしているので、JRubyにもrbenvを使います。 インストール可能なバージョンを確認します。
$ rbenv install -l
2.6.10
2.7.6
3.0.4
3.1.2
jruby-9.3.4.0
mruby-3.0.0
rbx-5.0
truffleruby-22.1.0
truffleruby+graalvm-22.1.0
Only latest stable releases for each Ruby implementation are shown.
Use 'rbenv install --list-all / -L' to show all local versions.
JRuby-9.3.4.0があるので、それをインストールします。
$ rbenv install jruby-9.3.4.0
この段階で私のUBUNTUには、「Ruby 3.1.2」「JRuby 9.3.4.0」の2つのRubyがインストールされました。 この使い分けには「rbenv local」コマンドを使います。 あるディレクトリでJRubyを使いたい場合は、そのディレクトリに移動して
$ rbenv local jruby-9.3.4.0
とします。 このディレクトリでrubyを起動するとJRubyが呼ばれます。 なお、このときディレクトリ内にRubyバージョンを書いた隠しファイル「.ruby-version」が置かれ、rbenvはそれを手がかりにRubyを起動するのです。
JRubyを指定したディレクトリ(上記の rbenv local jruby-9.3.4.0
したディレクトリ)でshoesのgemをインストールします。
$ gem install shoes --pre
Fetching shoes-4.0.0.rc1.gem
Fetching shoes-core-4.0.0.rc1.gem
Fetching shoes-package-4.0.0.rc1.gem
Fetching furoshiki-0.6.1.gem
Fetching shoes-swt-4.0.0.rc1.gem
Successfully installed shoes-core-4.0.0.rc1
Successfully installed furoshiki-0.6.1
Successfully installed shoes-package-4.0.0.rc1
Successfully installed shoes-swt-4.0.0.rc1
Building native extensions. This could take a while...
Successfully installed shoes-4.0.0.rc1
Parsing documentation for shoes-core-4.0.0.rc1
Installing ri documentation for shoes-core-4.0.0.rc1
Parsing documentation for furoshiki-0.6.1
Installing ri documentation for furoshiki-0.6.1
Parsing documentation for shoes-package-4.0.0.rc1
Installing ri documentation for shoes-package-4.0.0.rc1
Parsing documentation for shoes-swt-4.0.0.rc1
Installing ri documentation for shoes-swt-4.0.0.rc1
Parsing documentation for shoes-4.0.0.rc1
Installing ri documentation for shoes-4.0.0.rc1
Done installing documentation for shoes-core, furoshiki, shoes-package, shoes-swt, shoes after 5 seconds
5 gems installed
間違って、Ruby 3.1.2 が起動するディレクトリでgem install shoes
とすると、Shoes3のgemがインストールされるので注意してください。
また、このgemだけではShoes3は動かないようです。
手始めはいつも「Hello world」の表示です。 次のプログラムをShoes4で動かします。
Shoes.app title: "Hello" do
stack do
para "Hello world"
end
end
このプログラムをhello.rbのファイル名で保存し(Jrubyが動くディレクトリに、以下Jrubyを前提とする)コマンドラインからshoesを起動すると次のような画面が現れる。
$ shoes hello.rb
Shoesの使い方を説明します。
ウィンドウを作成するにはShoes.app
の特異メソッドのブロックにエレメントを作成するメソッドを書きます。
このとき、ブロックのselfはShoes::APP
クラスのオブジェクトに変更されます。
なお、Rubyの原則ではメソッドのブロックのselfはメソッド外側のselfと同じです。
この特異メソッドでは原則と異なる扱いになるように設定されているということです。
ブロック内の関数形式のメソッドはselfをレシーバとするので、Shoes.app
のブロック内の関数形式のメソッドはShoes::APP
クラスのインスタンスメソッドになります。
hello.rb
で使ったpara
というメソッドもShoes::APP
のインスタンスメソッドです。
hello.rb
ではstackメソッドも使いました。
stackメソッドのブロックではselfの変更はしません。
ほとんどのShoes4のメソッドはそのブロックでselfの変更をしませんが、マニュアルによるとwindowメソッドもselfの変更をするそうです。
ウィンドウ内にエレメントを置くメソッドには次のようなものがあります。
他にも沢山エレメントがあるので、Shoesのマニュアルを参照してください。
これらのメソッドの返り値はそれぞれのオブジェクトを返します。
例えば、paraメソッドはShoes::Para
クラスのオブジェクトを返します。
このオブジェクトに対してメソッドを使うことができます。
@para = para "こんにちは" #=> 「こんにちは」をウィンドウ内に段落として表示
@para.text = "さようなら" #=> その段落の文字列を「さようなら」に変更する
どのようなメソッドがあるかを調べるにはAPIドキュメントを見れば良いのですが、なかなか探しにくいかもしれません。
Shoes::Para
クラスがAPIドキュメントでは書かれていませんが、これはShoes::TextBlock
のサブクラスです。
このことは、ソースファイルを見るか、あるいはShoes::Para.ancestors
やShoes::Para.superclass
といったメソッドを実行して調べることで分かります。
Shoes::TextBlock
のメソッドにはtext
やtext=
があるので、これらはShoes::Para
でも使えることが分かります。
buttonメソッドには文字列の引数を与え、表示されたボタンのラベル(ボタンに書かれる文字列)を指定できます。
また、buttonメソッドが返すShoes::Button
クラスのオブジェクトにはclick
メソッドがあり、クリックされたときの動作を記述できます。
@button = button "ボタン"
@button.click do
(ボタンがクリックされたときの動作を記述)
end
クリック時の動作はbuttonメソッドにブロックを付けてそこに記述することもできます。
スロットはエレメントを並べるためのコンテナで、フローとスタックがあります。
エレメントはflowまたはstackメソッドのブロックに記述します。
簡単な電卓プログラムを書きました。
2つのファイルcalc.rb
とlib_calc.rb
から成ります。
calc.rb
がShoesを使ってウィンドウを表示し、lib_calc.rb
が文字列を構文解析して計算をします。
このプログラムを実行すると次のような画面が現れます。
$ shoes calc.rb
また、計算を実行すると次のようになります。
**
)、三角関数、指数関数、対数関数が可能v
で参照できるa=10+2
のようにイコールで代入する以下にcalc.rb
のプログラムを示します。
require_relative 'lib_calc.rb'
def get_answer a
if a.instance_of?(Float) && a.to_i == a
a.to_i.to_s
else
a.to_s
end
end
Shoes.app title: "calc", width: 400, height: 80 do
@calc = Calc.new
flow do
@edit_line = edit_line "", margin_left: 10
@do_calc = button "計算", margin_left: 10
@clear = button "クリア", margin_left: 3
@close = button "終了", margin_left: 10
end
stack do
@answer = para "", margin_left: 10
end
@do_calc.click do
@answer.text = get_answer(@calc.run(@edit_line.text))
end
@clear.click {@edit_line.text = ""}
@close.click {close}
end
lib_calc.rb
で定義されているrun
は引数に文字列を与えるとその計算をして答え(Floatオブジェクト)を返す。
エラーが発生したときは答えの代わりにエラーメッセージを返すget_answer
メソッドは答えが整数のとき、Integerクラスに変えてから文字列にしている。
このことにより、例えば「12.0」でなく「12」という文字列にするShoes.app
メソッドの中は2つのスロット(フローとスタック)を設定している@do_calc
は「計算」ボタンのオブジェクト。
clickメソッドで、入力枠の文字列から@calc.run
で計算し、get_answer
で文字列化して@answer
の段落エレメントの文字列に代入している@clear
は「クリア」ボタンのオブジェクトで、クリックされたときに@edit_line
オブジェクト(入力枠)の文字列を空文字列にする@close
は「終了」ボタンのオブジェクトで、クリックされたときにclose
メソッドを呼び出す。
close
メソッドはウィンドウを閉じる。Shoesのプログラムはこのように簡単です。
click
メソッドは、ボタンクリックのイベントに対するハンドラを定義しています。
この段階ではイベント処理をしているのではなく、イベント処理のハンドラのセットをしているだけです。
開発が活発でないのは残念ですが、電卓のような簡単なプログラムであれば開発には十分です。 ちょっと気になるのはJRubyの起動に時間がかかることです。
最後にlib_calc.rb
のソースを示しますが、長いので説明は省略します。
なお、プログラムのコードはGitHubのBlog-about-Rubyレポジトリにあります。
ディレクトリは_example/shoes/
です。
class Calc
include Math
def initialize
@table = {}
@value = 0.0
end
# calculate s
# error => return error message
# success => return the result as a string
def run(s)
a = parse(s)
if a.instance_of? Float
@value = a # keep the result of the calcukation.
a = a.to_i if a.to_i == a
end
a.to_s
end
# error => return nil
# success => return array like:
# [[:id, "var"], [:=, nil], [:num, 12.34], [:+, nil], ... ... ...]
def lex(s)
result = []
while true
break if s == ""
case s[0]
when /[[:alpha:]]/
m = /\A([[:alpha:]]+)(.*)\Z/m.match(s)
name = m[1]; s = m[2]
if name =~ /sin|cos|tan|asin|acos|atan|exp|log|sqrt|PI|E|v/
result << [$&.to_sym, nil]
else
result << [:id, name]
end
when /[[:digit:]]/
m = /\A([[:digit:]]+(\.[[:digit:]]*)?)(.*)\Z/m.match(s)
result << [:num, m[1].to_f]
s = m[3]
when /[+\-*\/()=]/
if s =~ /^\*\*/
result << [s[0,2].to_sym,nil]
s = s[2..-1]
else
result << [s[0].to_sym, nil]
s = s[1..-1]
end
when /\s/
s = s[1..-1]
else
@error_message = "Unexpected character."
result = nil
s = "" # remove the rest of the string.
end
end
result
end
# BNF
# program: statement;
# statement: ID '=' expression
# | expression
# ;
# expression: expression '+' factor1
# | expression '-' factor1
# | factor0
# ;
# factor0: factor1
# | '-' factor1
# ;
# factor1: factor1 '*' power
# | factor1 '/' power
# | power
# ;
# power: primary ** power
# | primary
# ;
# primary: NUM | 'PI' | 'E' | '(' expression ')' | function '(' expression ')' | 'v';
# function: 'sin' | 'cos' | 'tan' | 'asin' | 'acos' | 'atan' | 'exp' | 'log' ;
# parser
# error => return error message
# success => return the result of the calculation (Float)
def parse(s)
tokens = lex(s)
return @error_message unless tokens # lex error
tokens.reverse!
a = statement(tokens) # error
return "syntax error." unless tokens == []
a ? a : @error_message
end
private
# error => return false and the error message is assigned to @error_message
# success => return the result of the calculation (Float)
def statement(tokens)
token = tokens.pop.to_a
case token[0]
when :id
a = token[1]
b = tokens.pop.to_a
if b[0] == :'='
return false unless c = expression(tokens)
install(a, c)
c
else
tokens.push(b) if b[0]
tokens.push(token)
expression(tokens)
end
when nil # token is now empty.
syntax_error
false
else
tokens.push(token)
expression(tokens)
end
end
def expression(tokens)
return false unless (a = factor0(tokens))
while true
token = tokens.pop.to_a
case token[0]
when :'+'
b = factor1(tokens)
unless b
break false
end
a = a+b
when :'-'
b = factor1(tokens)
unless b
break false
end
a = a-b
when nil
return a
else
tokens.push(token)
break a
end
end
end
def factor0(tokens)
token = tokens.pop.to_a
case token[0]
when :'-'
b = factor1(tokens)
b ? -b : false
when nil
syntax_error
false
else
tokens.push(token)
factor1(tokens)
end
end
def factor1(tokens)
return false unless (a = power(tokens))
while true
token = tokens.pop.to_a
case token[0]
when :'*'
b = power(tokens)
unless b
break false
end
a = a*b
when :'/'
b = power(tokens)
unless b
break false
end
if b == 0
@error_message = "Division by 0.\n"
break false
end
a = a/b
when nil
break a
else
tokens.push(token)
break a
end
end
end
def power(tokens)
return false unless (a = primary(tokens))
token = tokens.pop.to_a
case token[0]
when :'**'
b = power(tokens)
if b
a**b
else
false
end
when nil
a
else
tokens.push(token)
a
end
end
def primary(tokens)
token = tokens.pop.to_a
case token[0]
when :id
a = lookup(token[1])
@error_message = "Variable #{token[1]} not defined.\n" unless a
a ? a : false
when :num
token[1]
when :PI
PI
when :E
E
when :'('
b = expression(tokens)
return false unless b
unless tokens.pop.to_a[0] == :')'
syntax_error
return false
end
b
when :sin, :cos, :tan, :asin, :acos, :atan, :exp, :log, :sqrt
f = token[0]
unless tokens.pop.to_a[0] == :'('
syntax_error
return false
end
b = expression(tokens)
return false unless b
unless tokens.pop.to_a[0] == :')'
syntax_error
return false
end
method(f).call(b)
when :v
@value
when nil
syntax_error
false
else
syntax_error
false
end
end
def install(name, value)
@table[name] = value
end
def lookup(name)
@table[name]
end
def syntax_error
@error_message = "syntax error."
end
end
単項マイナスとは 単項マイナスと括弧 括弧なし単項マイナスを許容する場合の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の最も基本的なオブジェクトである整数について説明します。
「徒然なるままに」をネットで調べてみると、「することもなく、手持無沙汰なのにまかせてという意味」とありました。 まさに、自分の現状を言い当てた言葉。 しかも、ブログに書くネタもなかなか思いつかない日々。