おもこん

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

徒然Ruby(42)gemを作って公開してみた

lbtというgemを作って公開してみた

以前からLaTeXで効率的に書籍を作ることを考えていました。 特に大きな文書、例えば100ページを越えるような文書ではタイプセットに時間がかかるのが問題です。 それを解決するには

  • 文書を複数のファイルに分ける
  • ひとつのファイルだけをタイプセットしてその出来栄えをチェックする。 これによって、タイプセットの時間を短縮できる

ということが必要です。

そのためのツールとしてLaTeX-Buildtoolsというプログラム群を作ってきました。 最初のバージョンはBashスクリプト、2番目はRubyとRakeを使ったものでした。 今回、3番めのバージョンとしてgemにすることを考えました。 それによって、ツールのインストールが格段に易しくなるからです。

$ gem install lbt

この1行だけでインストールが完了します。

今回の記事は、この作業で得た知見をもとに、gemのビルドと公開について書きたいと思います。 なお、RubyGems.orgのガイドに分かりやすいチュートリアルがあるので、そちらをご覧になるのも有益です。

lbtはどんなgemか

本題に入る前にlbtがどんなものかを説明します。

$ lbt new sample

これでsampleフォルダができ、その中にmain.texやhelper.texといったテンプレートが生成されます。 テンプレート内のタイトルや著者を書き直します。 そして本文部分をchap1/sec1.tex、chap1/sec2.texなどのファイルに、セクションごとに作っていきます。 なお、「chap数字」は章を表すディレクトリで、「sec数字.tex」はセクションのファイルです。 ファイル構成についてはGitHubのLbtのrakeバージョン・ブランチのReadme.mdを参考にしてください。 これができあがったらPDFファイルを生成します。 sampleフォルダをカレント・ディレクトリにして

$ lbt build

なお、ソースファイルはMarkdownも可能です。

ファイルの配置

gemを作るには特定のファイル配置をしなければなりません。

$ tree
.
├── License.md
├── README.md
├── Rakefile
├── Tutorial.en.md
├── Tutorial.ja.md
├── bin
│   └── lbt
├── lbt.gemspec
├── lib
│   ├── lbt
│   │   ├── build.rb
│   │   ├── create.rb
│   │   ├── part_typeset.rb
│   │   ├── renumber.rb
│   │   └── utils.rb
│   └── lbt.rb
└── test
    ├── test_build.rb
    ├── test_create.rb
    ├── test_lbt.rb
    ├── test_num2path.rb
    ├── test_part_typeset.rb
    ├── test_renumber.rb
    ├── test_utils1.rb
    └── test_utils2.rb

これがlbtディレクトリ構成です。 ポイントになるのは、

  • License.md、README.md、Rakefilelbt.gemspecをトップディレクトリに置く
  • 実行ファイル(lbt)はbinディレクトリの下に置き、実行可能属性をオンにする(chmodで755にすればよい)
  • libディレクトリの下にlbt.rb、つまり「gem名.rb」というファイルを置き、このファイルを通して下位ファイルをrequireないしrequire_relativeで取り込む
  • libディレクトリの下にlbtディレクトリを置き、その中に下位ファイルを置く
  • testディレクトリの下にテスト用ファイルを置く

以上から、本体のプログラムは、bin/lbt、lib/lbt.rb、lib/lbtディレクトリ下の諸ファイル、になります。

lbt.gemspec

「gemの名前.gemspec」というファイル(上記ではlbt.gemspec)がgemの内容を定義するファイルです。

Gem::Specification.new do |s|
  s.name              = 'lbt'
  s.version           = '0.5'
  s.summary           = 'LaTeX Build Tools'
  s.description       = 'Lbt is a build tool for LaTeX. It is useful for big documents.'
  s.license           = 'GPL-3.0'
  s.author            = 'XXXX XXXX'
  s.email             = 'XXXXXX@XXXXl.com'
  s.homepage          = 'https://github.com/ToshioCP/LaTeX-BuildTools'
  s.files             = ['bin/lbt', 'lib/lbt.rb', 'lib/lbt/build.rb', 'lib/lbt/create.rb', 'lib/lbt/part_typeset.rb', 'lib/lbt/renumber.rb', 'lib/lbt/utils.rb']
  s.executables       = ['lbt']
end

名前、バージョン、要約、説明、ライセンス、著者、連絡先email、ホームページ、gemに取り込むファイルの配列、実行ファイル名を指定しています。 この他にも設定項目を設けることができるので詳細はRubyGems.orgのガイドを参照してください。

Rakefile

Lbtでは、Rakefileにドキュメント生成(RDoc)とテストについて記述しました。 これに加えて、gemのビルドを記述することもできます。 Rubyのドキュメントを参考にしてください。

require "rdoc/task"
require "rake/testtask"

RDoc::Task.new do |rdoc|
  rdoc.main = "README.md"
  rdoc.title = "LaTeX-Buildtools"
  rdoc.rdoc_dir = "doc"
  rdoc.rdoc_files.include("README.md", "License.md", "Tutorial.en.md", "Tutorial.ja.md", "lib/lbt.rb", "lib/lbt/*.rb")
end
task :rdoc do
  touch "doc/.nojekyll"
end

Rake::TestTask.new do |t|
  # t.libs << "test"
  t.test_files = Dir.glob("test/test_*")
  t.verbose = true
end

RDoc::Task.new以下がドキュメント作成タスクを生成し、Rake::TestTask.new以下がテストの実行タスクを生成します。 コマンドラインからは、rdoc、testをrakeの引数にすることでそれぞれのタスクを実行します。

