人力検索はてな
モバイル版を表示しています。PC版はこちら
i-mobile

以下のperlのコードが動かない理由を教えて下さい。

FizzBuzzのコードなのですが、$iが11,26,41,56,71,86の時にBuzzと出力されてしまいます。
なんとなく直前のループ時の$outの内容が引き継がれている気がしますが、理由がわかりません。

---
#!/usr/bin/perl

use strict;
use warnings;
use Perl6::Say;

foreach my $i (1..100) {
my $out = "Fizz" if $i%3 == 0;
$out .= "Buzz" if $i%5 == 0;
say $out || $i;
}
---

なお$outの宣言を別にしたところ、正常に動作しています。
---
foreach my $i (1..100) {
my $out;
$out = "Fizz" if $i%3 == 0;
$out .= "Buzz" if $i%5 == 0;
say $out || $i;
}
---

とんでもない思い違いをしている気がしますが、よろしくお願い致します。


●質問者: taichino
●カテゴリ:コンピュータ
✍キーワード:Buzz FizzBuzz Out Perl コード
○ 状態 :終了
└ 回答数 : 4/4件

▽最新の回答へ

1 ● Reiaru
●23ポイント

コメント欄に書きたかったのですが…


なお$outの宣言を別にしたところ、正常に動作しています。

---

foreach my $i (1..100) {

my $out;

$out = "Fizz" if $i%3 == 0;

$out .= "Buzz" if $i%5 == 0;

say $out || $i;

}

---


「別にしたところ」と言いますか、$out の宣言は以下の様に foreach の外に出して下さい。


my $out;

foreach my $i (1..100) {

$out = "Fizz" if $i%3 == 0;

$out .= "Buzz" if $i%5 == 0;

say $out || $i;

}


taichino0730 様の提示したサンプルはどちらもイレギュラーな書き方です。

ループ内で変数宣言を行なうのは、Perl に限らずどの様な言語でも通常はあり得ない書き方です。


今回の Perl の件は、変数宣言を行なう事で別の名称の変数が宣言された様な状態になっているのだと思います。

一度目が $out1 ならば、二度目が $out2 の様に (← 少々よく分からないのですが)。


変数を初期化を明示的に初期化するというのであれば、$out = ""; や $out = 0; を用いて下さい。

my は変数の中を空にするという意味ではありません。

明らかに無駄な処理が増えるだけですし、他にも色々と問題があります

(その上、メモリの少ないサーバー上で 100 万回単位のループをかけるとととんでもない事になりそうな)

◎質問者からの返答

Reiaru 様

ご回答ありがとうございます。

変数宣言を行なう事で別の名称の変数が宣言された様な状態になっているのだと思います。

未熟で申し訳ありません,まだ理解できていないので、もう少し詳しく教えて頂けないでしょうか。

perldoc内のmyを参照しても特にそのような挙動は記述されていませんでした。。

また本題とは少しずれますが、ループ内で変数宣言については、重いコンストラクタを持つオブジェクトでも無い限り問題が無いと考えています。よろしければどのような問題があるのかもあわせて教えて頂けますと幸いです。

手元で以下のコードで実行時間の測定と、メモリ消費量の測定をしてみましたが、問題を抽出できませんでした。

#!/usr/bin/perl

use strict;
use warnings;
use Perl6::Say;
use Benchmark qw(timethese cmpthese);

my $result = timethese(10000, {
Inside => 'inside',
Outside => 'outside',
});
cmpthese($result);

sub inside {
foreach my $i (1..100) {
my $out;
$out = "Fizz" if $i%3 == 0;
$out .= "Buzz" if $i%5 == 0;
say $out || $i;
}
}

sub outside {
my $out;
foreach my $i (1..100) {
$out = "";
$out = "Fizz" if $i%3 == 0;
$out .= "Buzz" if $i%5 == 0;
say $out || $i;
}
}

以下がベンチマークの結果です

Benchmark: timing 10000 iterations of Inside, Outside...
 Inside: 19 wallclock secs (17.54 usr + 0.03 sys = 17.57 CPU) @ 569.15/s (n=10000)
 Outside: 20 wallclock secs (17.60 usr + 0.04 sys = 17.64 CPU) @ 566.89/s (n=10000)
 Rate Outside Inside
Outside 567/s -- -0%
Inside 569/s 0% --

メモリ量の測定 (回数の多いループ内で変数宣言してメモリ使用量を調べてみました)

#!/usr/bin/perl

use strict;
use warnings;
use Perl6::Say;
use Devel::MemUsed;

my $monitor = Devel::MemUsed->new;
$monitor->reset;
say $monitor;

my $count = 5000000;
my $total = 0;
for my $i (1..$count) {
my $d = $i*$i;
$total += $d;
say "loop count: $i, memory used: $monitor" if $i % 100000 == 0;
}

測定結果

loop count: 100000, memory used: 4312
loop count: 200000, memory used: 4320
loop count: 300000, memory used: 4320
loop count: 400000, memory used: 4320
~ 途中省略 ~
loop count: 5000000, memory used: 4320

