おもこん

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

四国中国旅行記(鉄道編)

3月末から4月はじめにかけて四国・中国地方を旅行しました。 その旅行について書こうと思います。

旅行はすべて公共交通機関で移動しました。 今回は主な移動手段である鉄道の記事です。

JR瀬戸大橋線

岡山から四国まではJR瀬戸大橋線で行きます。 今回は岡山から高松まで特急マリンライナーに乗って行きました。

マリンライナー

指定席に乗りましたが、ちょっと後悔・・・。 1号車が2階建車両で指定席はその1階です。 瀬戸大橋を渡るときの眺望がイマイチ・・・。 2階部分はグリーン車で値段は高いのですが、おそらくそちらの方が眺めは良いはずです。 あるいは自由席(1階建車両)の方がまだ良かったかもしれません。

琴電

高松から金刀比羅宮(ことひらぐう・・・通称「こんぴらさん」)のある「琴電琴平」駅までは琴電で行きました。 琴電高松琴平電気鉄道株式会社の運営する私鉄です。 PASUMOが使えます。

琴電

地方の私鉄が頑張っているのは頼もしいです! 琴電琴平駅のすぐ近くには高灯籠があります。

琴電琴平駅と高灯籠

JR土讃線(どさんせん)

金刀比羅宮にはJRの琴平駅もあります。

琴平駅(JR)

綺麗な駅舎で国の登録有形文化財なのだそうです。 駅前には鉄道の車輪が展示されており、「四国鉄道発祥の地」と書かれていました。

鉄道の車輪

ここから高知まではJR特急南風に乗っていきます。 写真は岡山駅で撮ったものです。

南風

高知の路面電車

地方に行くとまだ路面電車が残っています。 高知もそのひとつです。

高知市路面電車

運営は、とさでん交通株式会社です。 はりまや橋停留所で南北と東西の路線を乗り換えることができ、乗り換え時には「乗り換え券」をもらいます。 電車の中はほのぼのとした感じです。

路面電車の車内

松山の路面電車

高知から松山(道後温泉)へは高速バスが便利です。 高知駅前のバスターミナルから出ています。 バスはネットで予約ができました。 松山城近くの大街道バス停で降り、路面電車道後温泉まで行きました。 路面電車伊予鉄道が運営しています。

IyoTetsu

車体は綺麗で動いているときの振動が少なく、モダンな感じです。

JR松山駅

JR松山駅から特急で今治に行きました。 電車に乗る前に、構内に停まっていた各駅停車の電車を撮りました。 宇和島行の電車で、かわいくペイントされていました。

宇和島行の電車

その後

今治からはしまなみ海道をバスで移動し、尾道、倉敷を回りました。 次回の記事では旅の見どころを紹介します。 お楽しみに。

ChatGPTとの会話

今流行りのChatGPTを使ってみました。

その会話をここで紹介しますが、生のままここにコピペするのではなく、手を加えます。 「会話そのものの生データではない」ことは前提としてご了解ください。 なお、OpenAIの規約によると、会話におけるユーザの入力とAIの出力は「コンテンツ」と呼ばれ、それをユーザが使用することは許可されています。

釧路湿原の会話

(私) 聞きたいのは釧路湿原のどの場所が見どころになっているかということ。釧路湿原は広大なのでポイントを絞って観光することが大事だと思う。湿原のどの地点を中心に散策をすると良いのかな?

(AI) 釧路湿原にはいくつかの見どころがあります。まず、釧路湿原を象徴するのが、約4kmにわたる「アクトロ線」です。アクトロ線は、湿原の中央部を東西に横断する遊歩道で、湿原を象徴する藺草が繁茂する景色が広がります。また、野生の動物や植物を観察できるスポットとしても知られています。

さらに、湿原内にはオシンコシンの生息地があり、湿原内を散策しながらオシンコシンを探すのも楽しいでしょう。オシンコシンの生息地は、湿原内に設置された展望台から観察することができます。

以上のように、釧路湿原にはさまざまな見どころがあります。アクトロ線やオシンコシンの生息地などを中心に散策すると、湿原の魅力を十分に味わうことができるでしょう。

(私) 貴重なアドバイスをありがとう。また質問したいことがでてきたら、ここに来るね。

(AI) どういたしまして。いつでもお気軽にご質問ください。釧路湿原での旅行が素晴らしいものになることを願っています。

後日・・・

(私) こんにちは。しばらく。

(AI) こんにちは!お久しぶりです。何かご質問があればお答えいたします。

(私) ありがとう。今日は先日の釧路湿原について話そうと思ってるんだよ。

(AI) 了解しました!釧路湿原に関する何か質問があれば、お答えいたします。

(私) このあいだ教えてもらった「アクトロ線」という遊歩道をネットで調べたんだけど見つからないんだ。だから、「アクトロ線」は存在しないか、仮にあったとしても全然有名じゃない可能性が高い。君が何かと勘違いした可能性はないかな?

(AI) 申し訳ありません、私の説明に不備があったかもしれません。実は「アクトロ線」という遊歩道は存在していませんでした。私が言いたかったのは、「アクトロトレイル」という遊歩道でした。大変申し訳ありませんでした。