$ rake rdoc #=>ドキュメントを生成
$ rake test #=>テストを実行

ドキュメントやテストの内容は省略しますが、興味のある方はGitHubレポジトリを参照してください。

gemのビルド

gemをビルドするには、gemコマンドを用います。

$ gem build lbt.gemspec

これにより、カレントディレクトリにlbt-0.5.gemが出来上がります。 このファイルからgemをインストールするには

$ gem install ./lbt-0.5.gem

とします。 インストールが完了すると、lbtコマンドが実行できるようになります。 例えば

$ lbt new sample

でsampleディレクトリを生成し、テンプレートをその下に作ります。

RubyGems.orgへのアップロード

RubyGems.orgにgemをアップロードすることにより一般に公開することができます。 他のユーザは

$ gem install lbt

という1行でlbtをインストールできるようになります。

アップロードは次の手順で行います。

  • RubyGems.orgにサインアップ(ユーザ登録)する(サインアップはRubyGems.orgのウェブ画面から行う)
  • gem push (gemファイル名)でアップロードする(その時登録したユーザ名とパスワードが必要)

以上、gemの作成と公開の手順を紹介しました。 みなさんもRubyの有用なアプリやライブラリを持っていたらぜひGemとして公開してください。

徒然Ruby(41)エンコーディング

文字列のエンコーディングに頭を悩ませることはほとんどなくなりました。 なぜなら、どのアプリ、システムもUTF-8を使うようになったからです。 Rubyでもエンコーディングの問題が起こることはまず無いでしょう。 ですが、今回はエンコーディングの考え方を整理してみたいと思います。

ASCIIコード

コンピュータの内部では文字を数字に置き換えて記憶しています。 これを文字コードといいます。 初期の有名な文字コードにASCII(アスキー)がありますが、これは7ビットで表すことができます。 ビットとは、メモリーの最小単位で、1と0を区別できるものです。 8個のビットをバイトといい、コンピュータはバイト単位でメモリーを扱います。 1ビットは0と1を表すことができますが、1バイトだと2^8=256個を区別でき、数字としては0から255までを区別できるようになります。 ASCIIは7ビットなので、0から2^7-1=127までの数字に文字が対応します。

ASCII - Wikipedia

例えば大文字のAは16進数の41(10進数の65)小文字のaは16進数の61(10進数の97)などです。 詳しくは上記のリンク先を参照してください。 ASCIIで表せるのは大文字と小文字のアルファベット、ピリオドなどの記号、改行などを表すコントロールコードだけです。 要するに、キーボードで直接入力できる文字だと思えば良いでしょう。

ASCIIは7ビットですが、コンピュータはバイト単位にデータを処理するので、ASCIIも8ビットで処理されることが普通です。 このとき、最上位ビットは0になります。 もしも最上位ビットが1だと、ASCIIの定義外なので、文字としては不定ということになります。 Rubyではこのような1バイト単位で、0から127まではASCIIとして扱うことができるコード体系(エンコード)をASCII_8BITとしています。 主にバイナリデータを扱うのに使われます。

ASCII 以外のコード

日本語にはアルファベット以外に、ひらがな、カタカナ、漢字があります。 他の言語でも、例えばドイツ語ではウムラウトエスツェット(ß)があります。 これらをASCIIで表すことはできません。 そのため、2バイト以上を使って様々な文字と数字(文字コード)を対応させるということが考えられました。 この方法が現在ではUTF-8でほぼ統一されていますが、過去にはSHIFT-JISやEUC-JPなどがありました。 それらをエンコーディングといいます。 つまり、エンコーディングは文字と数字(文字コード)の対応を表すルールなのです。

しかし、UTF-8、SHIFT-JIS、EUC-JPには互換性がありませんので、あるコード体系から別のコード体系には「変換」が必要です。 過去にはWindowsはSHIFT-JISが使われLinuxではEUC-JPが使われていましたので、両者でデータのやりとりをするときには文字コードの変換が必要でした。 また、これらのコードは日本語以外の言語(ASCII以外)の文字サポートがありませんでした。 最終的にはUnicodeという様々な国の言語の文字をサポートするコード体系が生まれ、特にUTF-8が標準的に用いられるようになりました。 現在ではWIndowsLinuxMacUTF-8が標準です。

このようにしてUTF-8がどのシステムでも使われるようになったので、問題は起こらなくなりました。 これらの文字コードのことをエンコーディングといいます。 Rubyでは文字列にエンコーディングが付随していて、UTF-8以外にEUC-JPやSHIFT-JISにも対応できるようになっています。

以下ではRubyエンコーディングが問題になることがらについて説明します。

スクリプトエンコーディングリテラルエンコーディング

Rubyで書いているプログラム自体の文字コードはどのような問題を含むでしょうか? これは「スクリプトエンコーディング」の問題と呼びます。

Rubyのキーワードは、すべてASCIIの範囲にあり、その限りではRubyは正しくスクリプトを解釈してくれます。 UTF-8、SHIFT-JIS、EUC-JPなどは、すべてASCIIの範囲の文字はそのとおりにコードになっています。 例えば「def」の文字コードは16進数で「64 65 66」で、これは上記の3つの文字コードでも同じです。 このようにASCIIの範囲の文字はASCIIと同じコードを使うエンコーディングをASCII互換エンコーディングといいます。 それ以外のエンコーディングはASCII非互換エンコーディングです。

RubyスクリプトにはASCII互換エンコーディングRubyがサポートしている)を使うことができます。 逆にそれ以外、ASCII非互換やRubyがサポートしないエンコーディングは使うことができません。

