機能性ノートブック

機能性ノートブック

私が学生の頃教科書のアウトラインを書き写すノートを作るのが嫌いでした。ノートを作っても教科書の内容を繰り返すだけで時間の無駄のような気がしていました。また、ノートを取った後も頭に残る気がしなかったのです。パソコンを使い始めた頃、ノートをパソコンに入力するだけで、問題集ができたり、アイディアを実際に動かしてみたりすることができれば良いのにと思っていました。しかしながら、エクセルでは自分がイメージしていた分野とは離れていて満足できるものではありませんでした。また、プログラムは作ってしばらくすると自分でも使い方が分からなくなることもしばしばでした。ところが、Ruby では実行コードをオブジェクトやメソッドの中に簡単にかくしてしまうことができます。そこで、あこがれの機能性ノートブックのプロトタイプを作ってみました。機能性ノートブックのプログラムの本体は node.rb という次のスクリプトです。

$node_list = {}

class Node
  def initialize( name, question, *choice )
    @name = name
    @question = question
    @choice = choice
    $node_list[ @name ] = self
  end

  def display
    print "#{@name}) #{@question}\n"
    if (@choice != [])
      for i in 0...@choice.length
        print "\t#{i+1}) #{@choice[i][0]}\n"
      end
      print "\tchoice: "
      answer = gets.chomp
      case answer
        when /^[0-9]/
          answer = answer.to_i - 1
          if answer < 0
            print "\n"
            exit
          end
          $node_list[ @choice[answer][1] ]
        when /^[a-zA-Z]/
          $node_list[ answer ]
      end
    else
      exit
    end
  end
end

def n( name, question, *choice )
  Node.new( name, question, *choice )
end

def start( name )
  current = $node_list[ name ]
  while current
    current = current.display
  end
end

これを実際作りたいノートに require "node.rb" 文で取り込みます。機能性ノートブックのサンプルのソースは次のようになります。ファイル名を node_sample.rb で作成します。

require "node.rb"

n "a",
"Which do you like better, Yuki or Mai?",
	["Yuki", "b"],
	[ "Mai", "c" ]
n "b",
"Hi, I am Yuki. Do you like my music?", 
	[ "Yes", "d"],
	[ "No" , "e" ]
n "c",
"Hi, I am Mai. Do you like my music?",
	[ "Yes", "d"],
	["No", "e" ]
n "d", "Thank you!"
n "e", "It's a shame."

start( "a" )

これが Ruby のプログラムかと思われる位コードが表に現れないようにすることができます。n() という関数は、第1引数に質問のノード(節)の名称をとります。第2引数は質問の内容です。第3引数は個数が自由な引数ですが、選択肢とそれにリンクするノード(節)の名前の対でできた配列をとります。第3引数は省略可能です。実行例は次のようになります。

$ ruby node_sample.rb 
a) Which do you like better, Yuki or Mai?
        1) Yuki
        2) Mai
        choice: 1
b) Hi, I am Yuki. Do you like my music?
        1) Yes
        2) No
        choice: 1
d) Thank you!

上の例では次のノード(節)へ移動するには選択肢の番号を入力していますが、ノード(節)の名前で移動することもできます。次がその例です。

$ ruby node_sample.rb
a) Which do you like better, Yuki or Mai?
        1) Yuki
        2) Mai
        choice: d
d) Thank you!

node_sample.rb のようなソースを別のインタープリターを作らなくても簡単に実行させることができるのが Ruby のすごいところです。また、変数の型がないので、アイディアをそのままコードに落していくことができます。ルビーはプロトタイピングの言語としても注目されているようですが分かる気がします。アイディアを文章にするのではなく、直接コードにして実行してみることができるからです。また、後で読み返しても非常に読みやすいのです。

node.rb の説明

node.rb の説明をします。

      1 $node_list = {}

第1行目の名前の先頭に $ がついている変数 $node_list はグローバル変数です。グローバル変数はどのブロックからでも参照することができます。Ruby では $ の付かない普通の変数はすべてローカル変数です。また、定数は先頭を大文字にします。先頭に @ がつくのはインスタンス変数で、@@ がついたものはクラス変数です。 $node_list = {} は選択肢がリンク先のノード(節)に前方参照できるようにするための連想配列です。Ruby はプログラムを1パスで読み込むので、下の例のように定義されていないオブジェクトを含む配列を作ろうとすると NameError になってしまいます。

$ cat test.rb
class Test
end

