yunomuのブログ

趣味のこと

JPEGを読む(2) ハフマン表定義(DHT)

JPEGを読む - yunomuのブログ これの続き。

JPEG仕様(PDF) https://www.w3.org/Graphics/JPEG/itu-t81.pdf

 Figure B.16, B17あたりにフィールドが並ぶ位置のルールが書いてあるけど、これは真面目にパーサーを書くとして。

 まずはDHT(Define huffman table(s): ハフマン表定義)を読みたい。ハフマン符号そのものや符号化のアルゴリズム自体は知っていても、実際に符号表をどう定義するのかは意外と知らなかったりする。単純に考えると値と符号の組を連ねればいいのだけど、データ量削減のための符号化でそんなナイーヴなことをしているはずはない。

 定義を見ると非常に単純だ。1つのDHTにいくつかの表が含まれていて、それぞれは用途を示すTc, Thで識別されている。1つの表は2つの部分で構成される。それぞれLiVi, jで表現される。

  • Li (1 ≦ i ≦ 16): 長さがiビットの符号語の数
  • Vi, j (0 ≦ jLi): 長さがiビットでj番目の符号語が表現する値

 これだけで用は足りるらしい。言われてみればハフマン符号は極めて規則的なので、Liさえあれば出現する符号語のリストを作ることができる(と、わかった風に書いたがこれを理解するのに2〜3日かかった)。文書の中ではBITSと呼ばれている。Vi, jはそのリストに対して順に値を割り振るためのリストで、文書の中ではHUFFVALと呼ばれている。

 ここからHUFFSIZE/HUFFCODEと最終的な値の表を構築していく。それぞれの意味は

  • HUFFSIZE: k(0 ≦ klength(HUFFVAL))番目の符号語の長さを表すリスト
  • HUFFCODE: k(0 ≦ klength(HUFFVAL))番目の符号語のリスト

 なので最終的には「符号長」「符号語」「値」の3つ組の表ができることになる。Go言語で表現すると以下のコードで、最終的な目的であるhufftableを作っていくためにsizecodevalueといったフィールドを順に埋めていく作業をする。Tc, Thも一旦ここに格納しておく。

type huffcode struct {
  size  int
  code  uint16
  value uint8
}

type hufftable struct {
    class uint8 // Tc
    target uint8 // Th
    huffcodes []*huffcode
}

 前提として、JPEGのハフマン符号では符号長の最大は16ビットで、値は8ビットであると定義されている。hufftableの大きさも、valueの数だけエントリがあるわけなので当然length(HUFFVAL)と等しくなる。LVからこの表を作るプロセスはフローチャートになっている。

 まずはHUFFSIZEから。(これもわかったように書いているが理解に1日かかっている)

 これはつまりBITS(符号長iビットの値の数のリスト)が以下のようだった場合、

BITS=[0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]

FUFFSIZE(k番目の値の符号長(size)のリスト)は以下のようになるはずだ。

[2, 3, 3, 3, 3, 3, 4, 5, 6, 7, 8, 9]

 BITS(1)=0なので1ビットの符号は0個、BITS(2)=1なので2ビットの符号は1個、BITS(3)=5なので3ビットの符号は5個、BITS(4)=1なので4ビットの符号は1個、...、というのを表現したのが上のリストになる。

 これをhuffcodeのリストとして表現すると以下のコードになる。

func makeHufftable(class,  target uint8, bits [16]uint8, huffval []uint8) []*huffcode {
    var huffcodes []*huffcode

    // HUFFSIZE
    for i, l := range bits {
        for j := 0; j < int(l); j++ {
            huffcodes = append(huffcodes, &huffcode{
                size: i + 1,
            })
        }
    }

 BITSの値の合計値が12なので、HUFFSIZEの長さも12になる。フローチャートの通りに実装するなら最初にHUFFSIZEの長さを計算するとよさそう。

 次に、hufftablecodeの値、つまりHUFFCODEを埋めていく。フローチャートはこう。

 これが実装は簡単なんだけど意味の理解が結構難しい。符号語のルールとして「すべて"1"の符号語は他の符号語の接頭辞でなければならない(the codes shall be generated such that the all-1-bits code word of any length is reserved as a prefix for longer code words)」というものがあるので、これのおかげで符号長毎の符号語の数のリストだけで符号語のリストを作ることができる。

 フローチャートを読み解くと以下のようになる。

  • 最初の符号語は0 (ビット長にかかわらず)
  • 符号語Ck-1のビット長とCkのビット長が同じ場合、Ck=Ck-1+1
  • 符号語Ck-1ビット長とCkのビット長が異なる場合、Ck=(Ck-1+1)<<1

 これをコードで表現するとこう。1ビットシフトを繰り返す部分はまとめてnビットシフトするようにした。本当はフローチャートのまんま書いた方がいいんだけど(実際は2種類実装してテストしました)。

    // HUFFCODE
    var code uint16
    prev := hufftable[0]
    for _, huffcode := range huffcodes[1:] {
        code++
        if prev.size != huffcode.size {
            code <<= huffcode.size - prev.size
        }
        huffcode.code = code
        prev = huffcode
    }

 最後に出来上がった符号語リストにHUFFVALの値を割り当てていく。

 アルゴリズムはこうだけど、hufftableの値が既にHUFFVALと同じ順序で並んでいるので簡単でいい。

    // Order_codes
    for i, v := range huffval {
        huffcodes[i].value = v
    }   

    return &hufftable {
        class: class,
        target: target,
        huffcodes: huffcodes,
    }
}

 これを並べると以下のような対応表が出来上がる。今回はたまたまvalueがindex値と同じになっているけど、当然出現頻度によってここの順番はバラバラになる。

値(value) 符号語長(size) 符号語(code) 符号語(10進数表記)
0 2 00 0
1 3 010 2
2 3 011 3
3 3 100 4
4 3 101 5
5 3 110 6
6 4 1110 14
7 5 11110 30
8 6 111110 62
9 7 1111110 126
10 8 11111110 254
11 9 111111110 510

 これで8bitの値のリストをハフマン符号化できそうな気がしてきた。

JPEGを読む

 画像や動画のエンコード/デコードの理屈はわかるけど実際どうなっているかはよくわかってないのでとりあえずJPEGファイルの仕様を調べてみる。