また、スクリプトエンコーディングを明示的に指定したいときはマジックコメントを使います。 詳しくはRubyのドキュメントの多言語化を参照してください。

文字列リテラル正規表現リテラル、シンボルリテラルには文字列が出てくるので、エンコーディングが関わります。 これらはスクリプトエンコーディングに従います。 ただしバックスラッシュ記法で文字コードを表す場合は他の文字コードに変換されたり、エラーになることがあります。 詳細は多言語化を参照してください。 通常はリテラルエンコーディングスクリプトエンコーディングだと考えれば大丈夫です。

文字列のエンコーディング

Rubyの文字列オブジェクトはエンコーディングを持っていて、encodingメソッドでその文字列のエンコーディングを知ることができます。

p s.encoding #=>#<Encoding:UTF-8>
  • ある文字列を他のエンコーディングの同じ文字列に変更するにはencodeメソッドを使います。
  • ある文字列をその文字列の内容を変えずにエンコーディングを変更するにはforce_encodingメソッドを使います。

この2つは混乱しやすいので注意してください。 encodeメソッドはエンコーディングを変えるだけでなく、文字列の内容(コード)も変更しますが、force_encodingではエンコーディングのみが変更されます。

s = ""
p s.encoding #=>#<Encoding:UTF-8>
t = s.encode(Encoding::EUC_JP)
p t.encoding #=>#<Encoding:EUC-JP>
p s.force_encoding(Encoding::ASCII_8BIT)
p t.force_encoding(Encoding::ASCII_8BIT)

これを実行すると

#<Encoding:UTF-8>
#<Encoding:EUC-JP>
"\xE3\x81\x82"
"\xA4\xA2"

となります。 後半2行から、sとtでは文字コードが変更されていることが分かります。 異なるコードですが、表している文字は両方とも「あ」です。

文字列を==で比較する場合、「等しい」と判定されるのは次の2条件を満たすときです。

したがって、sとtは両方とも「あ」を表しているが、エンコーディングが異なるので、「s==t」はfalseになります。

このような問題は複数のエンコーディングを使っているところから発生するので、ひとつのエンコーディングだけならば、ことは単純になります。

I/Oのエンコーディング

外部から入力するときに、それが文字列であればエンコーディングが問題になります。

テキストの読み込み

テキスト読み込みメソッド、例えばIO.readlinesエンコーディングの影響を受けます。 読み込み元はRubyの外部ですから、Rubyエンコーディングを決めることはできません。 プログラマーが外部のエンコーディングを把握して、Rubyに設定することになります。 このエンコーディングをIOの「外部エンコーディング」といいます。

Rubyの内部で使っているエンコーディングはデフォルトでUTF-8です(変更もできますが)。 これを「内部エンコーディング」といいます。 あるIOに対して「外部エンコーディング」と「内部エンコーディング」がわかっていれば、Rubyは読み込み時に変換してくれます。 これらのエンコーディングを指定するのが「set_encoding」メソッドです。 その引数は、外部エンコーディング、内部エンコーディングの順に指定します。 エンコーディングには文字列またはエンコーディング定数(例えばEncoding::UTF_8)が使えます。

今、「こんにちは」という日本語テキストがEUC-JPで保存されたファイル「gr_euc.txt」があるとします。 これを読むこむときにUTF-8に変換するには次のようにします。

f = File.open("gr_euc.txt", "r")
f.set_encoding("EUC-JP", "UTF-8")
print f.read #=> こんにちは
f.close

詳細はIO のエンコーディングとエンコーディングの変換を参照してください。

Ruby/gtk4を使う場合、RubyでなくGTK 4、より正確にはGIOの入力関数を使うことがあります。 そのとき、エンコーディングが考慮されていないので、Rubyとしてはバイナリ入力のASCII-8BITでエンコーディングを設定することがあります。 そのときには必要なエンコーディングをforce_encodingメソッドで入力文字列に与えることが必要になります。

テキストの書き込み

テキストの書き込みは読み込みよりも単純です。

s = "あいうえお" # UTF-8
f = File.open("gr.txt", "w")
f.write(s) # UTF-8で出力
f.close

f = File.open("gr_euc.txt", "w")
f.set_encoding("EUC-JP")
f.write(s) # EUC-JPで出力
f.close

f = File.open("gr_sjis.txt", "w")
f.set_encoding("SJIS","UTF-8")
f.write(s) # Shift-JISで出力
f.close
標準入出力

ここでは、デフォルトの標準入出力である、キーボード入力と画面出力について扱います。 これらは、オペレーティング・システムによって、どのエンコーディングを使うかが決められます。 UBUNTUなどのLinuxオペレーティング・システムでは現在はほとんどUTF-8です。

ですから、Rubyスクリプトが他のエンコーディングの文字列を持っていて、それを画面出力するときにはUTF-8に変換しなければなりません。 この方法には2つあります。

  • 文字列のエンコーディングUTF-8に変換しておく。これはencodeメソッドでできます。
  • $stdoutの外部エンコーディングはデフォルトでnilになっている(つまり出力時に何の変換もしない)。それをUTF-8に設定すると出力時に自動的に変換をしてくれる。
$stdout.set_encoding(Encoding::UTF_8)

徒然Ruby(40)スレッドが意外に遅い

Fiberを書いたときから、次はスレッドを書こうと思っていましたが、時間がかかってしまいました。 その理由は、期待したとおりのスレッドの効果がなかったためです。 今回はそのことを書きますが、これはRubyのスレッドの抱えている問題なのか、自分のやり方が悪いのかははっきりしていません。

