Brainf*ck 括弧の対応

さて、Brainf*ck で括弧の対応関係は必須なんだろうか。要は、こんなのはエラーになるべきなのだろうか。

]

構文木版では、パースエラーになる、括弧の対応をもって文法規則にしているんだから当たり前だ。
一方その場実行版ではエラーにならない、「]」実行時点でテープ値は(何もしてないので)「0」、対応する括弧を探しに行くジャンプは行われないのだ。だから

+]

はエラーになる、corresponding_bracket の中でポインタ位置の不正をチェックしてる所に引っ掛かる。

どちらの動作があるべき姿なんだろうか。

Racc で Brainf*ck 、趣味的な実装

その場実行で Tape と Source クラスを作って抽象化してみる。

class BrainF_ckParser
rule# class BrainF_ckParser

expression :
           | expression TERMINAL { send(val[1][0]).send val[1][1], send(val[1][2]) }

end # class BrainF_ckParser

---- inner
  attr_reader :tape, :source

  def initialize(source)
    @source = Source.new(source)
    @tape = Tape.new
  end # def initialize(source)

  def parse(opts = nil)
    @yydebug = opts.yydebug if opts.respond_to? :yydebug
    yyparse self, :scan
  end # def parse(opts = nil)

  private
  def scan
    while c = @source.char do
      case c
        when '>' then yield [:TERMINAL, [:tape, :right, :id]]
        when '<' then yield [:TERMINAL, [:tape, :left, :id]]
        when '+' then yield [:TERMINAL, [:tape, :plus, :id]]
        when '-' then yield [:TERMINAL, [:tape, :minus, :id]]
        when '.' then yield [:TERMINAL, [:tape, :put, :id]]
        when ',' then yield [:TERMINAL, [:tape, :get, :id]]
        when '[' then yield [:TERMINAL, [:source, :bra, :tape]]
        when ']' then yield [:TERMINAL, [:source, :ket, :tape]]
      end # case c
      @source.next
    end # while c = @source.char do
    yield nil
  end # def scan

---- header
# -*- coding: utf-8; -*-
Version = '$Id: brainf_ck_tape_source.y 3304 2009-03-07 06:39:01Z hs9587 $'
# $Date: 2009-03-07 15:39:01 +0900 (土, 07 3 2009) $

---- footer
class Tape
  def initialize
    @tape = []
    @pointer = 0
  end # def initialize

  def value
    @tape[@pointer] ||= 0
  end # def value

  def right(nil)
    @pointer += 1
  end # def right

  def left(nil)
    @pointer -= 1
  end # def left

  def plus(nil)
    @tape[@pointer] = value + 1
  end # def plus

  def minus(nil)
    @tape[@pointer] = value - 1
  end # def minus

  def put(nil)
    $stdout.print value.chr
  end # def put

  def get(nil)
    c = $stdin.getc
    @tape[@pointer] = c.respond_to?(:ord) ? c.ord : c
  end # def get
end # class Tape

class Source
  def initialize(source)
    @source = source.split //
    @pointer = 0
  end # def initialize(source)

  def char
    @source[@pointer]
  end # def char

  def next
    @pointer += 1
  end # def next

  def bra(tape)
    corresponding_bracket if tape.value == 0
  end # def bra

  def ket(tape)
    corresponding_bracket unless tape.value == 0
  end # def ket(tape)

  private
  Correspondings = {
    '[' => ['[', ']', +1 ],
    ']' => [']', '[', -1 ],
     }
  def corresponding_bracket
    bra_ket, ket_bra, sign = Correspondings[char]
    @pointer += sign
    corr = 0
    until corr == 0 and char == ket_bra do
      raise unless (0...@source.size).include? @pointer
      corr += 1 if char == bra_ket
      corr -= 1 if char == ket_bra
      @pointer += sign
    end # until corr == 0 and char == ket_bra do
    @pointer
  end # def corresponding_bracket
end # class Source

require 'optparse'
require 'ostruct'
opts = OpenStruct.new
opts.yydebug = false

ARGV.options do |opt|
  opt.on('--[no-]yydebug', 'debug mode (from racc -t) on/off'){ |v| 
opts.yydebug = v }
  opt.release = 'TERMINAL acts directly with Tape and Source'
  opt.banner += "\n\tBrainf*ck interpreter.\nOptions:"
  opt.parse!
