おもこん

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

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

3月末に四国を旅しました。 高松(香川県)、高知(高知県)、道後温泉松山市愛媛県)です。

高松

まず駅前の玉藻公園高松城跡)です。

玉藻公園

写真奥に見える建物は艮櫓(うしとらやぐら)です。 この公園は高松城の跡地ですが、天守閣は残っていません。 しかし、石垣は見事ですし、松がとても美しい。 お掘りを小舟で巡る「城船体験」もあり、楽しめます。

次に有名な栗林公園(りつりんこうえん)です。 栗林公園高松駅から少し離れています。 高松駅近くの「高松築港」駅から琴電で3駅先の「栗林公園」駅で降りるとすぐです。 非常に大きな日本庭園です。 東京にも庭園はたくさんありますが、おそらく栗林公園の方が広いと思います。

栗林公園

私が訪れた3月末はちょうど桜が満開でした。 観光客だけでなく、(おそらく地元の)花見客もたくさん来ていました。 japantravelwithbonbonさんのインスタグラムがちょうど同じ時期に四国を旅行していたようで、ナイスな写真が載っています。 下のリンクをクリックして、他の写真も見てくださいね。 (インスタのアカウントを持っていなと見ることができないかもしれません)

園内には茶店売店があります。 この写真の売店は昭和のイメージで面白いと思い、シャッターを切りました。

栗林講演の売店

ソフトクリームは売店の定番で、四国の観光地には必ずあります。

高松では屋島にも行きました。 屋島は昔は島だったそうですが、今は陸続きになっています。 屋島はテーブル上の高地でその上からは瀬戸内海や高松市を見ることができます。 また、水族館など楽しめる施設もあります。

ヤシガニ

写真は水族館にいたヤシガニです。 これには思い出があって、思わず写真を撮りました。 私は5年ほど前にソロモン諸島という国に行きました。 そこの市場ではヤシガニを売っています。 いつもというわけではなく、ヤシガニが捕れたときに市場に並ぶのです。 味は日本で食べるカニに似ています。 ソロモンにいるうちに一度は食べたいと思っていましたが、あるとき市場で見つけ、すぐに買って食べました。 料理方法は簡単で煮て火が通ればOKです。 ただ、一部食べられない部分があるので気をつけなければいけません。

ところで、石垣島ではヤシガニを食べられると聞いたことがあります。 ヤシガニを試したい方は石垣島を調べてみてください。

金刀比羅宮(ことひらぐう)

こんぴらさんで有名な金刀比羅宮は高松から西南西に約30kmの琴平町にあります。

金刀比羅宮への標識

琴電琴平駅を降り、すぐ前の道路を右に200mほどでこの道標が現れます。 左折して400メートルほどで右折し参道に入ります。 ここからが大変で700段あるといわれる階段を登らなければなりません。

金刀比羅宮参道

頑張って御参りを済ませ、帰りにソフトクリームを食べました。

ソフトクリーム

これは「おいりソフト」といいます。 丸い「おいり」は甘くて独特の舌触りです。 食べると幸せになる、といわれているらしいです。

高知

高知県、土佐といえば坂本龍馬ですね。 高知駅には坂本龍馬をはじめ3人の志士の像が立っています。

高知駅前の銅像

高知からバスに乗って桂浜に行きました。 そこにも坂本龍馬の像があります。

坂本龍馬

坂本龍馬は全国的に有名ですから、高知県(土佐)の人にとって名誉なことでしょう。

桂浜には面白いオブジェがありました。

桂浜のくじら

これは植物で作ったくじらのオブジェです。 期間限定らしく、4月23日まで、とのことです。

高知市内に引き返し、高知城に行きました。

高知城

天守に登ると市内を一望できます。 また、お城の敷地内の桜が見事で、多くの人で賑わっていました。

翌日は高知県立美術館に行きました。

高知県立美術館

建物のデザインは土佐漆喰の土蔵をモチーフとしたものだそうです(ウィキペディアより)。 建物だけでなく、その周りの庭はお掘りのように水が流れて美しいです。 私が訪れたときは空いていて本当にリラックスできました。 当日はシャガールの展示などがありました。 ここは高知の穴場スポットとしてオススメです!

道後温泉、松山、今治

高知から道後温泉までは高速バスが便利です。 ネットで予約することができます。 道後温泉で楽しみにしていたのは、道後温泉本館を見ることでした。 残念なことに保存修理をしていて、全体が覆われていました。

道後温泉本館」保存修理中

道後温泉は温泉だけでなく、土産物屋がたくさんあり、活気がありました。 一度は訪れてみたい場所です。

松山の滞在時間はあまりなかったのですが、松山城に行きました。 お城は市内中心部の山の上にあり、リフトまたはロープーウェイで行くことができます。 お城にリフトで行くというのは面白いですよね。 残念ながら閉館時間を過ぎて天守には登れませんでした。

JR松山駅のすぐ前に「非核平和宣言都市」のモニュメントがありました。

松山市の非核平和宣言都市モニュメント

被爆国日本においてこのような宣言とモニュメントがあるのは大事なことだと思います。 なお、このような宣言をしている都市は全国にたくさんあります。 宣言だけでなくこのようなモニュメントを設置すると、宣言の周知にもなって良いことです。

JRで今治まで行き、そこから尾道までしまなみ海道をバスで移動しました。 瀬戸内海に入る手前に今治造船の工場があります。

今治造船

たくさんクレーンがあり、独特の景観を作っていました。

これよりしまなみ海道を渡って中国地方に入りますが、その話題は次回の記事に書く予定です。

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

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.

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