スレッドの基本

スレッドとは

  • 並行して走るプログラムで、Rubyの場合は「プログラム」はブロックになる
  • ひとつのプロセス内で複数のメソッドが並行して動き、プロセスをまたいでメソッドが動くことはない
  • Rubyの場合は一度にはひとつのメソッドしか動かない(例外有り)。複数のメソッドが交代で動くイメージ

Rubyには子プロセスを立ち上げる機能もあります。 それを使うと2つのプログラムが同時に動くことができます。 現在は複数コアのCPUがほとんどなので、まさに同時です。 それぞれのプログラムが関連することなく切り分けられれば、複数プロセスが最速になります。

Threadクラス

RubyではThreadクラスでスレッドを生成します。 次の例では最初のスレッドがaからzまでを画面に出力、2番めのスレッドが100から200までを画面に出力します。 スレッドは途中で切り替わるので、アルファベットと数字が混在して出力されます。

def ax100200
  t1 = Thread.new {("a".."z").each {|c| print "#{c}\n"}}
  t2 = Thread.new {(100..200).each {|x| print "#{x}\n"}}
  t1.join
  t2.join
end
  • Thread.newのブロックがひとつのスレッドになる。newメソッドの返り値はスレッドオブジェクトになる
  • このプログラムにはスレッドが3つあり、t1、t2とメインスレッド(最初に動くプログラム自身)がある
  • メインスレッドが終了すると、子孫メソッドも強制的に終了させられる。 それを避けるには、メインスレッドを無限ループにするか、joinメソッドで子孫メソッドの終了を待つようにする。 上記のプログラムでは、t1.joinでt1の終了までメインプログラムが待つようになり、t1の終了で再開の後にt2.joinでt2の終了を待つようになる

joinメソッドのタイミングは重要で、仮にt2生成の前にt1.joinしてしまうと、t1の終了後にt2が生成されることになり、並行には動いていないことになります。

スレッドが有効なケース

Rubyのスレッドは一度にはひとつのスレッドしか動かないので、CPUで大量の計算をするようなプログラムをスレッドにしても時間短縮にはなりません。 しかし、CPUに待ち時間があり、その間他のスレッドを実行することにより、全体の実行時間を短縮することは期待できます。

  • I/OはCPUに比べ低速なので、I/O待ちのあるプログラムに使う
  • 同様に通信もCPUに比べて低速なので、通信待ちのあるプログラムに使う。例えばダウンロードを別プロセスにするなど

これ以外に、同時に2つのものが動くような事象をプログラム化するときにはスレッドが向いています。 例えば点Aを点Bが最短で追跡するとき、Aの動きとBの動きをシミュレートするなどが考えられます。 ただ、スレッドの切り替わりをスレッド自身がコントロールできないので、シミュレーションは完全なものにはなりませんが。 そのモデルによりますが、ファイバーのほうが良い場合もありえます。

以上の考察に基づき、プログラムを試してみました。 その結果ははたして・・・・?

ファイルの読み込み

ファイルの読み込みには時間がかかるから、マルチスレッドにすれば速いのではないか? 実際にやってみました。

def s_read(files)
  files.each {|f| File.read(f)}
end

def c_read(files)
  threads = []
  files.each do |file|
    threads << Thread.new(file) {|f| File.read(f)}
  end
  threads.each {|t| t.join}
end

def s_or_c_input
  files = Dir.glob("_example/*.rb")

  t1 = Time.now
  s_read(files)
  t2 = Time.now
  p t2 - t1

  t1 = Time.now
  c_read(files)
  t2 = Time.now
  p t2 - t1
end

s_or_c_input

s_readがスレッドなしのシーケンシャル(一列に並んだ)に読み込むメソッド、c_readがコンカレント(同時並行)な読み込みのプログラムです。 実行してみると

$ ruby _example/example40.rb
0.011881702
0.034613109
$ ruby _example/example40.rb
0.000459287
0.034651356

なんと、シーケンシャルの方が速い。 しかも2回めは圧倒的な差に広がっています。

ということは、メソッドの生成にかかる時間が大きく影響しているのではないでしょうか。 また、2回目で大差になったのは読み込みにおけるキャッシュの効果ではないかと思いました。

書き込みではどうかと思い、実験しましたが、そちらもシーケンシャルの方が速かったです。 2回行うと、2回めの方が差が開きました。 書き込みにおいてもキャッシュの効果が出たようです。

コマンドの受付をスムーズに行う

コマンドを受け付けて、それに対応する処理をする場合、処理時間が長いと次の受付までの待ち時間が発生します。 それをスレッドを使うことによって待たずに済むようにすることができます。

Readlineクラスを使ってやってみました。

require "readline"

def rl
  threads = []
  # If the input is EOF (ctrl-d), Readline.readline returns nil.
  while buf = Readline.readline("> ", false)
    i = buf.to_i
    if 1 <= i && i <= 9
      threads << Thread.new(i) do |n|
        x = (1..(n*100000000)).inject {|a,b| a+b}
        File.open("tempfile","a") {|file| file.print("#{x}\n")}
      end
    end
  end
  threads.each {|t| t.join}
end

rl

目論見通り処理を待たずに次のプロンプトが出るのですが、マルチスレッドの影響でreadlineのプロンプトに乱れが出ました。 ちかちかしたり、プロンプトが2個でたりします。 readlineはスレッド対応しているとのことなので、原因は良くわかりませんでした。 終了させるにはCtrl-Dを押します(Linuxの場合)。 それはreadlineにはEOF(end-of-file)となって伝わり、readlineメソッドがnilを返してループを抜けることができます。 しかし、子メソッドの終了を待つので、プログラム全体の終了には時間がかかります。 これは高速化とはいえません。

