Techioz Blog

条件付きで作成された配列が作成後に表示されない

概要

与えられたファイルまたはストリームの行を反転するタイプの tac を Ruby で書いているとします。

1行目 2行目 3行目 [Rubyスクリプト] => 3行目 2行目 ライン1

以下にいくつかのテスト ファイルを示します。

printf "f1, Line %s\n" $(seq 3) >f1
printf "f2, Line %s\n" $(seq 5) >f2
printf "f3, Line %s\n" $(seq 7) >f3

それを簡単に書く方法は次のとおりです。

ruby -e ' # read each ARGF and reverse it
$<.each_line{|line| 
    lines=Array.new if $<.file.lineno==1 
    lines.unshift(line)
    p lines if $<.eof?
}'

ただし、そのバージョンでは次のエラーが発生します。

-e:4:in `block in <main>': undefined method `unshift' for nil:NilClass (NoMethodError)

    lines.unshift(line)
         ^^^^^^^^
    from -e:2:in `each_line'
    from -e:2:in `each_line'
    from -e:2:in `<main>'

スクリプトを次のように変更することでこれを修正できます。

ruby -e 'BEGIN{lines=[]}
$<.each_line{|line| 
    lines=Array.new if $<.file.lineno==1 
    lines.unshift(line)
    p lines if $<.eof?
}'

しかし、なぜ BEGIN ブロックが必要なのでしょうか?ライン配列は最初のスルーで作成されませんか?配列の使い捨て定義のようです…

最終バージョンは実際に動作します。

cat f1 | ruby -e 'BEGIN{lines=[]}
$<.each_line{|line| 
    lines=Array.new if $<.file.lineno==1 
    lines.unshift(line)
    p lines if $<.eof?
}' - f2 f3
["f1, Line 3\n", "f1, Line 2\n", "f1, Line 1\n"]
["f2, Line 5\n", "f2, Line 4\n", "f2, Line 3\n", "f2, Line 2\n", "f2, Line 1\n"]
["f3, Line 7\n", "f3, Line 6\n", "f3, Line 5\n", "f3, Line 4\n", "f3, Line 3\n", "f3, Line 2\n", "f3, Line 1\n"]

しかし、ループ内で再度定義するためにのみ、BEGIN ブロック内で行を定義する必要があるのはなぜでしょうか? BEGIN ブロック内でどの行が定義されているかは関係ありません。数値、ブール値、ハッシュなど何でも構いませんが、名前は存在する必要があります。

アイデアは?

% ruby -v
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [arm64-darwin23]

コメントと回答ありがとうございます。なぜ私の筋肉の記憶が混乱したのかを考えるときに、この Python を参照してください。

def f():
    # local li is created first iteration and used on subsequent... 
    # Similar to Ruby, li is local to this scope of f()
    for x in [1,2,3,4]:
        if x==1: li=[] 
        li.append(x)

    return li 

解決策

まず、Unix パイプや入出力とは何の関係もありません。 Ruby のみのバリアントでも同じエラーが発生します。

[1, 2, 3].each do |i|
  ary = [] if i == 1 
  ary.unshift(i)
end
# undefined method `unshift' for nil:NilClass

最初の反復では ary が定義されているが、後続の反復では定義されていないため、例外が発生します。ここでは、ary は nil になります。 Ruby では、ブロックは新しいローカル変数スコープを作成し、

これは、同じブロックを複数回呼び出す場合にも当てはまります。

def foo
  yield
  yield
end

foo do
  p before: defined? a
  a = 1
  p after: defined? a
end

出力:

{:before=>nil}
{:after=>"local-variable"}
{:before=>nil}
{:after=>"local-variable"}

ご覧のとおり、変数のスコープはブロック呼び出し間では保持されません。同じことが、ブロックを複数回呼び出すそれぞれにも当てはまります。

望ましい動作を得るには、ブロックの外に変数を作成するだけです。例:

ary = []
[1, 2, 3].each do |i|
  ary.unshift(i)
end
ary #=> [3, 2, 1]