perlclass 初探: 以模擬 Monty Hall Problem 為例
作者:gugod 發佈於: #perlMonty Hall Problem (蒙提霍爾問題)是個奇妙而有趣的數學遊戲問題。維基百科上提供的問題本文為:
假設你正在參加一個遊戲節目,你被要求在三扇門中選擇一扇:其中一扇後面有一輛車;其餘兩扇後面則是山羊。你選擇了一道門,假設是一號門,然後知道門後面有什麼的主持人,開啟了另一扇後面有山羊的門,假設是三號門。他然後問你:「你想選擇二號門嗎?」轉換你的選擇對你來說是一種優勢嗎?
此問題的解答是「會」,也就是換答案後會比較容易得到汽車的意思。
但,這答案是以列舉出幾種狀況的機率來比較而得的。以機率而論,如果我能玩這遊戲 1000 次,那麼每一局都依主持人提供的資訊來更換答案後,我能在這 1000 局內獲勝的總次數較多。期望值約為 1000 2/3,也就是約為 666 次。而如果我每一局都不換答案,那我在這 1000 局內的獲勝期望值則為 1000 1/3,也就是約為 333 次。
既然如此,那就必須寫個程式來模擬看看了。以下是 play-monty-hall.pl ,以 perlclass 提供的新語法寫成的一個模擬程式,可模擬兩種策略玩家在玩了多局之後的勝率。雖然沒有註解,但應該還算是能夠望文生義的程度。
#!/usr/bin/env perl
use v5.38;
use feature 'class';
no warnings 'experimental::class';
class Game {
field $doors :param;
field $winningDoor = 1 + int rand($doors);
method doors { $doors }
method winningDoor { $winningDoor }
method loosingDoors {
grep { $_ != $winningDoor } (1..$doors);
}
};
class FirstChoicePlayer {
field $doors :param;
field $firstChoice = 1 + int rand($doors);
field @loosingDoors;
method doors { $doors }
method addLoosingDoors ($n) { push @loosingDoors, $n }
method loosingDoors () { @loosingDoors }
method firstChoice () { $firstChoice }
method finalChoice () { $firstChoice }
};
class ChangeChoicePlayer :isa(FirstChoicePlayer) {
method finalChoice () {
my %isLoosing = map { $_ => 1 } $self->loosingDoors();
my @choices = grep { $_ != $self->firstChoice() } grep { ! $isLoosing{$_} } 1..$self->doors();
die "Illegal state" if @choices != 1;
return $choices[0];
}
};
class GameMaster {
field $doors :param;
method playWith ($player) {
die "No player ?" unless defined $player;
my $game = Game->new( doors => $doors );
my %untold = map { $_ => 1 } ($game->winningDoor, $player->firstChoice);
while ((keys %untold) == 1) {
my $door = 1 + int rand($doors);
$untold{$door} = 1;
}
my @revealLoosingDoors = grep { ! $untold{$_} } (1..$doors);
for my $door (@revealLoosingDoors) {
$player->addLoosingDoors($door)
}
my $finalChoice = $player->finalChoice();
return $finalChoice == $game->winningDoor;
}
};
sub playOneRound($playerClass) {
my $doors = 3;
my $gm = GameMaster->new( doors => $doors );
my $player = $playerClass->new( doors => $doors );
return $gm->playWith( $player );
}
sub play ($rounds, $playerClass) {
my $wins = 0;
for (1 .. $rounds) {
my $win = playOneRound($playerClass);
$wins++ if $win;
}
return $wins;
}
sub sim ($rounds) {
for my $playerClass ("FirstChoicePlayer", "ChangeChoicePlayer") {
my $wins = play($rounds, $playerClass);
my $pWin = $wins / $rounds;
say "$playerClass wins $wins / $rounds. p(win) = $pWin";
}
}
sim(shift // 1000);
可以提供一個參數,表示要模擬幾局。預設為 1000 局。執行起來如下:
# play-monty-hall.pl
FirstChoicePlayer wins 323 / 1000. p(win) = 0.323
ChangeChoicePlayer wins 665 / 1000. p(win) = 0.665
# play-monty-hall.pl 1000000
FirstChoicePlayer wins 333606 / 1000000. p(win) = 0.333606
ChangeChoicePlayer wins 666348 / 1000000. p(win) = 0.666348
這程式內主要幾個互相作用的 class:
FirstChoicePlayer
: 以不變為其策略的玩家ChangeChoicePlayer
: 以改變為其策略的玩家GameMaster
: 與玩家互動的遊戲主持人。Game
: 維護單一一局的遊戲初始資訊。基本上只有記住哪號門是贏門 ($winnigDoor
),並提供列舉出所有輸門的方法loosingDoors
Game 物件在建構、初始完畢後計基本上是個常數了,沒有什麼特別的地方。 FirstChoicePlayer 也差不多,只是會有個 @loosingDoors
內部狀態來記著那些由 GameMaster 提供的輸門號碼。
而此處我讓 ChangeChoicePlayer
為 FirstChoicePlayer
的延伸,依照 $self->firstChoice
與 $self->loosingDoors
的內容來推算出 finalChoice
。這邊我原本以為透過繼承機制,FirstChoicePlayer
的兩個 field
變數都可以直接在 ChangeChoicePlayer
內來使用。但結果是不行,必須在透過 FirstChoicePlayer
內定義的 getter method 才行。這表示由 field
所定義出來的便是是私有於該類別而已。
在所有類別中有一項共同參數 $doors
,也就是這遊戲的門的數量。Game
類別是依 $doors
來隨機生成出贏門的號碼。兩個玩家類別內則是以數字去隨機產生 firstChoice
。GameMaster
類別則是以此資訊來建構先的 Game
物件,還有推算出能告訴玩家哪些門是輸門。這邊我用了 :param
這個添在 field
變數上的屬性 (field $doors :param;
),來代表說這 field
的值必須被提供給類別的建構函式 new
。這用法尚算直覺。而其他依賴 $doors
的值所計算出來的各 field
都似乎是正確地在物件建構時期被計算完畢了。無須另外調整。
目前幾個有需要用到的 getter method 都是我自己手動補上的,讓程式碼看來量很多。但依 perlclass 文件中 TODO 一節 內的說明,看來以後能寫個 :reader
來讓 perl 自動生成出 getter method。就能省下不少打字的力氣。
雖然 perlclass 語法跟類別機能十分基本,比不上其他以物件導向為核心設計的語言。但以這個小程式的用途用起來沒意外,很夠用。等以後改版之後再來看看有沒有什麼更有意思的地方。