YAML で遊ぼう

Ruby 1.8 に標準添附されている YAML はオブジェクトのデータをテキストファイルに落したり、テキストファイルからオブジェクトに読みこんだりするときの規格です。YAML の利用の仕方は概ね次のようになります。

YAML を利用したアプリケーションを作る

YAML の情報については、YAML を Ruby で使うや、るびまの YAML 入門に詳しいので省略して、次のような YAML のデータを利用したプログラムでテストしてみましょう。

# card_data.txt
---
- card 1
- |
  This is an interactive card program.
  Press 1 when you go to card 2.
  Press 2 when you exit this program.
- [to card 2, end]
- [card 2, end]
---
- card 2
- |
  This is another card.
  Press 1 when you return to card 1.
  Press 2 when you exit this program.
- [to card 1, end]
- [card 1, end]
---
- end
- This is the last card
- - $
- - $

これは、文章を表示した後つぎの文章を撰ぶための選択肢を表示し、番号で選択して次の表示に進むといった動作をするプログラムのためのデータです。銀行でキャッシュカードを使うときに見かけるようなものです。これを利用するためのプログラム card.rb は次のようになります。

# card.rb: interactive card program
# written by tnomura in /04/16/2006

require 'yaml'

class Card
  def initialize(card)
    @title = card[0]
    @document = card[1]
    @choice = card[2]
    @links = card[3]
  end

  attr_reader :title

  def disp
    puts
    puts "[#{@title}]", @document

    return nil if @choice[0] == '$'

    puts
    itemNo = 1
    @choice.each {|item| print "#{itemNo}: #{item}\t"; itemNo += 1}
    puts
    print "chose number > "
    i = gets.to_i - 1
    @links[i]
  end
end

card_table = {}

str = IO.read(ARGV.shift)
cards = YAML.load_stream(str).documents

cards.each do |node|
  card = Card.new(node)
  card_table[card.title] = card
end

card = card_table[cards[0][0]]

while true
  next_card = card.disp
  card = card_table[next_card]
  break if not card
end

実行例は、次のようになります。

$ ruby card.rb card_data.txt

[card 1]
This is an interactive card program.
Press 1 when you go to card 2.
Press 2 when you exit this program.

1: to card 2     2: end
chose number > 1

[card 2]
This is another card.
Press 1 when you return to card 1.
Press 2 when you exit this program.

1: to card 1     2: end
chose number > 2

[end]
This is the last card

YAML でユーザ定義クラスのオブジェクトを記述する

YAML ではユーザが定義したクラスのオブジェクトも記述できます。card_data.txt から直接 Card クラスのオブジェクトを読みこめるようにした記述はつぎのようになります。

# card2_data.txt

--- !ruby/object:Card
title: card 1
document: |
  This is an interactive card program.
  Press 1 when you go to card 2.
  Press 2 when you exit this program.
choice:
  [to card 2, end]
links:
  [card 2, end]
--- !ruby/object:Card
title: card 2
document: |
  This is another card.
  Press 1 when you go to card 1.
  Press 2 when you exit this program.
choice:
  [to card 1, end]
links:
  [card 1, end]
--- !ruby/object:Card
title: end
document: |
  This is the last card.
choice: - $
links:  - $

これを利用する為の変更を card.rb に加えたプログラム card2.rb は次のようになります。

# card2.rb: interactive card program
# written by tnomura in /04/16/2006

require 'yaml'

class Card
  def initialize
    @title = nil
    @document = nil
    @choice = nil
    @links = nil
  end

  attr_reader :title

  def disp
    puts
    puts "[#{@title}]", @document

    return nil if @choice[0] == '$'

    puts
    itemNo = 1
    @choice.each {|item| print "#{itemNo}: #{item}\t"; itemNo += 1}
    puts
    print "chose number > "
    i = gets.to_i - 1
    @links[i]
  end
end

card_table = {}

str = IO.read(ARGV.shift)
cards = YAML.load_stream(str).documents

cards.each do |card|
  card_table[card.title] = card
end

card = cards[0]

while true
  next_card = card.disp
  card = card_table[next_card]
  break if not card
end

card2.rb の initialize メソッドではわざと引数を取らないようにしましたが、関係なく YAML から Card オブジェクトにデータが取りこまれました。

論理値の記述

YAML では Ruby の論理値を記述することができます。true と false はそのまま記述し、nil は ~ (チルダ記号) で表します。論理値を導入したときの card2.rb と card2_data.txtの変更点は次のようになります。

$ diff card2.rb card3.rb
20c20
<     return nil if @choice[0] == '$'
---
>     return nil unless @choice
$ diff card2_data.txt card3_data.txt
25,26c25
< choice: - $
< links:  - $
---
> choice: ~

テキストファイルで Ruby のオブジェクトが記述できるといろいろ面白そうなことができそうです。とくに設定ファイルとして YAML を用いると、さまざまなアプリケーションプログラムの設定ファイルが共通の書式になるために、ユーザにとっては非常なメリットになると思います。Linux のディレクトリーのあちこちに散らばっている設定ファイルが全部 YAML だったらどんなに楽でしょう。また、プログラムを作る方も設定ファイルのパーサを作るのを手抜きできます。

エピローグ

card.rb でいろいろ遊んでいたら、コンストラクターがなくてもきちんと動作することに気がつきました。コンストラクターを消してしまって、いらない空行も消して行を詰めた 最終版の card.rb は次のようになりました。随分コンパクトになって満足です。

コンストラクタのないオブジェクト指向プログラムというのも変な感じですが、それだけ、Ruby と YAML の相性がいいのでしょう。なんだか、データの方がプログラムの動作をコントロールしているように思えて面白い現象です。

#!/usr/bin/ruby
# card.rb: interactive card program
# written by tnomura in /04/29/2006
# instance variables : @title, @document, @choice, @links

require 'yaml'

class Card
  attr_reader :title

  def disp
    puts
    puts "[#{@title}]", @document
    return nil unless @choice
    puts
    itemNo = 1
    @choice.each {|item| print "#{itemNo}: #{item}\t"; itemNo += 1}
    puts
    print "chose number > "
    i = gets.to_i - 1
    @links[i]
  end
end

card_table = {}

cards = YAML.load_stream(File.read(ARGV.shift)).documents
cards.each do |card|
  card_table[card.title] = card
end

card = cards[0]

while true
  next_card = card.disp
  card = card_table[next_card]
  break if not card
end

最終版の card.rb とサンプルデータ data.txt の tar ファイルを card.tar.gz に置いておきます。

追記

YAML の多バイト文字のエンコーディングは UTF だけですが、YAML::load のとき、入力文字のエンコーディングは行わないそうです。したがって、EUC-jpで書いた次のようなファイルも利用することができます。しかし、Ruby オブジェクトの EUC のデータを to_yaml で YAML ファイルに書き出すと、データがバイナリになって、日本語としては読めません。

# data.txt : YAML data file for card.rb
# written by tnomura on /04/30/2006

--- !ruby/object:Card
title: カード 1
document: |
  これは、インタラクティブ・カード・プログラムです。
  カード 2 を見たいときは 1 を選択して下さい。
  プログラムを終了するときは 2 を選択して下さい。
choice: [カード 2 へ, 終了]
links:  [カード 2, 終了]

--- !ruby/object:Card
title: カード 2
document: |
   これは 2 番目のカードです。
   一番目のカードにもどるときは 1 を選択して下さい。
   プログラムを終了するときは 2 を選択して下さい。
choice: [カード 1 へ, 終了]
links:  [カード 1, 終了]

--- !ruby/object:Card
title: 終了
document: |
  これは最後のカードです。
choice: ~