perlclass 初探: 以模擬 Monty Hall Problem 為例

作者:   發佈於:   #perl

Monty 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:

Game 物件在建構、初始完畢後計基本上是個常數了,沒有什麼特別的地方。 FirstChoicePlayer 也差不多,只是會有個 @loosingDoors 內部狀態來記著那些由 GameMaster 提供的輸門號碼。

而此處我讓 ChangeChoicePlayerFirstChoicePlayer 的延伸,依照 $self->firstChoice$self->loosingDoors 的內容來推算出 finalChoice。這邊我原本以為透過繼承機制,FirstChoicePlayer 的兩個 field 變數都可以直接在 ChangeChoicePlayer 內來使用。但結果是不行,必須在透過 FirstChoicePlayer 內定義的 getter method 才行。這表示由 field 所定義出來的便是是私有於該類別而已。

在所有類別中有一項共同參數 $doors,也就是這遊戲的門的數量。Game 類別是依 $doors 來隨機生成出贏門的號碼。兩個玩家類別內則是以數字去隨機產生 firstChoiceGameMaster 類別則是以此資訊來建構先的 Game 物件,還有推算出能告訴玩家哪些門是輸門。這邊我用了 :param 這個添在 field 變數上的屬性 (field $doors :param;),來代表說這 field 的值必須被提供給類別的建構函式 new。這用法尚算直覺。而其他依賴 $doors 的值所計算出來的各 field 都似乎是正確地在物件建構時期被計算完畢了。無須另外調整。

目前幾個有需要用到的 getter method 都是我自己手動補上的,讓程式碼看來量很多。但依 perlclass 文件中 TODO 一節 內的說明,看來以後能寫個 :reader 來讓 perl 自動生成出 getter method。就能省下不少打字的力氣。

雖然 perlclass 語法跟類別機能十分基本,比不上其他以物件導向為核心設計的語言。但以這個小程式的用途用起來沒意外,很夠用。等以後改版之後再來看看有沒有什麼更有意思的地方。