結論

Rubyのスレッドは時間がかかるので、効果があるようなケースを見つけて使うことになると思います。 おそらくサーバ関係のプログラムでは効果を発揮すると思います。 また、ダウンロードやバックアップをバックグラウンドでやるのも効果がありそうです。 普段のちょっとしたプログラムでは使いそうもないな、というのが実感でした。

Worry に悩む

英語のテキストにこのような文章がありました。

  • I'm worrying about that.
  • I'm worried about that.

ネットを検索すると、「worryは現在進行形にはしない」という発言もありますが、実際には英語のテキストには存在します。 さて、この2つについて考えてみました。 以下は私見ですので、必ずしも正しいとは限りません。 ご注意ください。

worry の自動詞、他動詞

worryは自動詞としても他動詞としても使えます。

  • 自動詞:(人が、何かのことで)心配する、気にする、悩む。前置詞aboutなどが使える
  • 他動詞:(人・物・事が、人を)心配させる、悩ませる、苦しめる。前置詞aboutなどが使える

自動詞としては

I worry about my son.

「私は息子のことが心配だ」 現在形は繰り返し行われることや、習慣的な行為などを表すので、これは話者がいつも息子のことを心配している、という意味になります。 息子が良い成績をとろうが、悪い成績をとろうが、いつも息子のことで頭を悩ませている、ということです。

I'm worried about her illness.

「私は彼女の病気が心配だ」 これは受動態ですが、「彼女の病気」が心配の種なので、いつでも心配しているわけではありません。 今、彼女が病気になっていて、そのことが私を心配させる、それを受動態で表現したものです。 また、このworriedは形容詞化してきていて、byでなくaboutを使うのもそのあたりの影響のようです。

上記の2つは時間的な広がりが違うので、その点を押さえれば使いやすくなります。

I'm worrying

I'm worrying about that.

「私はそのことを心配している」

I'm worried about that.

「私はそのことが心配だ」

この2つはほぼ同じ意味だと考えて良い、と辞書にありました(ジーニアス英和辞典、ただし例文は異なるものが使用されている)。 前者が、現在進行形になっているので、現在その動作(心配すること)が続いている、動作中である、というニュアンスだと思います。 自分の印象としては、若干前者が能動的、後者が受動的な感じがします。

最も良く使われるのは

いろいろな情報に当たっても、同様に「be worriedがもっとも良く使われる」と書かれています。 ですので、現在進行形よりも受動態の「be worried」を使うようにすれば良いのではないでしょうか。

worryは自動詞と他動詞の2つがあるので、このような複雑な事態が生じますが、exciteのように他動詞しかなければ単純になります。

The concert was exciting.

コンサートはエキサイティングだった。

The concert excited the audience.

コンサートが聴衆を興奮させた。

これらは他動詞として、コンサートが主語で、聴衆が目的語、動詞の意味は「興奮させる」になります。

I was excited.

私は興奮した(うきうきした)。 私は何かによって興奮させられた、という受動態の文になります。 excitedは過去分詞ですが形容詞化しています。

この場合 I was exciting. は基本的にない。 あるとすれば、私が誰かを興奮させた、という文脈になります。

日本語と英語の違い

日本語は感じた人が主語になります。

「私はびっくりした」

英語は感じた人は受け身になります。

I was surprised.

何かが人をびっくりさせた、という発想です。

徒然Ruby(39)Fiber

Fiberは「ノンプリエンプティブな軽量スレッド」とRubyのマニュアルに記載されています。

  • ノンプリエンプティブ(non preemptive)とは、マルチタスクの切り替えをプログラム自身にまかせること
  • プリエンプティブ(preemptive)とは、マルチタスクの切り替えをOSが行うこと

preemptiveは金融の用語のようです。 「先買いの、先買権のある」と辞書にありますが(ジーニアス英和辞典)、株式の売却時に優先的に買い取る権利を含む契約で、敵対勢力に株式を渡さないための方策だそうです。 このことについては、私は門外漢なので、正確な情報ではないことをお断りしておきます。 IT用語においては、「プリエンプティブ」はCPU時間を割り当てるときにOSが優先的にCPU時間を買取ってタスクに割り当てる、ということから使われるようになったのではないでしょうか。

さて、Fiberを使う場合、メインのプログラムとFiberのブロックの2つのタスクが動きます。 そして、プログラム中のresumeやyieldというメソッドがタスクの切り替えをします。 したがって、切り替えは完全にプログラムによってコントロールされており、その点でいつ切り替わるかはOS次第というプロセスとは異なります。 また、ここでいうタスクはRubyのThreadとは異なるので注意してください。

簡単なプログラム例

ファイバーの定義

ファイバーを定義するには、Fiber.newを使い、ファイバー自体はそのブロックに記述します。

fiber = Fiber.new do
  "abc".each_char do |c|
    print "#{c}\n"
    Fiber.yield
  end
  nil
end

このブロックはメインプログラムの中で呼ばれるfiber.resumeメソッドによって実行されます。

  • 最初にfiber.resumeが呼ばれたとき、ブロックの最初からFiber.yeildまでが実行される
  • Fiber.yieldの実行により、ブロックは一旦実行が止まり、メインルーチンのfiber.resumeの次からに実行が移る
  • 次にfiber.resumeが呼ばれたときは、ブロックのFiber.yieldから実行され、再びFiber.yieldに達するか、ブロックの最後に達するまでそれが続く その後は実行はメインルーチンのfiber.resumeの次に移る
  • ブロックの最後まで達したファイバーはfield.resumeが呼ばれても実行できない。これはエラーになる