危険な認識をしている可能性がありますので、お手数ですが何卒よろしくお願いします。


2 ● Craftworks
●100ポイント ベストアンサー

単純に、前者だと if 文の条件に当てはまらないと my が実行されないからです。後者のように my は独立させて書きましょう。

ループ内で my 宣言するのは Perl では問題無い書き方です。むしろ各ループ毎に初期化されて欲しい変数ならループ内で my 宣言するべきです。

でないと、毎ループごとに $out がレキシカルスコープになりません。直前のループの内容を引き継ぐのはレキシカルになってないためです。


ソースの書き方は、CatalystPlagger などのモダンなモジュールのソースを見て参考にしましょう。バージョン番号の横の、Source というリンクで確認できます。

◎質問者からの返答

Craftworks 様

ご回答ありがとうございます。

単純に、前者だと if 文の条件に当てはまらないと my が実行されないからです。

仰る通りですね。思い込みが強くて嫌になります。。ありがとうございます。

毎ループごとに $out がレキシカルスコープになりません。

すいません、こちらの理由がまだわかりません。試しに以下のコードを実行してみました。

#!/usr/bin/perl

use strict;
use warnings;
use Perl6::Say;

for my $i (1..10) {
my $num = $i * $i if $i%2;
say "$i : $num";
}

結果は以下のようになり、特に直前のループの内容が引き継がれてはいません。

1:1
Use of uninitialized value in concatenation (.) or string at ./lexical_test.pl line 9.
2:
3:9
Use of uninitialized value in concatenation (.) or string at ./lexical_test.pl line 9.
4:
5:25
Use of uninitialized value in concatenation (.) or string at ./lexical_test.pl line 9.
6:
7:49
Use of uninitialized value in concatenation (.) or string at ./lexical_test.pl line 9.
8:
9:81
Use of uninitialized value in concatenation (.) or string at ./lexical_test.pl line 9.
10:

ループ内でmy宣言した変数がレキシカルにならない条件を教えて頂けないでしょうか。

お手数ですがよろしくお願い致します。


追記

ソースの書き方は、Catalyst や Plagger などのモダンなモジュールのソースを見て参考にしましょう。

ありがとうございます。Catalystは興味があったので読んでみようと思います。

Plaggerはもう読んだんですが活かせてないですね。。


3 ● Craftworks
●22ポイント
for my $i (1..10) {
my $num = $i * $i if $i%2;
say "$i : $num";
}

の場合、1回目のループではレキシカルになるので、1回目のループが終わるときに消滅し、2回目のループでは if の行が実行されずに $num は undef に、3回目にレキシカルに、と交互にレキシカルとそうでないのを繰り返すので毎回引き継がれていないように見えます。if の内容を変えたら引き継がれるように見えませんか?

ループ内でmy宣言した変数がレキシカルにならない条件を教えて頂けないでしょうか。

「my 宣言した変数がレキシカルにならない」のではなくて、「my 宣言されていない」ということです。宣言が実行されればレキシカルになります。

◎質問者からの返答

Craftworks 様

ご回答ありがとうございます!少し理解できてきたような気がします。

先ほどのコードを修正して実行してみました。

#!/usr/bin/perl

use strict;
use warnings;
use Perl6::Say;

for my $i (1..10) {
my $num = $i if $i%3==0;
$num += $i;
say sprintf("%02d : %02d : %p", $i, $num, \$num);
}