end # ARGV.options do |opt|

BrainF_ckParser.new((ARGV.size > 0 ? ARGF : $stdin).read).parse(opts)

相変らず終端記号は一つだけ、ちょっと苦しかった。素直にブラケット系終端記号は別にした方が分かり易いだろう。なのでちょっと趣味的な実装という。
そういうわけでアクション「{ send(val[1][0]).send val[1][1], send(val[1][2]) }」が大変分かり難い。終端記号 TERMINAL の持ってくる値(val内)は項目数3の配列とした。0番目はテープ操作なのかソース操作なのかに応じて、それぞれのインスタンス変数を attr_reader 経由で取って来る、1番目はそのオブジェクトに与える操作メソッド名、2番目はソース操作系ならテープの値が必要なのでその辺、テープ操作系の場合は必要無いものなので単にオブジェクトIDを読み捨てる。いずれにしてもスキャナからはシンボル(の配列)がやってくるだけなので send の嵐になって大変分かり難い。
Tape と Source クラスの定義はまあ。テープ操作系メソッドが 1引数 nil を取ってそれを何も使わないのは上記分かり難いアクションの 2番目に相当する引数の数の調整。ソースの対応する括弧を探すメソッドは前と一緒。

Racc で Brainf*ck 、その場で実行の為に

対応する括弧を行き来する感覚を大事にする為にその場で実行する方向で考える。ソースを行き来できるようにするわけだ。その為に、ソースとソース上の位置を示すポインタを導入する。

class BrainF_ckParser
rule# class BrainF_ckParser

expression :
           | expression TERMINAL { send val[1] }

end # class BrainF_ckParser

---- inner
  def initialize(source)
    @source = source.split //
    @source_pointer = 0
    @tape = []
    @pointer = 0
  end # def initialize(source)

  def parse(opts = nil)
    @yydebug = opts.yydebug if opts.respond_to? :yydebug
    yyparse self, :scan
  end # def parse(opts = nil)

  def right
    @pointer += 1
  end # def right

  def left
    @pointer -= 1
  end # def left

  def plus
    @tape[@pointer] = (@tape[@pointer] ||= 0) + 1
  end # def plus

  def minus
    @tape[@pointer] = (@tape[@pointer] ||= 0) - 1
  end # def minus

  def put
    print (@tape[@pointer] ||= 0).chr
  end # def put

  def get
    c = $stdin.getc
    @tape[@pointer] = c.respond_to?(:ord) ? c.ord : c
  end # def get

  def bra
    if (@tape[@pointer] ||= 0) == 0 then
      @source_pointer = corresponding_bracket(@source_pointer) - 1
    end # if (@tape[@pointer] or 0) == 0
  end # def bra

  def ket
    unless (@tape[@pointer] ||= 0) == 0 then
      @source_pointer = corresponding_bracket(@source_pointer) - 1
    end # unless (@tape[@pointer] or 0) == 0
  end # def ket

  private
  def scan
    while c = @source[@source_pointer] do
      case c
        when '>' then yield [:TERMINAL, :right]
        when '<' then yield [:TERMINAL, :left]
        when '+' then yield [:TERMINAL, :plus]
        when '-' then yield [:TERMINAL, :minus]
        when '.' then yield [:TERMINAL, :put]
        when ',' then yield [:TERMINAL, :get]
        when '[' then yield [:TERMINAL, :bra]
        when ']' then yield [:TERMINAL, :ket]
      end # case c
      @source_pointer += 1
    end # while c = @source[@source_pointer] do
    yield nil
  end # def scan

  Correspondings = {
    '[' => ['[', ']', +1 ],
    ']' => [']', '[', -1 ],
     }
  def corresponding_bracket(s_pointer)
    bra_ket, ket_bra, sign = Correspondings[@source[s_pointer]]
    s_pointer += sign
    corr = 0
    until corr == 0 and @source[s_pointer] == ket_bra do
      raise unless (0...@source.size).include? s_pointer
      corr += 1 if @source[s_pointer] == bra_ket
      corr -= 1 if @source[s_pointer] == ket_bra
      s_pointer += sign
    end # until corr == 0 and @source[s_pointer] == ket_bra do
    s_pointer
  end # def corresponding_bracket(s_pointer)