簡単なプログラム例

次の例は、メインとファイバーのブロックで交互にprint文を実行するメソッドです。

def example1
  fiber = Fiber.new do
    "abc".each_char do |c|
      print "#{c}\n"
      Fiber.yield
    end
    nil
  end

  101.upto(103) do |j|
    fiber.resume
    print "#{j}\n"
  end
end

# main

example1

このプログラムの実行順を図にしました。

Fiberの実行順

実行結果はつぎのようになります。

$ ruby _example/example39.rb
a
101
b
102
c
103
$ 

ポイントは、Fiber.yieldfiber.resumeで切り替わるところです。

ファイバーの使いどころ

ファイバーはどのようなプログラムに適しているのでしょうか? ファイバーはコルーチンと呼ばれることもありますが、ウィキペディアコルーチンでは、

サブルーチンと異なり、状態管理を意識せずに行えるため、協調的処理、イテレータ、無限リスト、パイプなど、継続状況を持つプログラムが容易に記述できる。

と書かれています。

ここでは、このうちイテレータとパイプについて考えてみたいと思います。 なお、ここでの記述については、誤りを含んでいるかもしれませんので、ご注意ください。 また、誤りにお気づきの方はコメントでご指摘いただければありがたいです。

イテレータ

イテレータというと、Rubyのeachメソッドを思い浮かべるのではないでしょうか。 eachメソッドは、そのオブジェクトの要素を取り出してブロック・パラメータに代入し、繰り返しブロックを実行します。 このような繰り返し処理を「イテレータ」といいます。 eachメソッドの方式は「内部イテレータ」といいます。

これに対して「外部イテレータ」というのがあります。Enumeratorクラスのnextメソッドはその例です。 nextメソッドは呼ばれるたびに「次のデータ」を返します。

def example2
  a = [1,2,3].to_enum
  p a.next #=> 1
  p a.next #=> 2
  p a.next #=> 3
end

example2

このプログラムでは配列[1,2,3]to_enumメソッドでEnumeratorオブジェクトに変換しています。 Enumeratorオブジェクトのnextメソッドは呼ばれるたびに、1,2,3と順にその要素を返していきます。 ひとつのメソッド「next」が呼ばれるたびに異なる要素を返すので、これもイテレータと呼ばれるのです。 より正確には「外部イテレータ」です。

外部イテレータはFiberで簡単に実装できます。

def example3
  fiber = Fiber.new do
    [1,2,3].each do |i|
      Fiber.yield(i)
    end
  end

  p fiber.resume
  p fiber.resume
  p fiber.resume
end

example3

Fiber.yieldに引数をつけると、その引数の値が対応するfiber.resumeの値になります。 これによって、ファイバーから外部にデータを渡すことができます。

この例では、ファイバー外部でresumeを呼ぶたびにファイバー内部のイテレータが繰り返し処理をするので、順に要素が返されます。

なお、Rubyのドキュメント

Enumerator(の外部イテレータ)は Fiber を用いて実装されています。

と書かれています。

パイプ

パイプというのは、Bashなどのシェルプログラムで2つのプロセスをつなぎ、片方の出力を他方の入力につなげる機能です。 例えば

  • cat: ファイルを読み込み、標準出力に書き出す
  • wc: 文字数、単語数、行数などを計算して書き出す

をパイプ「|」で結びつけると、

$ cat example3.rb | wc
     39      59     455

となり、ファイルexample3.rbは

  • 行数が39
  • 単語数が59(単語は空白や改行で区切られる文字列)
  • 文字数が455

であることがわかります。 このとき

  • catによってexample3.rbの内容が標準出力に送られ
  • その出力はパイプ「|」によって次のコマンドの標準入力に結び付けられ
  • それはwcによって行数、単語数、文字数として標準出力に出力される

ということになります。

一般にあるプロセスの出力をバッファに保存し、それを別のプロセスの入力につなげる問題を「生産者ー消費者問題」といいます。 並行動作するプロセスでこれを行う場合、セマフォを使って実現します。 セマフォは一般に短いプログラムで、セマフォの実行中は他のタスクに切り替わらないことが保証されています。

ファイバーを使う場合は、タスクが切り替えのタイミングを決められるのでセマフォは不要で、プログラムも簡単になります。 また、消費者側をメインにし生産者側をファイバーにする「消費者起動方式」が理解しやすいです。

catとwcに相当するプログラムをFiberで作ってみましょう。 ただし、単純化するためwc部分は行数のみをカウントすることにします。

def example4 filename
  fiber = Fiber.new do
    File.open(filename) do |file|
      while (s = file.gets)
        Fiber.yield(s)
      end
    end
    nil
  end

  nline = 0
  while fiber.resume
    nline +=1
  end
  print "#{nline}\n"
end

example4("example39.rb")

ファイバー側はファイルをオープン後、一行入力してはyieldします。 EOFになるとfile.getsnilを返すのでwhileループが終了します。 ですから、最後にfiber.resumeで呼ばれたときは、whileループを脱出するのでFiber.yieldは実行されません。 そのときにはFiber.newのブロックの値(ブロックの最後に評価された値)がfiber.resumeの値として返されます。 すなわち、nilが返ります。

メインルーチンはfiber.resumeの値をチェックして真(この場合は文字列が返っている)ならばnlineをカウントアップします。 最後にnilが返ってきてwhileループを抜け、nlineの値をプリントします。