そして結果は以下のようになりました。(各行の#以降は手入力です)

01 : 01 : 9ad9dfc # 1
02 : 03 : 9ad9dfc # (1) + 2
03 : 06 : 9ad9dfc # 3 + 3 [my宣言]
04 : 04 : 9ad9dfc # 4
05 : 09 : 9ad9dfc # (4) + 5
06 : 12 : 9ad9dfc # 6 + 6 [my宣言]
07 : 07 : 9ad9dfc # 7
08 : 15 : 9ad9dfc # (7) + 8
09 : 18 : 9ad9dfc # 9 + 9 [my宣言]
10 : 10 : 9ad9dfc # 10

my宣言された時(i=3 6 9)は想定通りの挙動をしめしていて、ここは普通です。

それ以外の場合ですが、例えば(i=1 2)の時の挙動は以下のコードと等しくなると認識しました。

#!/usr/bin/perl
use Perl6::Say;

for my $i (1..2) {
$num += $i;
say sprintf("%02d : %02d : %p", $i, $num, \$num);
}

use strictを付けると、変数宣言の無い変数は使用できないと思い込んでいました。この場合はuse strictをすり抜けて、変数宣言の無い変数が使用されていたと言う事ですね。

また変数のアドレスを出力させてみるとmy宣言されたレキシカル変数$numと宣言無しのグローバル変数$numが同じアドレスに確保されているので、レキシカル変数$numのスコープを抜ける度にグローバル変数$numもリセットされていた訳ですね。インタプリタの挙動を理解していないので完璧ではないですが。。

まとめ

御陰様で軽い気持ちで書いたFizzBuzzから色々と学ぶ事ができました!ありがとうございました!




追記 2009-08-28 16:41

上記認識はまだ間違ってそうです。

#!/usr/bin/perl

use Perl6::Say;

$num = 20;
say sprintf("%d, %p", $num, \$num);
{
my $num = 10;
say sprintf("%d, %p", $num, \$num);
}
say sprintf("%d, %p", $num, \$num);

このコードの結果が以下になります。同じアドレスにも関わらず値が上書きされていません。もう少し調べてみます。

20, 9c8ec28
10, 9c8ec28
20, 9c8ec28

4 ● りゅう
●50ポイント

my宣言はコンパイル時に発揮される効果と実行時に発揮される効果の2種類の効果があります。

コンパイル時の効果は、use strict 'vers'を黙らせ未定義変数の使用に対するエラーを出さなくすることです。この場合のmyはまさに宣言的な扱いなので、コンパイラに認識さえされれば実際に実行される状態にある必要はありません。つまり my $out if 0; などと書いてもmy宣言は有効です。

実行時の効果は、現在のスコープに変数を定義し初期化することです。my宣言に初期値が指定されていればその値で、指定されていなければundefで初期化します。この効果は実際に実行されないと発揮されません。


my宣言に条件修飾子を付けて、宣言はされているけれど実行はされないという状況を作ると、未定義のレキシカル変数を使用するという通常できないことができてしまいます。その結果どのようなことが起こるかは未定義とされています。


注意: (my $x if ... のような) 条件構造やループ構造で修飾された my 文の振る舞いは 未定義 です。 my 変数の値は undef かも知れませんし、以前に代入された値かも 知れませんし、その他の如何なる値の可能性もあります。 この値に依存してはいけません。 perl の将来のバージョンでは現在のバージョンとは何か違うかも知れません。 ここには厄介なものがいます。

http://perldoc.jp/docs/perl/5.10.0/perlsyn.pod

今回の挙動は、時々しかmy宣言を通らないので、通らない場合はundefに初期化されずBuzzが残るということだと思います。

ただ、my宣言を通る前から$outを参照しているので、$outがどこのスコープに定義されているかは謎です。

◎質問者からの返答

rryu 様

ご回答ありがとうございます。

perldocの探し方が下手で済みません。確かに厄介なものがいるようですね。

それで結局(my $x if..)がどう動作するのかという疑問を感じましたので、もう少し調べてみました。


今まで知らなかったのですが、perlはランタイムコンパイル言語だということでしたので、

(my $x if ..)実行時の挙動を調べる為に、以下の簡略化したコードに対してDevel::Optraceを使用してopcodeを吐かせてみました。

foreach $i (1..4) {
 my $num = $i if not $i%3;
 $num += $i;

結果が長いので抜粋しますと、myが実行される時とmyが実行されない時の2行目のopコードの違いは以下の用になりました。

 # myが実行されないときの2行目のopコード
 nextstate(main loop_test.pl:2) VOID
 gvsv($i) SCALAR
 const(3) SCALAR
 modulo SCALAR KIDS
 not SCALAR KIDS
 and VOID KIDS

 # myが実行される時の2行目のopコード
 nextstate(main loop_test.pl:2) VOID
 gvsv($i) SCALAR
 const(3) SCALAR
 modulo SCALAR KIDS
 not SCALAR KIDS
 and VOID KIDS
 gvsv($i) SCALAR
 padsv($num) LVAL_INTRO SCALAR REF MOD SPECIAL
 sassign VOID KIDS STACKED

上記結果からmyが実行される時のSPECIALというopコードフラグが怪しいと思いました。

OPf_SPECIAL なにか変わったことをするop (op.hを参照)

http://fleur.hio.jp/perldoc/modules/perl/perl-5.8.8/ext/B/B/Conc...

ただperl自体のコードを読むのに抵抗があったので、さらに以下のコードのopコードを見てみました。

foreach $i (1..4) {
 my $num = $i;
 $num += $i;
}

同様に上記コードにおけるmy文のopコードは以下になります。

nextstate(main loop_test.pl:2) VOID
gvsv($i) SCALAR
padsv($num) LVAL_INTRO SCALAR REF MOD SPECIAL
sassign VOID KIDS STACKED

my文のopコードが後置ifがある時と無い時で違う事を期待したのですが、これは先ほどのmyが実行される時のopコードと同じです。というわけで残念ながら後置if文があろうが無かろうが、出力されるopコードに変化は無いようです。opコードが同じなのに別の挙動を示すなんてあり得ない気もするのですが、僕が手軽に追える範囲はこの辺りで限界ですので、一端ここでクローズさせて頂きます。

皆様のお陰でFizzBuzzのコードからperlの一段深い部分の入り口を見つけられて、とても良かったです。

まだ残っている疑問については、自分で一度消化してみて消化不良を起こしましたら、再度質問させて頂ければと思います。


ryuu様、皆様ありがとうございました。

関連質問


●質問をもっと探す●



0.人力検索はてなトップ
8.このページを友達に紹介
9.このページの先頭へ
対応機種一覧
お問い合わせ
ヘルプ/お知らせ
ログイン
無料ユーザー登録
はてなトップ