---- header
# -*- coding: utf-8; -*-
Version = '$Id: brainf_ck_source_pointer.y 3298 2009-03-07 02:50:25Z hs9587 $'
# $Date: 2009-03-07 11:50:25 +0900 (土, 07 3 2009) $

---- footer
require 'optparse'
require 'ostruct'
opts = OpenStruct.new
opts.yydebug = false

ARGV.options do |opt|
  opt.on('--[no-]yydebug', 'debug mode (from racc -t) on/off'){ |v| 
opts.yydebug = v }
  opt.release = 'TERMINAL acts directly'
  opt.banner += "\n\tBrainf*ck interpreter.\nOptions:"
  opt.parse!
end # ARGV.options do |opt|

BrainF_ckParser.new((ARGV.size > 0 ? ARGF : $stdin).read).parse(opts)

もう文法要素は最低限、終端記号も一種類。アクションも単純、そして send で分岐している。
ソース @source は配列にしてみた、文字列のままでも良かったのだけど、1.8系と 1.9系で文字列の要素へのアクセスが変わっているのでちょっと避けた('String'[n])。
corresponding_bracket はちょっと複雑かな、まあ対応する括弧を探してるだけといえばそれだけ、括弧のネストを数えるのが少し煩雑。この中ではテープからはみ出るのチェックしている。あとはこういったソース位置の計算と、スキャナ内でのソース位置の変更(ひとつ先に進む)のタイミングを合わせないといけなかったり。

Brainf*ck の Hello, wold!

そうそう、Brainf*ck のテスト用にちょっと

A 一文字表示

++++++
[> ++++++++++ < -]
> +++++.

Hello, world!

>+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.
[-]>+++++++[<++++++>-]<++.>++[<------>-]<.
>+++++++++[<+++++++++>-]<++++++.>++[<---->-]<.+++.------.--------.
[-]>++++++++[<++++>-]<+.[-]++++++++++.

何箇所か出てくる「[-]」は、そのテープ位置の値をゼロクリアするもの。

Racc で Brainf*ck 、構文木

取り敢えずルールはこんな感じかな。

program : expression

expression :
           | expression bracket
           | expression TERM

bracket : '[' expression ']'