ファイバーとメソッド

ファイバーはイテレータやパイプなどを分かりやすく表現することができるのですが、同様のことはインスタンス変数とメソッドで実現することができます。 そのため、あまりファイバーは使われないのが実情ではないでしょうか。

しかし、最後の例のようなファイルをオープンするケースをメソッドで実装する場合、オープンとクローズは別メソッドで行うのが多いです。 つまり、オープン、一行読込、クローズの3つのメソッドを用いることになります。 Fiberは、その中でオープン、クローズも行えるので一つで済み、実装が簡単です。

ファイバーは他の言語ではコルーチンと呼ばれることもあります。 例えば、Luaではコルーチンと呼んでいます。 ネットでもファイバーよりもコルーチンで検索するほうが多くの情報にヒットします。

Ruby-GNOME レポジトリへのコントリビューション

今まで他のレポジトリにプルリクエストを出すことは、ほとんどしてきませんでした。 というのは、自分が貢献(コントリビューション)できそうな部分がほとんどなかったからです。 ところが、Ruby-GNOMEプロジェクトについては、多少活動できそうなので、はじめの一歩を踏み出してみました。 つまり、プルリクエストを出しました。

現在Ruby-GNOMEのメインはGTK 4をRubyバインディングすることですが、ほとんど一人の方がコミットしているのみです。 そのため、ドキュメントの方面はあまり手がついていません。 自分は少し前にRuby/GTK4の記事を書いたので、サンプルプログラムの作成ならば協力ができると思いました。

現在は、application1というディレクトリをプルリクエストして、マージしていただきました。 しかし、うかつ者の自分のしたことですから、プロジェクトのオーナーから沢山のサジェスチョンが来ました。 ひとつのプルリクエストがきちんとしたものに仕上がるのに多くの手間がかかり、プロジェクトの方にも迷惑をかけてしまったと思います。

まあ、要領はつかめたので、次回からは前よりはしっかりしたものを作れると思います。

自分はかねがねドキュメントの重要性を主張してきました。 どんなにすばらしいプログラムでもドキュメントがないとユーザの裾野が広がりません。 というのは、プログラムのユーザはハイレベルとは限らないからです。 できるだけ敷居を下げることが普及の条件だと思っています。

これからしばらくは、Ruby-GNOMEのサンプルプログラムの作成をするつもりです。

不定方程式ax+by=1の特殊解の求め方

高校では整数の章で不定方程式ax+by=1を習います。 ただし、文字はすべて整数で、aとbは「互いに素」です。

例えば

 7x+4y=1

の解は


   \begin{cases}
   x &= 3\\
    y &= -5
    \end{cases}

です。 ただし、この他にも(x,y)=(7,-12)、(11, -19)など、無数に解があり、その一般形は


   \begin{cases}
   x &= 3+4t\\
    y &= -5-7t
    \end{cases}

となります。ただし、tは整数です。 この解一般を表したものを「一般解」、そのうちのひとつ(どれでも良い)を「特殊解」といいます。

高校では特殊解から一般解を出すところは説明されているのですが、特殊解の求め方については十分な説明がありません。 また、ネットにおける情報も十分ではないように思います。 そこで、今回は「特殊解の求め方」について書いてみたいと思います。

以下では「特殊解」を単に「解」ということにします。

最も良い方法を手っ取り早く知りたい人は最後の節(合同式を使う方法)だけを読んでください。

総当り方式

総当り方式というのは、整数を代入して試し、解を発見する方法です。 「総当り」というのは、整数全部を試すということではありません。 例えば先程の方程式

 7x+4y=1

の場合、xならば0から3までを試せば解を発見できます。 (0からyの係数4より1だけ小さい数3まで)。 他方、yならば0から6までを試せば解を発見できます。 ですから、xで探すほうが効率が良いです。

  • x=0 ならばy=1/4で不適
  • x=1ならばy=-3/2で不適
  • x=2ならばy=-13/4で不適
  • x=3ならばy=-5で適

高校の試験ではこの方式で短時間で見つけられるような問題が多いです。 しかし、容易に想像できますが、係数が大きくなれば計算は大変になります。 例えば、

 723x+431y=1

の解は


   \begin{cases}
   x &= 31\\
    y &= -52
    \end{cases}

ですが、これを総当り方式でやるのは本当に大変です。 ネット情報では、このようなときに「ユークリッドの互除法を使う」と書かれていることが多いです。 次にこの方法を説明します。

ユークリッドの互除法

ユークリッドの互除法を知っている方はこの節を飛ばしてください。

2つの整数の最大公約数を求める方法のひとつに「ユークリッドの互除法」があります。 例えば、51と36の最大公約数を求めるには、次のようにします。

  • 51を36で割る=>商が1あまりが15
  • そのわり算の「割る数」を「あまり」で割る→36÷15→商が2であまりが6
  • そのわり算の「割る数」を「あまり」で割る→15÷6→商が2であまりが3
  • そのわり算の「割る数」を「あまり」で割る→6÷3→商が2であまりが0

割り切れたときの割る数が最大公約数。したがって、この例では3が最大公約数。

なぜ、これで最大公約数が求まるかは、教科書やネット情報を参照してください。 大事なことは、この方法ならば、

  • さほど多くない計算で最大公約数が求まる
  • かならず最大公約数を求めることができる

ということです。

また、 アルゴリズムがはっきりしているので、プログラムにするのも容易です。 プログラムの本ではリカーシブコールの例として取り上げられていることが多いです。

ユークリッドの互除法を用いた不定方程式の解法