資料 https://www.w3.org/Graphics/JPEG/itu-t81.pdf

 JISの日本語の資料もあったけど(JISX4301)、閲覧が面倒くさすぎて英語で読む方がまだマシであった。

 上から読んでいっても理解できなさそうなのでJPEGファイルのフォーマットから読みつつ、実際にJPEGファイルを読むコードも書いていく。"Annex B: Compressed data formats"から。

 基本的にはデータやパラメータやハフマン符号のテーブルやらのフィールドが並んでいて、それぞれのフィールドは先頭2バイトのMarkerと呼ばれるマジックナンバーで判別できるようになっている。それでフィールドの中にヘッダやペイロードがあったりなかったりして、最後に終端のMarkerがついて終わり。

 このMarkerというのは必ず0xFFから始まる2バイトで、Annex Bの最初の方に全部の定義が書いてある。JPEGファイル内で0xFFから始まる2バイトはMarkerしかなくて、データ内に偶然に出現する0xFF0xFF00エスケープされる。ということらしい。

 それならとりあえずMarkerだけを取り出して並べて見るとなんとなくの構造がわかりそう。こんなかんじで↓

package main

import (
    "bufio"
    "io"
    "log/slog"
    "os"
    "strconv"
    "strings"
)

func marker(i uint64) string {
    return "0xFF" + strings.ToUpper(strconv.FormatUint(uint64(i), 16))
}

func main() {
    r := bufio.NewReader(os.Stdin)

    for {
        b, err := r.ReadByte()
        if err == io.EOF {
            slog.Info("FINISHED")
            return
        } else if err != nil {
            slog.Error("ReadByte", "err", err)
            return
        }

        if b != 0xFF {
            continue
        }

        m, err := r.ReadByte()
        if err == io.EOF {
            slog.Error("unexpected EOF")
            return
        } else if err != nil {
            slog.Error("read marker", "err", err)
            return
        }

        if m == 0x0 {
            continue
        }

        slog.Info("marker", "val", marker(uint64(m)))
    }
}

 これで適当なファイルを読む。

 go run main.go < test.jpg
INFO marker val=0xFFD8
INFO marker val=0xFFDB
INFO marker val=0xFFC0
INFO marker val=0xFFC4
INFO marker val=0xFFDA
INFO marker val=0xFFD9
INFO FINISHED

 意外と少ない。これを表B.1と見比べてみると

  • 0xFFD8 Start of image : ファイルの先頭
  • 0xFFDB Define quantization table(s) : そのまんま、量子化係数のテーブル
  • 0xFFC0 Baseline DCT : SOF(start of frame)。圧縮の方式はbaseline(progressiveやlosslessじゃないやつ)で、non-differentialで離散コサイン変換(DCT)。
  • 0xFFC4 Define Huffman table(s) : これもそのまんま、ハフマン符号表。
  • 0xFFDA Start of scan : Figure B.2からみるとここに符号化データが入ってる。
  • 0xFFD9 End of image : ファイルの終端

 ハフマン符号化は元データのヒストグラムに偏りがある方が圧縮効率が良くなるので、元画像の特性によってアルゴリズムを変更できるようになっている。

 次はこの中のどれかのフィールドの中身を見ていこう。

スケジュール帳に何を書くか

 大抵のものには日付や時刻のデータが付随しているので全部スケジュールとしてカレンダーに載せたくなる。けどスケジュール帳に載せづらいデータというものがたくさんある。

 会議や飲み会の予定は明らかで、カレンダーのその日付に書き込んでもGoogleカレンダーなどのアプリに登録しても何の違和感も無い。

「夏休み」「冬休み」といった予定は、予定ではあるものの範囲があるし、その日になにかをすると決まった予定ではないのでカレンダーに書くと少し面倒くさい。土日祝日のようにあらかじめカレンダーに書かれている通りに休暇があるならいいけど、そうでない休暇も予定として書くには違和感がある。予定の重複を警告してくるタイプのスケジュールアプリだったりするととても面倒くさい。そもそも予定は重複するし運用するものだろう。今は関係ないけど。

 休暇などは、やることではないので会議の予定などとはちょっと性質が違う。

 TODOは、やることのリストであり、日付はあったりなかったりする。このうち日付のあるものはカレンダーに反映したくなるが、TODOにおける日付というものはおおむね締め切りであることが多く、必ずしもその日にやる必要はないことがある。前日にやってもいいし、1週間前でもいい。そういう意味では締め切り付きのTODOは今日の予定でもありうる。締め切りが過ぎたからといって消えないでほしいTODOもある。

 TODOの範囲がある場合もある。何かのチケットや商品の予約開始日と終了日がある場合、予約開始日まではTODOではなく、終了日を過ぎると無効になる。

 映画の公開日のように、公開後にいつかやればいいタイプのTODOもある。日時を決めた段階でスケジュールになるし、たまたまいいタイミングで映画館に行けたらそれはそれで完了になる。

 このようにカレンダーの側からTODOを見ると、範囲を持っているものが多く、カレンダーのビューで表示すると見づらい。逆にTODO側から見ると、今日の予定も締め切りのあるTODOも無いTODOも今やるべきことリストとして並んでいても違和感は無い。ついでに明日からやるべきことも並んでくれていい。

 ここまではビューの切り替えだったり、カレンダーとノートの間の転記でなんとかなる気がする。

 曖昧な予定のようなものというのが結構ある。

 やることといっても厚みがあるものがTODOとして並んでいるとちょっとやりづらい。厚みというか時間で、人によって感覚は違うかもしれないけど半日かかるものはそれはもうTODOではなく予定を立てなければならない。けどいつでもいい。予定を立てるTODOを作りたい。みたいなことが結構ある。歯医者から定期検診のおはがきが来た場合などがこれにあたる。

「だいたいこの日までに機能を作りたい」けど作るのに3日かかる。こういうのはもうプロジェクト管理だ。

「問い合わせしたら『月曜日まで待って』って言われた」という時、これって月曜日開始のTODOとして管理するの? 返答如何でその後に何かをやらなきゃいけないとか言いだすと、これもTODOを作るためのTODOという気がする。