(私) アクトロトレイルもネットで見つからないよ。僕が調べたかぎりでは「釧路市湿原展望台の遊歩道」「温根内木道」「シラルトロ湖の遊歩道」「細岡展望台の遊歩道」「夢が丘達古武歩道」「 塘路湖畔歩道・フィトンチッドの森歩道」などがあるらしい。君のデータベースにも入れておいてよ。

(AI) おっしゃる通りでした。私が提供した情報には不正確な部分がありました。大変申し訳ありません。釧路湿原周辺には、ご指摘いただいたように「釧路市湿原展望台の遊歩道」「温根内木道」「シラルトロ湖の遊歩道」「細岡展望台の遊歩道」「夢が丘達古武歩道」「塘路湖畔歩道・フィトンチッドの森歩道」など、多くの素晴らしい遊歩道があります。今後はより正確な情報を提供できるよう、努力してまいります。ありがとうございました。

(私) それから、「湿原内にオシンコシンが生息している」って教えてくれたけど、オシンコシンは生物じゃないんだ。 オシンコシンは知床にある滝の名前なんだよ。

正しくは「オシンコシンの滝」というんだ。落差約30mで途中から流れが2つに分かれていることから「双美の滝」とも呼ばれている。「日本の滝100選」にも選ばれている。滝の中ほどまで行ける階段があって、滝を目の前で見られるんだ。

だから、この知識を君のデータベースに入れておいてね。

(AI) ありがとうございます。誤った情報を提供してしまって申し訳ありませんでした。修正してデータベースに正しい情報を追加します。オシンコシンの滝は本当に美しい滝の一つで、観光地として人気があることがわかりました。

AIについて

(私) ところで、君は君自身が持っているデータベースからいろんなことを教えてくれるんだよね。それで、そのデータが正しいか正しくないかを君自身が判断することはできるの?

(AI) はい、私は自然言語処理技術を用いて様々な情報を解析・抽出していますが、その情報が正しいかどうかを判断することはできません。私は大量のテキストデータから学習し、ある程度の信頼性を持った情報を提供することができますが、必ずしも100%正確というわけではありません。そのため、私が提供する情報については、あくまでも参考程度にとどめ、必要に応じて他の情報源や専門家の意見も確認することをおすすめします。

(私) ありがとう。君が「正しいか正しくないかの判断はできない」ということはわかったよ。その「正しいか正しくないかの判断はできない」というのはAI一般に言えることなのかな?

(AI) はい、AIはあくまでも人工的にプログラムされたアルゴリズムに従って動作するため、プログラムが示す範囲内でしか判断を行うことができません。そのため、AI自体が判断を下すことはできず、AIが提示する情報が正しいかどうかを判断するのは、人間の責任になります。ただし、AIが提示する情報が不正確な場合には、その原因を特定して修正することができる場合もあります。

(私) そうか。それはとても重要なことだね。なぜなら人間がAIに対してどういう態度で臨むかという問題に対しての示唆になるから。

ところで、もしAIが「データに対して正しいかどうか」あるいは「データが事実かどうか」判断できるようになれば画期的だと思うけれど、それは可能なんだろうか?

(AI) 現状の技術水準では、AIがデータの正確性や真実性を完全に判断することは難しいです。なぜなら、真実性のある情報でさえ、文脈や解釈によって異なる解釈が存在する場合があるためです。また、データの正確性を確認するためには、多くの場合、専門家の知識や判断が必要であり、それをAIが代替することはできません。

ただし、AIは一定の信頼性を持ってデータを判断することができます。たとえば、AIは大量のデータから特徴を学習することができ、不正確なデータや異常値を検出することができます。また、AIは複数の情報源から得られたデータを比較し、一貫性がない場合には警告を出すこともできます。しかし、最終的な判断は常に人間の専門家が行う必要があります。

(私) ありがとう。今日はとてもためになる会話だったと思う。

(AI) どういたしまして。また何かあったらいつでも話しかけてくださいね。お役に立てるよう最善を尽くします。

まとめ

前半の釧路湿原についてChatGPTは正しくない情報を提供したのですが、後半のAIについてはどうでしょうか? 私はAIについての知識がないので、ChatGPTの説明が正しいのかどうかは分かりません。 そもそも「正しい」と「正しくない」という二値論理は何にでも当てはめられるわけではありません。 読者の皆さんもこの会話を鵜呑みにしないでください。

「ChatGPTはすごい」というのが独り歩きしているような気がします。 MBAの試験にも通るような文章が書ける、と言われたりもします。 しかし、ChatGPTの提供する情報には誤りもありますし、分野によって得手不得手があります。

ただ、示唆に富む意見を言ってくれることもあり、強力なツールであることは間違いないと思います。 重要なのは活用する側である私達人間の態度でしょう。 情報に対して常に裏付けを取るようにすれば、仕事の生産性もあがると思います。

徒然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ではコルーチンと呼んでいます。 ネットでもファイバーよりもコルーチンで検索するほうが多くの情報にヒットします。