はじめは、簡単な例で説明します。

 7x+4y=1

まず、7と4にユークリッドの互除法を用いて最大公約数を求めます。

  • 7÷4 → 商が1あまりが3
  • 4÷3 → 商が1あまりが1
  • 3÷1 → 商が3あまりが0

これから、最大公約数は1になります。 不定方程式を解くときには下から2番めまでの式を検算の式に変形して使います。 検算の式は、たとえば

 7=4\times 1+3

ですが、ここでは右辺を余りだけにします。

 7+4\times (-1)=3

同様に第2式から

 4+3\times (-1)=1

この式の第2項の3は第1式のあまり、すなわち右辺です。 第1式を第2式に代入して

 4+\{7+4\times (-1)\}\times (-1)=1

これを整理すると


    \begin{align*}
    4+7\times(-1)+4\times(-1)\times(-1)=1\\
    7\times(-1)+4\times 2=1
    \end{align*}

この最後の式から、x=-1, y=2が不定方程式の解です。

この例では係数が小さいので割り算(検算)の式を2つしか使いませんでした。 そのため、代入は1回ですみましたが、係数が大きい場合は数回の代入が必要になることもあります。

  • 割り算の下から2番めの式をベースにする
  • あまりの代入は下から3番めの式から上の式に向かって順番に行う

この方法はアルゴリズムが確定しているので、かならず答えにたどり着くことができるのですが、代入の計算は結構わかりにくく慣れが必要です。 わかりにくい原因は代入後変形が必要なところですが、それを公式化してわかりやすくすることが考えられます。

互除法利用の解法を使いやすくする

さきほどの方法を分析してみます。 互除法の途中の検算の式を

 a+b\times(-q)=r

となっていたとしましょう。 aとbは互除法の途中に出る数字(aは前の式の「割る数」bは「あまり」)で、qとrはこの式における商とあまりです。 下から代入を繰り返して、この式のひとつ下まで来ていたとします。 そのときの方程式が

 bu+rv=1

となっていたとしましょう。 あまりrを代入すると


    \begin{align*}
    bu+\{a+b\times(-q)\}v=1\\
    av+b(u-qv)=1
    \end{align*}

ですので、代入の結果得られる解はx=vとy=u-qvになります。 これを公式とみなして、順にあてはめていけば良いのです。 例をやる前に、今の公式をまとめておきます

  • a÷b → 商がqあまりがr → av+b(u-qv)=1 (下の式から得られる式) → 解は x=v, y=u-qv
  • b÷r → ・・・・・・・ → bu+rv=1 (下から順に得られた式) → 解は x=u, y=v

例として、7x+4y=1を解いてみます。 はじめに左から2カラム目まで(商とあまりの計算まで)行い、一番右のカラムは下から上に戻ってきます。

  • 7÷4 → 商が1あまりが3 → x = -1, y=1-1*(-1)=2
  • 4÷3 → 商が1あまりが1 → x = 1, y=-1
  • 3÷1 → 商が3あまりが0 → 関係ないので無視

これから、x=-1, y=2が解です。

さらに複雑な方程式でやってみましょう

 723x+431y=1
  • 723÷431 → 商が1あまりが292 → x = 31, y=-21-1*31=-52
  • 431÷292 → 商が1あまりが139 → x = -21, y=10-1*(-21)=31
  • 292÷139 → 商が2あまりが14 → x = 10, y=-1-2*10=-21
  • 139÷14 → 商が9あまりが13 → x = -1, y=1-9*(-1)=10
  • 14÷13 → 商が1あまりが1 → x = 1, y=-1

答えはx=31、y=-52です。

 723\times 31+431\times (-52)=1

慣れればできるけれど、やり方を忘れてしまいそうですね。 これに比べて、次の合同式を使う方法は覚えやすいものです。

合同式を使う方法

合同式についてはネット情報などを参考にしてください。 高校では習いませんが、それほど難しくないです。 おそらく授業2コマくらいで基本は十分教えることができます。

合同式ではまず、法を決めます。 3を法にしてみましょう。 このとき、3で割ったあまりが等しい数は合同といい、

 x\equiv y \pmod{3}

と書きます。 法が、書き手と読み手で了解済みであれば省略されます。 たとえば、4と7は両方とも3で割ったあまりが1なので合同です。

 4\equiv 7

合同式はイコールとほぼ同じように和、差、積を行えます。 両辺に同じ数を加えたり、引いたり、掛けたりしても合同は保たれます。 たとえば、さきほどの4と7に10をかけて

 40\equiv 70

です。実際に3で割ったあまりは両方とも1ですから。

不定方程式は合同式で表せます。

 723x+431y=1

これは次のようになります。

 723x\equiv 1 \pmod{431}

なぜなら、431yの部分は431の倍数、すなわち割ったときのあまりが0ですから、723x+431yを431で割ったあまりと723xだけを431で割ったあまりは等しいからです。

また、431xは431で割り切れるので、0に合同ですから

 431x\equiv 0 \pmod{431}

この2つの式から、互助法のように割り算の検算のようなことをしていきます。

2つの式で引き算をする。以下modの部分は略します。

 292x\equiv 1

今の式と、そのひとつ前の式の差を取る。

 139x\equiv -1

一つ前の式から今の式の2倍を引く。

 14x\equiv 3

一つ前の式から今の式の9倍を引く。

 13x\equiv -28

差をとる

 x\equiv 31

これで、x=31が得られました。 元の方程式に代入して、y=-52を得ることができます。

合同式の方法は新たに公式を暗記しなくてすみます。 分かりやすい方法なので、一番お勧めです。