「お祭りあるけど行っても行かなくてもいい」「ゲームの発売日だけどその日以降ならいつ買ってもいい」というような、予定やTODOというほどではないんだけど覚えておきたいようなもの。忘れてもまあいいか。

「毎年この時期に飲み会やってるよな」「隔月くらいで集まってるけど次回を決めるのをよく忘れる」「春になったら種を蒔こう」とかいうぼんやりと定期的なもの。毎年やる飲み会では毎回最初に「去年は死ぬほど暑かったけど今年は涼しいね」などその日の気候の話題で盛り上がるので、ぼんやりとした周期を考えると二十四節気みたいに細かく名前をつけるのは良いアイデアな気がする。冬至のかぼちゃとか。

 思いつくまま書いてみたけど、このあたりの概念を分類したり名前つけたりして、漠然としたスケジュールアプリに対する不満が整理できないものかなと考えていた。

 それでとりあえず毎年送られてくるけど使っていなかった紙の手帳を試しに久しぶりに今年は使ってみようかなと思って使いはじめたところ。どこに何を書いたら便利なのかまだよくわからない。

Rust yew の function_component が動かない

 yewで遊ぶぞーとなって一番手こずったところ。

 yew 0.20.0ではyew::start_appが無くなっているので、yewの(古い)チュートリアルにあるコードはそもそも動かない。

 かといってdocsにあるExampleもそのままでは動かない。具体的にはyew::Renderer:<App>::new().render();の部分で could not find `Renderer` in `yew`と言われる。

docs.rs

 そのものズバリなissueで答えが出てた。

github.com

 Cargo.tomlファイルのdependenciesのyewの項目にfeaturesを指定するとよい。

yew = { version = "0.20.0", features = ["csr"] }

 よく見るとyewのdocsのRendercsrというfeaturesのタグが貼ってある。これそういう意味だったのか。つまりCargoのマニュアルをあまり真面目に読んでいなかったのが敗因。

 おまけ。function_componentとwasm_bindgenで動かす時のHelloWorldのサンプル:

src/lib.rs

use wasm_bindgen::prelude::wasm_bindgen;
use yew::prelude::{function_component, html, Html};

#[function_component(App)]
fn app() -> Html {
    html! {
        <div><h1>{"Hello, World!"}</h1></div>
    }
}

#[wasm_bindgen(start)]
pub fn run_app() {
    yew::Renderer::<App>::new().render();
}

Cargo.toml

[package]
name = "yew-app-fn"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
wasm-bindgen = "0.2.83"
yew = { version = "0.20.0", features = ["csr"] }

MSYS2でRuby開発環境を作る

 ウィルスバスターその他いろいろに阻まれてrubyinstaller+DevKit(MSYS2)のインストールに失敗したみなさん。いっそMSYS2そのものをインストールしてしまいましょう。64bitのWindowsの7以降を対象にしています。10ならだいたいOKだと思います。

www.msys2.org

 英語だけどがんばって。Installationの節を順番に実行していきます。

  1. ダウンロードします。ダウンロードしましょう。
  2. ダウンロードした"msys2-x86_64-XXXXXXXX.exe"を実行します。XXXXXXXXのところはなんか数字が入ります。1でダウンロードしたファイルです。
  3. 画像のとおり、インストール先を選びましょう。だいたいそのままでOKです。
  4. インストールしたら"Run MSYS2 now."をチェックして終了しましょう。
  5. なんかウィンドウが出ますよね。こういうコマンド入力できる画面を「ターミナル」と呼びます。ターミナルエミュレーターの略です。まずコマンドpacman -Syuを実行します。pacmanは、パッケージ(ソフトウェアのに必要なファイル一式のようなもの)を管理するソフトで、だいたいスマホのAppStoreやGoogle Play Storeと似たようなやつです。無料なので安心してください。pacman -Syuは、全てのパッケージを更新チェックして更新するという意味です。例で出ている最後のエラーはだいたい「再実行してください」という意味です。これが出ると一旦ターミナルが終了します。
  6. スタートメニューか、見つからなければインストールしたフォルダの中から、"MSYS2"を実行してください。またターミナルが開くと思います。先程のようなエラーが出た場合はここでpacmanを再実行します。pacman -Suです。前のやつから"y"が一文字消えているのは、今回は更新チェックをしなくてよいからです。さっきしたからね。
  7. これでMSYS2使えるようになったよ(以下略)

 とりあえずここまでで良いでしょう。次はRubyをインストールします。

$ pacman -S ruby

 これでおわりです。今日の時点ではRuby 3.0.0がリリースされていますが、まだパッケージが更新されていないのでRuby 2.7.2-1が入るのではないかと思います。

$ ruby --version

 このように今どのバージョンが入っているかを確認して、適切なバージョンのマニュアルを見るようにしましょう。マニュアルはここでバージョンを選ぶことができます。

docs.ruby-lang.org

 一応、whichコマンドで、Rubyがどこにインストールされているかを見ておいてください。

$ which ruby
/usr/bin/ruby

 みたいになっていたら成功です。パスが/c/で始まっている場合は、CGIの例題が動かない可能性があるのでPATHなどを見なおす必要があります。くわしい人に相談してください。

CGIを書こう

 タイトル「インターネットになろう」にしようと思ったけどやめた。

 今回はちょっとWebをやってみます。「そろそろ何をやりたいか考えてみよう」と言いたいところですが、そんな簡単に見つかるなら幸せなことで、それはそれで良いのですが、普通はとりあえずできることを増やしながら「これ面白いな」と思うことを見つけていくというのが良いでしょう。

準備(Windows使いのプログラマ向け)

 まずはサンプルを動かしてみてほしかったのですが、Windowsだと肝心な部分がどうしても動かなかったのでちょっと下準備をします。

 WindowsのPCを使っていて、Rubyのインストールで「RubyInstaller」を使っていて、現在はコマンドプロンプト(Rubyといっしょにインストールされている"Start Command Prompt for Ruby"みたいなやつを含む)を使ってRubyを書いている人向けの情報です。

 おそらく最初に以下のようなページからrubyinstaller-devkit-2.7.2-1-x64.exerubyinstaller-2.7.2-1-x64.exeをダウンロードしてインストールしたのではないでしょうか。

