条件付きで作成された配列が作成後に表示されない
概要
与えられたファイルまたはストリームの行を反転するタイプの 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]