終端記号は、「TERM」テープの扱いや入出力と括弧 '['、']'。一方「program」はまあ、最終的な左辺値として全体を代表する。それと同じものなのに更に「expression」を作ってるのは、「'['」「']'」で挟まれるものに「program」は無いだろうという思いもある。その「expression」は空を含む「bracket」と「TERM」の列。「bracket」はその「expression」を囲むもの。
その「bracket」のだけど

  1. 「[」ポインタ位置の数値が 0 以外なら何もしない、0 なら対応する「]」まで行き、その「]」の次からまた始める
  2. 「]」ポインタ位置の数値が 0 なら何もしない、0 以外なら対応する「[」まで戻り、その「[」の次からまた始める

対応する処理を考えると要するに while ループ(0 以外なら繰り返しを続ける) なわけだ。それが分かっているから次のようになる。だが、対応する括弧を行き来する気分が無くなってしまうのだけど。

class BrainF_ckParser
rule# class BrainF_ckParser

program : expression
  { result = RootNode.new val[0] }

expression :
  { result = NullNode.new }
           | expression bracket
  { result = ExpressionNode.new val[0], val[1] }
           | expression TERMINAL
  { result = ExpressionNode.new val[0], TerminalNode.new(val[1]) }

bracket : '[' expression ']'
  { result = BracketNode.new val[1] }

end # class BrainF_ckParser

---- inner
  def initialize(source)
    @source = source
    @tape = []
    @pointer = 0
  end # def initialize(source)

  def parse(opts = nil)
    @yydebug = opts.yydebug if opts.respond_to? :yydebug
    yyparse self, :scan
  end # def parse(opts = nil)

  private
  def scan
    @source.each_char do |c|
      case c
        when '>' then yield [:TERMINAL, :right]
        when '<' then yield [:TERMINAL, :left]
        when '+' then yield [:TERMINAL, :plus]
        when '-' then yield [:TERMINAL, :minus]
        when '.' then yield [:TERMINAL, :put]
        when ',' then yield [:TERMINAL, :get]
        when '[' then yield ['[', nil]
        when ']' then yield [']', nil]
      end # case c
    end # @source.each_char do |c|
    yield nil
  end # def scan

---- header
# -*- coding: utf-8; -*-
Version = '$Id: brainf_ck.y 3300 2009-03-07 05:29:42Z hs9587 $'
# $Date: 2009-03-07 14:29:42 +0900 (土, 07 3 2009) $

---- footer
require 'singleton'

class Tape
  include Singleton

  def initialize
    @tape = []
    @pointer = 0
  end # def initialize

  def value
    @tape[@pointer] ||= 0
  end # def value

  def right
    @pointer += 1
  end # def right

  def left
    @pointer -= 1
  end # def left

  def plus
    @tape[@pointer] = value + 1
  end # def plus

  def minus
    @tape[@pointer] = value - 1
  end # def minus

  def put
    $stdout.print value.chr
  end # def put

  def get
    c = $stdin.getc
    @tape[@pointer] = c.respond_to?(:ord) ? c.ord : c
  end # def get
end # class Tape

class Node
  def initialize(*args)
  end # def initialize(*args)

  def evaluate(opts, *args)
    $stderr.puts Tape.instance.inspect if opts.yydebug
  end # def evaluate(opts, *args)
end # class Node

class NullNode < Node; end

class RootNode < Node
  def initialize(expression)
    super
    @expression = expression
  end # def initialize

  def evaluate(opts, *args)
    super
    @expression.evaluate(opts, *args).flatten
  end # def evaluate(opts, *args)
end # class RootNode < Node

class ExpressionNode < Node
  def initialize(expression, other_expression)
    super
    @expression = expression
    @other_expression = other_expression
  end # def initialize(expression, otherexpression)

  def evaluate(opts, *args)
    super
    [@expression.evaluate(opts, *args), @other_expression.evaluate(opts, *args)]
  end # def evaluate(opts, *args)
end # class ExpressionNode < Node

class TerminalNode < Node
  def initialize(term)
    super
    @term = term
  end # def initialize(term)

  def evaluate(opts, *args)
    super
    Tape.instance.send @term
  end # def evaluate(opts, *args)
end # class TerminalNode < Node

class BracketNode < Node
  def initialize(expression)
    super
    @expression = expression
  end # def initialize(expression, other_expression)

  def evaluate(opts, *args)
    super
    while Tape.instance.value != 0 do
      @expression.evaluate(opts, *args)
    end # while Tape.instance.value != 0 do
  end # def evaluate(opts, *args)
end # class BracketNode < Node

require 'optparse'
require 'ostruct'
opts = OpenStruct.new
opts.yydebug = false

ARGV.options do |opt|
  opt.on('--[no-]yydebug', 'debug mode (from racc -t) on/off'){ |v| 
opts.yydebug = v }
  opt.release = 'Node tree'
  opt.banner += "\n\tBrainf*ck interpreter.\nOptions:"
  opt.parse!
end # ARGV.options do |opt|

BrainF_ckParser.new((ARGV.size > 0 ? ARGF : $stdin).read).parse(opts) \
  .tap{ |tree| tree.inspect.display $stderr if opts.yydebug }.evaluate(opts)

Nodeクラスの定義群が入ってきて、そろそろファイルの分割を考えたくなってきた。そしてこれだけクラス定義をするとなると、テストやスペックを書きたくもなって来る。
それから、Singleton を使って Tape を抽象化してみた。

さらに命名について。Node類が出てきたので、パーサにも Parser と接尾辞をつけよう。各 Node(継承)クラス名は各記号の名前文字列に対応したものにしているが、RootNode だけは ProgramNode では気分がでないのでそうした。また、NullNode は作らないで単に何もしないでよかったかも知れないが、一応作ってみた。

Racc で Brainf*ck

そして Brainf*ck、「奇妙な言語」p.46 数値データ格納用のテープの存在(先頭位置 0 から始まる不定長一次元配列)とテープ上の位置を示すポインタを想定した上で

  1. 「>」ポインタを右に一つ動かす
  2. 「<」ポインタを左に一つ動かす
  3. 「+」ポインタ位置の数値を一つ増やす
  4. 「-」ポインタ位置の数値を一つ減らす
  5. 「.」ポインタ位置の数値を ASCIIコードと見做してその文字を出力
  6. 「,」標準入力から一文字読み込みその文字の ASCIIコードをポインタ位置に書き込む
  7. 「[」ポインタ位置の数値が 0 以外なら何もしない、0 なら対応する「]」まで行き、その「]」の次からまた始める
  8. 「]」ポインタ位置の数値が 0 なら何もしない、0 以外なら対応する「[」まで戻り、その「[」の次からまた始める

取り敢えず、今までと同じ方針で「[」「]」(制御構造、ジャンプ)なしのもの。

class BrainF_ckParser
rule# class BrainF_ckParser

expression :
           | expression TERM { send val[1] }

end # class BrainF_ckParser

---- inner
def initialize(source)
  @source = source
  @tape = []
  @pointer = 0
end # def initialize(source)

def scan
  @source.each_char do |c|
    case c
      when '>' then yield [:TERM, :right]
      when '<' then yield [:TERM, :left]
      when '+' then yield [:TERM, :plus]
      when '-' then yield [:TERM, :minus]
      when '.' then yield [:TERM, :put]
      when ',' then yield [:TERM, :get]
    end # case c
  end # @source.each_char do |c|
  yield nil
end # def scan

def parse(opts = nil)
  @yydebug = opts.yydebug if opts.respond_to? :yydebug
  yyparse self, :scan
end # def parse(opts = nil)

private
  def right
    @pointer += 1
  end # def right

  def left
    @pointer -= 1
  end # def left

  def plus
    @tape[@pointer] = (@tape[@pointer] ? @tape[@pointer] : 0) + 1
  end # def plus

  def minus
    @tape[@pointer] = (@tape[@pointer] ? @tape[@pointer] : 0) - 1
  end # def minus

  def put
    print (@tape[@pointer] ? @tape[@pointer] : 0).chr
  end # def put

  def get
    c = $stdin.getc
    @tape[@pointer] = c.respond_to?(:ord) ? c.ord : c
  end # def get

---- header
# -*- coding: utf-8; -*-
Version = '$Id: brainf_ck_without_bracket.y 3298 2009-03-07 02:50:25Z hs9587 $'
# $Date: 2009-03-07 11:50:25 +0900 (土, 07 3 2009) $

---- footer
require 'optparse'
require 'ostruct'
opts = OpenStruct.new
opts.yydebug = false

ARGV.options do |opt|
  opt.on('--[no-]yydebug', 'debug mode (from racc -t) on/off'){ |v| 
opts.yydebug = v }
  opt.release = 'start'
  opt.banner += "\n\tBrainf*ck interpreter.\nOptions:"
  opt.parse!
end # ARGV.options do |opt|

BrainF_ckParser.new((ARGV.size > 0 ? ARGF : $stdin).read).parse(opts)

今までと同じように全部その場で実行する。テープとポインター位置はインスタンス変数に持ってる、ソースを initilarize する時同時に初期化。scan で六つの命令を case分けする、記号の種類は一つだけにして、メソッド名を value に持たせることで処理を振り分ける。ポインタがテープの先頭を越えて左に行くときとか、値の範囲チェックとかしていない、真面目に作るならその辺もきちんとしないといけない(「奇妙な言語」pp.47, 55 参照)。

さて、これに条件付きジャンプ命令を追加しようとすると、全部その場で実行してそれを忘れてしまうという今の方針ではなかなか上手くいかない。素直に構文木を作りましょう。

Racc で HQ9+ 、ちょっと

というわけで、HQ9+言語の処理系を作ったのだけど、二点ほどちょっとした事を試してみよう

スキャナの yiled nil

スキャナ

def scan
  @source.each_char{ |hq9p| yield [hq9p, nil] if /[HQ9\+]/ =~ hq9p }
  yield nil
end # def scan

この、スキャンの終了を送る「yield nil」がちょっと唐突に感じる訳だ、たとえ「yield [false, nil]」にした所でその感じは否めない。なんとかならないだろうか。
@source本体の字句解析が全体として nil を返すとしたらどうだろう。each_char は元の文字列を返すだけなので、split で配列に分割して考える。

def scan
  yield @source.split(//).inject(nil){ |hq9p| yield [hq9p, nil] if /[HQ9\+]/ =~ hq9p }
end # def scan

ブロック内のこの yield は nil を返すし、この Array#inject は結果として nil を返す、inject初期値の nil は該当文字が全然無かったとき対策。
injectブロック内でのスキャンと yield が全部終わると、左辺の yield にその nil が与えられて、トークン列の終了を指図する。
終了は nil 又は [fals, <何か>] であって、false 単独ではエラーになるの注意

 :in `_racc_yyparse_c': scan() yielded FalseClass (must be Array[2]) (TypeError)

それはそれとして、何やってるのか実にわかりくい。左辺の yield に必ず nil が渡るというのが明確でない。というか、必ず nil を渡すんだから明記しよう。
という訳でこの案は不採用、元のソースの方が良い。

トークンの記号を Symbol で

さて、ルールセクションでの記号の取り扱い。前に挙げたソースでは引用符で囲むことで文字列で取り扱っていたが、Racc では引用符を使わず Symbol として取り扱う用法もある(というかその方が多いかもしれない)。
それでやってみよう。そのとき引用符なしで 9(数字) や + をルールセクションに書くと「unexpected token」とか言われてしまう。まあ、NINE とか PLUS とかそれっぽい終端記号を使うことにしよう。

class HQ9Plus
rule# class HQ9Plus

  program :
          | program H {puts "Hello world!\n"}
          | program Q {puts @source}
          | program NINE {puts ninety9_bottles_beer val[1]}
          | program PLUS {@count += 1; @racc_debug_out.puts @count if @yydebug}

end # class HQ9Plus

---- inner
def initialize(source)
  @source = source
  @count = 0
end # def initialize(source)

def scan
  @source.each_char do |hq9p|
    case hq9p                                               # (A)
      when /[HQ]/ then yield [hq9p.intern, nil]
      when /[1..9]/ then yield [:NINE, hq9p.to_i]
      when '+' then yield [:PLUS, nil]
    end # case hq9p
  end # @source.each_char do |hq9p|
  yield nil
end # def scan

def parse(opts)
  @yydebug = opts.yydebug if opts.respond_to? :yydebug
  yyparse self, :scan
end # def parse(opts)

private
def ninety9_bottles_beer(n_ty = 9)
  (0..(n_ty*11)).to_a.reverse.inject('') do |nnbb, b|
    case b
      when 0 then before, after = 'no more bottles', "{n_ty*11} bottles"
      when 1 then before, after = '1 bottle', 'no more bottles'
      when 2 then before, after = '2 bottles', '1 bottle'
      else before, after = "#{b} bottles", "#{b-1} bottles"
    end # case b

    action = (b == 0 \
      ? 'Go to the store and buy some more' \
      : 'Take one down and pass it around'
      )

    nnbb << "#{before.capitalize} of beer on the wall, #{before} of beer.\n"
    nnbb << "#{action}, #{after} of beer on the wall.\n"
    nnbb << "\n" unless b == 0
    nnbb
  end # (1..(n_ty*11)).to_a.reverse.inject('') do |nnbb, b|
end # def ninety9_bottles_beer(n_ty = 9)

---- header
# -*- coding: utf-8; -*-
Version = '$Id: hq9plus_digit.y 3261 2009-02-06 10:31:01Z hs9587 $'
# $Date: 2009-02-06 19:31:01 +0900 (金, 06 2 2009) $

---- footer
require 'optparse'
require 'ostruct'
opts = OpenStruct.new
opts.yydebug = false

ARGV.options do |opt|
  opt.on('--[no-]yydebug', 'debug mode (from racc -g) on/off'){ |v| 
opts.yydebug = v }
  opt.release = 'H'
  opt.banner += "\n\tHQ9+ interpreter.\nOptions:"
  opt.parse!
end # ARGV.options do |opt|

HQ9Plus.new((ARGV.size > 0 ? ARGF : $stdin).read).parse(opts)

その結果ルールセクションだけではなく、スキャナも分岐処理になってしまった (A)、単純な case文による分岐ではある。
折角なので、99本のビールだけじゃなくて、n十n本のビール文書を作れるようにしてみた、yield に与える配列の二項目トークンの値をルールセクションでどう拾うのか(val[1])、使ってみたかったのです。
それから、その辺の正規表現、「/[HQ9\+]/」「/[HQ]/」「/[1..9]/」なんだけど安全のためには「\A」「\z」で囲って前後に他の文字が無いことを保障すべきだろうという話しもある。ここでは、each_char にせよ、split(//) にせよ、一文字ずつ与えている事が確かなのでそこまでしていない。