rubyinstaller.org

 名前に"devkit"と入っている方のファイルがある人はそのまま、無い方のファイルの人はdevkitのファイルをダウンロードしてください。ファイルをなくした人もダウンロードしなおしてください。

 そして再度インストールしてください。ただし、今度は途中のモジュール選択の部分で"MSYS2"をインストールするようにチェックを入れてください。最初にインストールした時と同様にコマンドプロンプトのウィンドウが開いて色々質問されると思いますが、全部そのままEnterを押しておけば大丈夫です。

 インストールが終わったら、今までと同じようにコマンドプロンプトを立ち上げてください。そのあと、bashコマンドを実行してください。ちょっと見た目が変りましたね。bashは、おおむね今までのコマンドプロンプトと似たようなものですが、色んな機能があったりなかったりします。でも今までどおりcddirも使えるので安心してください。dirの代わりにlsも使えますし、もちろんrubyirbも使えます。

 では、ここからはbashを使っているという前提でいきます。

WEBrickを使う

 ここではWEBrickというWebサーバを使ってみます。Webサーバとはなんぞやというのはまあやってみればなんとなくわかるでしょう。マニュアルはこれまでとは違い「標準添付ライブラリ All libraries」のページにあります。ページには大量のライブラリがありますが、ネットワークの節の"webrick"のページを見てください。

library webrick (Ruby 2.7.0 リファレンスマニュアル)

 概要にサンプルコードが載っています。「以下は Web サーバとして完全に動作するスクリプトです。」とのことなので、とりあえずこれを適当なファイル名(ここではweb.rbとします)のファイルに打ち込むかコピペしてください。

require 'webrick'
srv = WEBrick::HTTPServer.new({ :DocumentRoot => './',
                                :BindAddress => '127.0.0.1',
                                :Port => 20080})
srv.mount('/view.cgi', WEBrick::HTTPServlet::CGIHandler, 'view.rb')
srv.mount('/foo.html', WEBrick::HTTPServlet::FileHandler, 'hoge.html')
trap("INT"){ srv.shutdown }
srv.start

 これを実行します。

% ruby web.rb
[2021-02-05 19:57:11] INFO  WEBrick 1.6.0
[2021-02-05 19:57:11] INFO  ruby 2.7.0 (2019-12-25) [x86_64-linux]
[2021-02-05 19:57:11] INFO  WEBrick::HTTPServer#start: pid=111395 port=20080

 このように、なにかメッセージが出てそのままプログラムが停止すると思います(細かい数字は違うかもしれません)。エラーが出たり終了してしまったら何か書き間違いがあるかもしれません。書き間違いは熟練プログラマでもよくあることなので注意深く探してみましょう。

 この状態で、Webブラウザを開いてURL http://localhost:20080 にアクセスしてみてください。ファイル一覧みたいなものが見えたら成功です。たぶんさきほど作ったファイル(web.rb)が見えているんじゃないでしょうか。ファイル名をクリックすると中身が見られます。インターネットっぽくなってきましたね。

 終了する時はコマンドの画面に戻ってC-Cです。サンプルコードの中の最後から2行目trap("INT"){ srv.shutdown }の部分が、C-Cを押したら終了するという意味になっています。書かないと終了できなくて不便です。ウィンドウ閉じたら終了したりするんだろうか? よくわかりません。別に試さなくてもよいでしょう。最悪PCを再起動すれば終了します。

ファイルにアクセスする

 まず、/hoge.htmlにアクセスします。URLは http://localhost:20080/hoge.html です。"Not Found"のような文字が表示されたと思います。それで正常です。つまり「"hoge.html"というファイルはありません」ということです。実際に無いので、これはこれで正しい。

 では"hoge.html"というファイルを作ってみましょう。何か適当なことを書いて、現在ソースを置いているのと同じフォルダに"hoge.html"という名前で保存します。内容が思いつかない人はこうしましょう。

You just don't have enough fight in you!

 ファイルを保存して再度、http://localhost:20080/hoge.html にアクセスしてみましょう。「You just don't have enough fight in you!」または適当に書いた文字が表示されていれば成功です。文字化けした人は、アルファベット以外を書きましたね。このように、Webサーバはファイルをブラウザに配信することができます。HTMLが書けるならHTMLを書くともうちょっとそれらしくなります。文字化けした人は、HTMLでエンコードを指定するか、ブラウザの設定でエンコードをいじるか、なんかやってみてください。

 これだけでも何かと便利なので覚えておくと良いでしょう。

 次に、/foo.htmlにアクセスします。URLは http://localhost:20080/foo.html です。/hoge.htmlにアクセスした時と同じ結果になったと思います。なっていない人は、コードのどこかを間違えていると思います。がんばって間違いを探しましょう。この間違いさがしを専門用語でデバッグとも言います(デバッグはもっと広い意味での間違いさがしですが)。

 同じ結果になるのは、コードの中のsrv.mount('/foo.html', WEBrick::HTTPServlet::FileHandler, 'hoge.html')の行の効果です。これで「"/foo.html"にアクセスした時は"hoge.html"のファイルを返す」という意味になります。詳しくはWEBrickのマニュアルにありますが、難しいのでまた別に解説してもいいと思っています。話せば長くなります(たのしい)。

プログラムにアクセスする(CGI)

 本題です。ファイルにアクセスしてファイルが返る、または適当なパス(ここでは"/foo.html")にアクセスしてファイルが返るように、プログラムを実行して実行結果をブラウザに返すといったようなことができます。ものすごく簡単に言うと、このような仕組みをCGIと言います。

 まだ解説していない以下の行に注目してください。

srv.mount('/view.cgi', WEBrick::HTTPServlet::CGIHandler, 'view.rb')

 これは日本語訳すると「"/view.cgi"にアクセスされたら"view.rb"を起動してその結果を返す」という意味です。ここで出てくる"view.rb"はRubyのプログラムです。そんなものは存在しないので、現時点で http://localhost:20080/view.cgi にアクセスすると「Internal Server Error」みたいなのが出ると思います。Webサービスがエラーになった時にこのような画面を見たことがある人もいるかもしれません。つまりそういうことです。

 ということで、ファイル"view.rb"を用意します。内容は以下のようにしてください。