a = Test.new
b = [ a, c ]
c = Test.new
$ ruby test.rb
test.rb:5: undefined local variable or method `c' for # (NameError)

しかしリンクを全てのノードを定義してからでないと張れないというのでは不便です。そこで、選択肢にたいするリンクの定義は "a" などのような文字列で行い、ノード定義の際に node_list にノード名をインデックスとしてそのノードのオブジェクト(self)を値とする連想配列を作って、リンク先のノード名からそのノードのオブジェクトを呼び出せるように工夫してあります。

      3 class Node
      4   def initialize( name, question, *choice )
      5     @name = name
      6     @question = question
      7     @choice = choice
      8     $node_list[ @name ] = self
      9   end

3 行目からが Node クラスの定義です。initialize はクラスからオブジェクトを作るときのメソッド new を呼び出すときに自動的に呼び出されてオブジェクトの初期化などを行うメソッドです。@ マークのついている変数はインスタンス変数でオブジェクトが作られるときに同時に定義されます。インスタンス変数はオブジェクトの外からは参照することができません。インスタンス変数を操作するためには、それを操作するためのメソッドがクラスに定義されている必要があります。Node クラスには @name (ノードの名称)、@question (そのノードが呼び出されたとき表示される質問) @choice (そのノードから他のノードへ移動するための選択肢とリンク先のノード名を納めるための2次元配列)の3つがあります。

def initialize( name, question, *choice ) で initialize を定義します。括弧の中の name, question, *choice の値が initialize メソッドの本文に渡されます。*choice はメソッドの最後に置く事ができ、不定数の引数を取ることができます。このとき * のつかない choice には、引数のリストが納められます。ちょっとテストをしてみましょう。

~$ irb
irb(main):001:0> def test( a, *b )
irb(main):002:1>   puts a.inspect
irb(main):003:1>   puts b.inspect
irb(main):004:1> end
nil
irb(main):005:0> test ( "apple", "orange", "banana", "melon" )
"apple"
["orange", "banana", "melon"]
nil

テストプログラムの a.inspect というのはオブジェクト a の情報を文字列にして出力するメソッドです。上のプログラムで分かるように c には引数の配列が納められます。

initialize メソッドの 5, 6, 7 行でインスタンス変数の初期化を行い、8 行目で $node_list にノード名をキーに自分自身のオブジェクトを値にした連想配列を登録します。self はそのブロックのオブジェクトをしめす疑似変数です。self を使うことでオブジェクトを定義するときに自分自身の参照をとりだしてデータにすることが可能となります。

     11   def display
     12     print "#{@name}) #{@question}\n"
     13     if (@choice != [])
     14       for i in 0...@choice.length
     15         print "\t#{i+1}) #{@choice[i][0]}\n"
     16       end
     17       print "\tchoice: "
     18       answer = gets.chomp
     19       case answer
     20         when /^[0-9]/
     21           answer = answer.to_i - 1
     22           if answer < 0
     23             print "\n"
     24             exit
     25           end
     26           $node_list[ @choice[answer][1] ]
     27         when /^[a-zA-Z]/
     28           $node_list[ answer ]
     29       end
     30     else
     31       exit
     32     end
     33   end

display メソッドはノードオブジェクトの質問を表示して選択肢を選択する要求を行い、次のノードのオブジェクトを返します。12 行目でノード名と質問を表示し、13, 14,15,16,17,18 行では choice が空でなければ、選択肢に番号を振って表示し、入力待ちのプロンプトを表示し、変数 answer への表示待ちになります。18 行目の gets.chomp は標準入力から1行入力しその行末の改行記号を取り除いた文字列を返します。オブジェクト.メソッドという形での情報の加工の簡潔さと便利さには使えば使う程愛着が湧いて来ます。

19 行からの case 文は Ruby 独特のもので強力です。20 行の when 以下の処理では変数 answer の先頭の文字が数字の場合には選択肢が選択されたと考え、そのリンク先のオブジェクトを返します。また、0 が入力された場合はスクリプト全体を終了します。また、27 行の when 以下の処理で answer の先頭がアルファベットの場合に answer の内容がノード名であると考えそのノード名に対応するオブジェクトを返します。

     36 def n( name, question, *choice )
     37   Node.new( name, question, *choice )
     38 end

36, 37, 38 行のメソッド n の定義は単に Node.new がソースファイルに現れないようにラッピングしているだけです。

     40 def start( name )
     41   current = $node_list[ name ]
     42   while current
     43     current = current.display
     44   end
     45 end

start メソッドはノード名を引数に取り current.display から返されるオブジェクトが nil でない限り 42, 43, 44 行を繰り返します。

node.rb を利用するソースファイルは次のような形式になります。

require "node.rb"

n "a",
"Which do you like better, Yuki or Mai?",
	["Yuki", "b"],
	[ "Mai", "c" ]

...................................................

n "e", "It's a shame."

start( "a" )

require メソッドは引数のファイルを読み込む働きがあります。C の #include に似ています。ソースの本文は n ノード名, 質問, [ 選択肢, リンク先ノード名], ... という形式になっています。関数への引数を渡すための()がありませんが、Ruby のメソッドは()を省略できます。したがって上のソースファイルはメソッド n を呼び出しているだけです。n を括弧を使って表現すると n("a", "Whichi do you like better, Yuki or Mai?",["Yuki", "b"], ["Mai", "c"]) となります。n を実行すると新しいノードが作られインスタンス変数が設定され、ノード名とそのノードのオブジェクトがノードリストに登録されます。最後に start メソッドでメインループが始まります。したがって、ソースリストはただのソースではなくスクリプト本体なのです。

改良型機能性ノートブック

node_sample.rb のソースでも十分キー入力は少なくなっていますが、n や文末の , 等を入力するのがうるさい感じがします。そこで、機能性ノートブックのスクリプトを次のように改造してみました。次のスクリプトを notebook.rb の名前で作成します。

#!/usr/local/bin/ruby -Ke

$node_list = {}

class Node
  def initialize( name, question, choice )
    @name = name
    @question = question
    @choice = choice
    $node_list[ @name ] = self
  end

  def display
    system "clear"
    print "#{@name}) #{@question}\n"
    if (@choice != [])
      for i in 0...@choice.length
        print "\t#{i+1}) #{@choice[i][0]}\n"
      end
      print "\tchoice: "
      answer = gets.chomp
      case answer
        when /^[0-9]/
          answer = answer.to_i - 1
          if answer < 0
            print "\n"
            exit
          end
          $node_list[ @choice[answer][1] ]
        when /^[a-zA-Z]/
          $node_list[ answer ]
      end
    else
      exit
    end
  end
end

def start( name )
  current = $node_list[ name ]
  while current
    current = current.display
  end
end

choice = []
line = gets
first = line.chomp
while( line )
  name = line.chomp
  question = gets.chomp
  line = gets
  while( line =~ /^\s*\[/ )
    choice.push( eval(line) )
    line = gets
  end
  Node.new( name, question, choice )
  break if (not line)
  choice = []
  line = gets
end

start( first )

今度のスクリプトでは、データファイルはプログラムとは別に作成します。使うときは ruby notebook.rb note_sample.txt のような使い方をします。次のようなサンプルのソースファイルをつくって note_sample.txt というファイルにします。

a
Which do you like better, Yuki or Mai?
	[ "Yuki", "b" ]
	[ "Mai", "c" ]

b
Hi, I am Yuki. Do you like my music? 
	[ "Yes", "d" ]
	[ "No" , "e" ]

c
Hi, I am Mai. Do you like my music?
	[ "Yes", "d" ]
	[ "No", "e" ]

d
Thank you!

e
It's a shame.

ソースファイルのルールは次のようになります。最初の1行は空けない、各ノードの第一行目はノード名、第二行目は質問、第三行目以降は、選択肢とリンクの対の配列です。選択肢にリンク先がないときはとりあえずそのノードにリンクさせて配列の形を整えます。ノードとノードの間は必ず空行で区切ります。node_sample.rb のソースと比べても随分見やすくなっていると思います。使い方は次のようになります。実行結果は省略します。

$ ruby notebook.rb note_sample.txt

notebook.rb には特別説明するような文はありませんが先頭の #!/usr/local/bin/ruby -Ke は shebang 記法です。シェルスクリプトと同じようにこの一文を先頭に置いておくとファイルをコマンドラインのコマンドとして使うことができます。この場合ファイルの属性を chmod +x notebook.rb で実行可能にしておく必要があります。Ruby のオプションの -Ke はスクリプトの中に日本語EUCを使うときに必要です。

この notebook.rb プログラムは、構成がちょっと込み入っている本の構造を調べるときなどに使うと便利です。このようなプログラムは C では素人にはとても作れるものではありませんでした。ruby のオブジェクト指向という特質は、プログラムの作りやすさと可読性のよさに本質的に関係しているような気がします。いま考えているのは Ruby を思索の道具に使えないかと言うことです。アイディアや読書によって得られる思考過程をシミュレートするアシスタントとして Ruby を使えないかと言うことです。思いつきを Ruby のプログラムにすることによって、アイディアの実体化と検証をする一般的な方法はないものか少し考えてみたいと思っています。