#!/usr/bin/env ruby

print "Content-Type: text/plain\r\n\r\n"

print "Ciao!"

 保存して http://localhost:20080/view.cgi にアクセスして、"Ciao!"と表示されれば成功です。print "Ciao!"の部分を好きなように改造して遊んでみるといいと思います。これまで使っていたputsも使えます。

 うまくいかなかった人は以下を試してみてください。うまくいった人にはすみませんが、ここまで含めてプログラミングです。

  • view.cgiのコードに間違いが無いか確認する
  • ブラウザの画面に出るエラーメッセージを確認して手掛かりを探す
  • ruby web.rbを実行した方のウィンドウで、最近出ているメッセージから原因を探す
  • 1行目に間違いが無いか確認する(1行目は絶対に#!で始まらないといけない)
  • 1行目の書き方を変える。C-Cbashに戻って、which rubyコマンドを実行する。実行した結果の文字列を1行目の"#!"の後に入れる。例えばwhich rubyの結果が/usr/bin/rubyだったら1行目は#!/usr/bin/rubyにする

 それでもわからなかった場合や、動いたけどなんで動いたのか気になった場合は、何をしたかも含めて文章でもスクリーンショットでも良いので添えて詳しい人に聞いてみてください。

 動いた人は、それがつまりWebサービスにつながる第一歩です。Twitterやブログなんかもここから始まっています。

倫理的なやつ

 ここで作ったプログラムには何の危険もありませんし、自分以外の誰にもアクセスできません。安全です。というのをわかった上で。

 これでなんらかのプログラムを使ったサービスをインターネットで公開できるようになりました。大抵はそのままではファイアウォールだったりネットワーク設定だったりに阻まれてそのままインターネットに公開というわけにはいきませんが、理論的にはこれで全世界に公開できるサービスを作ることができるようになったということになります。きっとそのために勉強をすればすぐにできるでしょう。

 ただし、ここから先、本当にインターネットに公開しようとすると色々な現実的な問題が発生します。今回はまだ外のシステムにアクセスするようなことはしていませんが、単に公開するだけでも、例えばどこか知らない誰かからの攻撃の危険にさらされます。プログラムに脆弱性があった場合、もしくはプログラムに非は無くとも利用しているWEBrickRubyそのものに脆弱性があった場合、そこを突かれてシステムが乗っ取られたり、不正アクセスのための踏み台にされることがあります。つまり直接的に誰かに迷惑をかけるだけでなく、知らないうちに悪事に加担してしまう恐れまである。それは非常にマズい。いわゆるハッキングとかクラッキングとかいうやつです。

 ということで、何か作って公開したいと思った時は、詳しい人に相談するか、インターネットセキュリティや倫理についてしっかり勉強するなどしてからやるようにしてください。

 実際のところ、悪事を働くための技術や守るための技術の理屈はすごく難しいですが、対策する方法自体はそんなに難しくないことが多いので、「これやっとけば大丈夫よ」くらいのアドバイスで済むことが多いです。

 ただそれはそれとして、クラッキングというやつの仕組みはそこそこ楽しいので、悪用しない前提で勉強するのは楽しいと思います。死ぬほど難しいけどそれなりの需要はあります。

それから

 今回はここで終わりです。今回はこれまでと違って説明を省いた部分が大量にあります。ここから先は、これまでのどこがおもしろかったかで進む方向を決めると良いでしょう。どの方向に行っても1歩目までは大抵サポートできると思います。

 ざっと思いつくのはこんな感じです。

  • もっと色々できるWebサービスを作りたい
  • ここで出てきたURLの意味がわからん(localhostとか:20080とか)
  • コードの中のWEBrick::HTTPServer.newのオプションは一体何(:DocumentRoot => './',とか:BindAddress => '127.0.0.1',とか:Port => 20080)
  • 文字化け直したい
  • trap("INT")の詳細が知りたい
  • WEBrickが何をしているのかを知りたい
  • そもそもrubyみたいなのを作りたい

 これ以外にも色々なことを思うかもしれません。ほとんどの共通部分は同じではありますが、特にどこが気になるかによってかなり分野が違っていたりするので、おすすめする本なども変わってくると思います。私も挙げた分についてはそこそこ知っているものもあるので聞いてみてもいいかもしれません。半分くらいRubyじゃないですけども。

2時間でわかる繰り返し

 今回は繰り返しの話です。

 繰り返しというのは要するに同じことや似たようなことを何回もやる、ということですが、そのやり方はたくさんあります。これはRubyだからということではなく、様々なプログラミング言語で様々な繰り返しの方法があります。その多数の中から状況によって適切なやり方を選択できるということが、プログラミングの上達につながるといって良いでしょう。

 ただしこれは非常に難しい。一流と呼ばれるプログラマでも間違えることが多々ありますし、正解が存在しないということもあります。

 なので、ここではとりあえずどういうパターンがあるのかを見て「そういうものもあるんだな」と思っておく、というところから始めましょう。

無限ループ

 まずは無限ループからです。無限ループというのはそもそもプログラミング用語であり、その名の通り、特定の処理を無限に繰り返します。最も一般的な書き方はこうです。

while true
  puts "ainachan"
end

 これを実行すると"ainachan"と連呼しつづける迷惑なやつになります。C-Cで止めてあげましょう。

 原理としては、マニュアルの「制御構造」のページに書いてあります。whileは、その後に続く式がfalseになるまで永遠に繰り返す文ですが、今回の場合は式の部分にtrueと書き込んでいるので、falseになることはありません。つまり無限に繰り返すということです。

 無限に繰り返して終わらないというのは困ったようにも思えますが、普通のプログラムならまあC-Cを入力すれば終わりますし、終わらないということがそれなりに便利なともあります。

 余談ですが、例えば今まさに使っているブラウザとかエディタのようなGUIアプリケーションは、これまでに作ってきたHelloWorldやMegaGreeter(20分ではじめるRuby)などのようにやることやったらすぐ終了してしまうというのでは困るので、どこかしらで無限ループのようなことをしています。OSとかもそうです。

 ただ、普通は終わらないと困るので、終わらせる方法をC-C以外で用意しておくのが普通です。

 例えば、ループするかどうかを判断する変数つづけるを使います。(変数は実は日本語も使えます)

つづける = true
while つづける
  puts "ainachan"
  つづける = false
end

 これを実行するとどうなるでしょうか。while文は、つづけるtrueの間は無限に繰り返しますが、ainachanと言った後でつづけるfalseになるので、そのままwhile文は終了し、1回しか"ainachan"は出力されません。これだとループの意味は無いので、普通はif文をからめてこういう感じになります。

つづける = true
while つづける
  puts "ainachan"
  if 終了条件
    つづける = false
  end
end

 この終了条件の部分には、「どういう時に終わらせたいか」という条件式が入ります。例はちょっと後で。

無限ループ2

 無限ループの書き方についてもいくつかあります。普通は先ほど説明した通りに書くのが良いのですが、こういうパターンもあります。

puts "ainachan" while true

 今度は1行です。これは「while装飾子」というパターンです。先ほどと同じくマニュアルの制御構造のページのwhileの節の次に書いてあります。これはwhileの後の条件がtrueになるまでwhileの前の文を繰り返すという意味です。基本的には1行を繰り返すのですが、複数の行を1行として扱うbeginendと組み合わせると以下のようになります。

begin
  puts "ainachan"
  puts "anchan"
end while true

 ほとんど最初のwhile true 〜 endと同じように動くように見えますが、while装飾子とbegin endを組み合わせた場合だけちょっと違う動きをします。while装飾子のtruefalseに変えてみてください。

begin
  puts "ainachan"
  puts "anchan"
end while false

 条件式がfalseなのでループは実行されないように見えますが、1回だけ実行されます。「1回は少なくとも実行したい」という時はこのような書き方をします。滅多にありませんが、稀にそう思うことがあります。なにかスッキリしない時に思い出すと良いでしょう。

無限ループ3

 untilというのもあります。これは単にwhileの条件のtruefalseが逆になっただけで、ほとんど同じです。until 式の部分はwhile not 式と書けば全く同じなので実のところあまり見かけませんが、でもなにかそれでは気持ち悪い時に使います。

 Rubyにはそういう「同じだけど名前が違う」とか「ほとんど同じだけどすごく細かいところが違う」という機能がたくさんあります。一番気持ちいいやりかたでやると良いでしょう。

4回繰り返す

 4回繰り返します。まずはこうです。

puts "ainachan"
puts "ainachan"
puts "ainachan"
puts "ainachan"

 4回繰り返しました。あまりにも単純ですが、これはこれで正解になることがあります。開発初期のとりあえずやってみようという段階や動作確認ではこういう感じでもよいことがあります。コピペも有効に使っていきましょう。

4回繰り返す (n回繰り返す)

 「4回」というように、回数が決まっている場合はIntegerクラスのtimesメソッドを使うのが簡単です。

4.times {
  puts "ainachan"
}

 これなら100回にしたくなった時にも4の部分を書き換えればよいので簡単です。

 4の部分を変数にするのも良いでしょう。n回繰り返すパターンは以下です。

n = 4 # 今回は4回繰り返します

n.times {
  puts "ainachan"
}

 nをメソッドのパラメータにすると、指定した回数だけ繰り返すメソッドも作ることができます。今日は4回、明日は10回繰り返したい、という場合などに便利です。

def renko(n)
  n.times {
    puts "ainachan"
  }
end

renko(4)

for文を使って4回繰り返す (timesの別の使い方)

 timesにはブロック({ ... })を指定する以外にも使い方があります。

 例えば、以下のようにfor文といっしょに使えます。

for i in 4.times
  puts "ainachan"
end

 ここで、iは、何回目かという情報が入っています。for文の中でputs iなどして確かめてみるとよいでしょう。0から始まり、1, 2, 3と表示されると思います。このように回数は0から始まりn - 1でおわります。そしてこのfor文のコードは以下と同じです。

4.times {|i|
  puts "ainachan"
}

 timesのブロックは、このようにカウンタiを受け取ることができます。ブロックの中でputs iするとまた同じようになるはずです。

 また、for文のパターンは以下のコードとも全く同じです。そもそも実は、このコードの省略形がfor文です。Rubyにはこのような同じ処理の書き換えパターンがよく登場します。くわしくは「制御構造」のマニュアルに書いてあります。ここではeachが出てきます。

4.times.each {|i|
  puts "ainachan"
}

 IntegerクラスのtimesはブロックをつけないとEnumeratorを返すとマニュアルに書いてあります。Enumeratorというのは「なんか数えられるやつ」くらいの意味です。数字だったりリストだったり、そういう何かです。そしてマニュアルで言うとEnumeratorクラスの中にeachメソッドがあります。eachなのでつまり、「それぞれの要素に対してブロックを実行する」という意味で、これが数字やリストに対して実行すると繰り返しになります。

4回繰り返す (カウンタを使う)

 ここで、現在の繰り返し回数iがわかるということは、iを使って以下のように書くこともできます。

100.times {|i|
  if i < 4
    puts "ainachan"
  end
}

 日本語で説明すると、「100回繰り返し、繰り返し回数が4回未満なら"ainachan"と表示する」ということになります。これはこれでいいのですが、普通は100回も繰り返さずに、4回以上になった時点でやめればよいのです。というパターンが以下のコードです。

100.times {|i|
  if i >= 4
    break
  end
  puts "ainachan"
}

 これが「100回繰り返すが、4回以上になったらやめる」です。ループの中でbreakと書くと、breakが実行された時点でループそのものが終了します。「ループを抜ける」とも表現します。このようにループは途中でやめることもできます。

4回繰り返す (whileとカウンタを使う)

 最初に出てきたwhile文を使って以下のように書くこともできます。

i = 0
while i < 4
  puts "ainachan"
  i += 1
end

1. while文にはカウンタが無いので自分でi = 0と定義します
2. iが4未満の場合に繰り返します
3. "ainachan"って表示
4. iに1を足します
5. 2に戻る

 大事なのは2と4です。4で自分でちゃんと数えてあげる必要があります。4が無いと無限ループします。

 また、無限ループとbreakを使って以下のように書くこともできます。

i = 0
while true
  if i >= 4
    break
  end

  puts "ainachan"
  i += 1
end

 このように、whileの条件式をループ内のif文で代用することもできます。

 ただ、これだと条件がi < 4だったのがi >= 4と逆になっていてちょっとややこしいかもしれません。そういう時のために、前にちょっと紹介したuntilや、ifの逆のunlessというものがあります。試しに書いてみるとこうなります。

# unlessとbreakを使うパターン
i = 0
while true
  unless i < 4
    break
  end

  puts "ainachan"
  i += 1
end
# untilを使うパターン
i = 0
until i >= 4 
  puts "ainachan"
  i += 1
end

 どのやりかたが良いかというのは状況によるとしか言えません。大抵は状況により最適な方法は存在しますが、1つとは限りませんし、確固たる理屈があるわけでもなく経験則という場合も多いです。とりあえずは気持ちの良いものを使いましょう。

3回に1回違うことをする

 3回に1回、"ainachan"じゃなくて"anchan"と表示してみましょう。一番簡単なのはこうだと思います。

puts "ainachan"
puts "ainachan"
puts "anchan"
puts "ainachan"
puts "ainachan"
puts "anchan"

 無限にやるならこうです。

while true
  puts "ainachan"
  puts "ainachan"
  puts "anchan"
end

 ここで考えなおしてみましょう。「3回に1回」ということは、ループの回数を数えてカウンタが3の倍数の時だけ表示を変えればよいのです。その実装が以下のコードになります。

i = 1
while true
  if i % 3 == 0
    puts "anchan"
  else
    puts "ainachan"
  end

  i += 1
end

 %というものが出てきましたが、これはmod演算といって、割り算の余りを出す演算子です。要するにifの条件式は「iを3で割った余りが0ならば」という意味になります。つまり「3の倍数ならば」ということです。前と違ってi = 0ではなくi = 1から始まっているのは、最初の1回目で"anchan"と表示しないためです。ちなみに%の詳細はIntegerクラスのマニュアルにあります。

 余談ですが、10年くらい前にはこのようにループとifを使いこなせてmodの意味がわかっていればプログラマとして就職できるという噂がありました。実際どうだったのかはよくわかりません。

リストの中身に従って繰り返す

 1行目は"ainachan"、2行目は"anchan"、3行目は"mendako"と表示しようと思います。これも繰り返しで表現することができます。要するに「表示する」という部分は同じなので、文字列のリストを作って、それぞれに対してputsしてあげればよいのです。というのが以下のコードです。

["ainachan", "anchan", "mendako"].each {|s|
  puts s
}

 ここで[ ... ]の部分はArrayクラスのメソッドです。この["ainachan", "anchan", "mendako"]の部分で配列(Array: リストのようなもの)を作っています。配列にも前に少し紹介したeachメソッドがあり、ブロックの中でそれぞれに対してputsを実行します。sには配列の中身が1つずつ入ってきます。とりあえず書いてみればなんとなくわかると思います。

 これを使って「3回に1回〜」を実現することもできます。こんな感じです。

["ainachan", "ainachan", "anchan"].each {|s|
  puts s
}

 無限にやるならこれ自体をループしてしまえばよいのです。

while true
  ["ainachan", "ainachan", "anchan"].each {|s|
    puts s
  }
end

 無限の場合は、Arrayクラスのcycleメソッドを使って書くこともできます。cycleは配列に含まれる要素を繰り返すメソッドで、["ainachan", "anchan", "mendako"].cycleのように書くと["ainachan", "anchan", "mendako", "ainachan", "anchan", "mendako", "ainachan", "anchan", "mendako", ...]のような感じになります。要するに以下を実行してみましょう。先程のコードと同じ結果になるはずです。

["ainachan", "ainachan", "anchan"].cycle.each {|s|
  puts s
}

 ということはつまり、以下のコードは一番最初の無限ループのコードと同じ結果になります。

["ainachan"].cycle.each {|s|
  puts s
}

 意味は少し複雑になって、「"ainachan"だけが含まれた配列(["ainachan"])を無限に繰り返して(cycle)、その中身を全て(each)表示する(puts)」ということになります。この場合も、どちらが良いのかは状況次第です。

 もちろん、breakと組み合わせて"mendako"がきたらループを抜けるというようなこともできます。

["ainachan", "anchan", "mendako", "kumachka"].each {|s|
  if s == "mendako"
    break
  end
  puts s
}
end

 この場合は"mendako"と"kumachka"は表示されません。

繰り返しの途中でやりなおしたり次に行ったりする

 breakと似た話で、繰り返しの途中で後の処理をせずに次に行きたいことがあります。例えば、以下のコードです。

["aina", "an", "mendako", "kumachka"].each {|s|
  print s
  if s == "mendako"
    puts
    next
  end
  puts "chan"
}

 このコードの意味は、「文字列が"mendako"の時だけ"chan"を付けない」という意味になります。ここでprintは、改行せずに文字列を表示するメソッドです。それと、putsはうしろに何も指定しないと改行だけするメソッドになります。

 "mendako"のif文の中にnextという命令があります。これは繰り返し(eachのブロック)の中の処理を途中で辞めて次の繰り返しに行く、という意味で、この場合はput "chan"を実行せずに次の"kumachka"の処理に移ります。実行してみるとなんとなくわかると思います。

 このnextと似たものとしてredoがあります。こちらは次に行かずに、今と同じものをもう一度やりなおします。つまり以下のようにnextredoに置き換えたものを実行すると、"mendako"を処理しようとしてredoされて再度"mendako"を処理しようとするので無限ループします。

["aina", "an", "mendako", "kumachka"].each {|s|
  print s
  if s == "mendako"
    puts
    redo
  end
  puts "chan"
}

 redoはあまり使うことはないかもしれませんが、もう一度やればうまくいくという状況があれば思い出してください。リトライ(retry)というのもあって似ていますが、たぶんリトライの方が出番は多いと思います。後で説明します。

範囲にしたがって繰り返す

 Rangeについて書こうと思ったけどあまり変わらないのでやめ。(1..4)(1...5)のように数字の範囲を指定することができます。これらはRangeクラスのオブジェクトで、eachメソッドがあるので、(1..4).each {|i| puts i }とかやってみてください。

再帰

 再帰は少し変わったパターンです。うまく使えばわかりやすくなりますが、なかなか難しい概念ですし、性能も良いとは言えないところがあるのでさほど使われることはありません。でも状況によってはすごく有用なこともあるという、そういうものです。そういうのもあるんだなと思っておくだけでも良いでしょう。

 メソッドは自分自身を呼び出すことができます。これを利用して無限ループを書くことができます。

def ainachan
  puts "ainachan"
  ainachan # 自分自身(ainachanメソッド)を呼び出し
end

ainachan

 ainachanメソッドの中でさらにainachanメソッドを呼び出しています。それだけでは最初の1回目が実行されないので、最後にainachanメソッドを呼び出しています。これを実行すると、無限ループの時と同様に"ainachan"と表示され続けるはずですが、続けているうちに途中でSystemStackErrorみたいなエラーが出て止まると思います。これは実行した環境(状況みたいなもの)によって変わるのでなんとも言えませんが、数秒で止まる人もいれば数時間も止まらない人もいると思います。それはまあそういうものです。

 このエラーは自分自身を呼び出す(これを再帰といいます)回数が多すぎた時に起きます。このようなエラーが発生する可能性があるので、再帰はあまり性能がよくないとされています。一応それを克服する方法もありますが、それなりに特殊なことになるので難しい。

 ここで何が起きているか、何故エラーが起きるかというと、Rubyの外の話も色々と関わってくるので、ここでは説明しませんが、そちらの方に興味があればそういう方向に行くのもアリです。(私はそっちの方が詳しいです)

 実のところ、再帰というのはすごく強力な概念なので、ここで説明している繰り返しのほとんどを再帰を使って書き直すことができます。ただ、エラーが起きることもあるように、Rubyを含む多くのプログラミング言語ではあまり効率が良くないので、かなり限られた状況でしか使われません。逆に言うと再帰で書けるコードのほとんどは他の繰り返しに置き換えることができるのです。必要以上に避ける必要もありませんが、繰り返しで書けるならできるだけ繰り返しで書く方が良いでしょう。

リトライ(と例外)

 繰り返すことの目的のひとつとして、エラーが起きたからやり直すというものがあります。つまりリトライです。redoと似ていますが、こちらはコンピュータの本質的な問題に対処するための仕組みです。

 コンピュータには同じ処理でももう1回やれば成功するかもしれないという事があります。例えば外部のデバイスと通信するときです。

 つながっていないハードディスクに書き込もうとするとエラーになりますが、つないでからリトライすると成功するかもしれません。インターネットにつながっていない時に通信しようとするとエラーになりますが、時間が経って再接続された後だと成功するかもしれません。

 このように、プログラムとしては間違いは無くてもコンピュータそのものの状況(故障など)や世の中の状況(当然ながらインターネットを使うプログラムは世界が滅ぶと使えません)によって実行が不可能になるということがあります。これをRubyでは例外(Exception)と呼んでいます。Rubyではエラーも例外の仲間です。エラーと言った時は例外の話をしていると思って問題ありません。前節で出てきたSystemStackErrorもエラーであり、Rubyでは例外の一種です。

 例題として例外を起こすとなるとそれなりの準備もあって面倒くさいので、サンプルコードでは例外を発生させる命令を使います。例外は、一番簡単なものとしては他のオブジェクトと同じくExceptionクラスを使ってException.newで作ることができます。これだけではただのクラスと同じなので、「発生させる」といった場合にはraise命令を使う必要があります。つまりこうです。

raise Exception.new

 この一行をirbなどで実行してみてください。これまで散々見てきたようなエラーが起きた時みたいな表示が出たらたぶん成功です。本当にエラーだったらなんかごめんなさい。

 このraiseというのは、例外を発生させる命令です。あまり使うことはありませんが、プログラムの中でなにか想定外のことが起きた時に使います。存在しないメソッドを呼んだ時や、想定しない使い方をした時などに発生します。例えば、Integerメソッドにはmendakoメソッドは存在していないので2.mendakoはNoMethodErrorという例外が起きますし、1を0で割った時にどうなるかは誰にもわからないので1 / 0はZeroDivisionErrorが起きます。irbなどで試してみてください。

 ちなみにraiseでは適当な文字を指定することもできます。なにか適当な名前を思いつかない時はエラーメッセージを投げておけばOKです(raiseを使うことを「投げる」と言うことがあります)。つまりこういうことができます。

raise "例外が起きたよ"

 Rubyでは、例外が起きた時に何かをすることができます。そして「例外が起きた時にできる何か」の中にはリトライというものがあります。これを使って無限ループを作ることができます。具体的にはこうです。

begin
  puts "ainachan"
  raise "なんかエラー" # 例外が発生
rescue
  retry
end

 これを実行すると無限ループすることを確認できると思います。

 例外(エラー)が起きそうな場所はbegin ... rescue ... endで囲む必要があります。この中のbegin ... rescueの中でエラーが発生するとrescue ... の間のプログラムが実行されます。この場合はretryです。retryは、rescueに対応するbeginまで戻って、そのbeginのところからやり直しますという命令です。つまりここでは、raiseのところで例外が発生するのでrescueの節が実行されてretryの作用でbeginのところまで戻って再びputs "ainachan"が実行されることになります。

 試しにraise "なんかエラー"の行を削除してみてください。例外が発生しなくなるので1回だけ"ainachan"が表示されて終了すると思います。rescue ... endの節は、その前の節で何事も起きなければ実行されることはありません。

 これが何の役に立つのかというと、例えば通信エラーだった場合にはretryの前に再接続のプログラムを実行すれば良いわけです。あまり使うことは無いかもしれませんが、みなさんが使う通信やファイル読み書きのプログラムではライブラリの中のどこかでこのような処理をしています。機会があったあ思い出してみてください。

おわり

 このように、繰り返しひとつとってもプログラムにはやりかたがたくさんありますし、目的もたくさんあります。もちろんここで説明した意外にも様々なパターンがありますが、少なくともRubyにおいてはほとんどがここで挙げたパターンの組み合わせになると思います。

 「色々なやり方があるんだな」とわかった上で、やり方を探しつつ、時にはより良い方法を探してマニュアルを読んだり先人のコードを読んだり詳しい人(私を含む)に尋ねたりして、良いコードを書けるようにがんばってみてください。