Хабр точно знаком с парадоксом, а вот с некоторыми фичами павершелла, вероятно, нет, поэтому тут больше про него.
Используем пайплайн в Powershell
Алгоритм прост, первым идет генератор случайных дверей, затем генератор выбора пользователя, затем логика открытия дверей ведущим, еще одно действие пользователя и подсчет статистики.
А поможет нам в этом павершельный ValueFromPipeline
, который позволяет указывать командлет один за другим, трансформируя объект шаг за шагом. Вот примерно должен выглядеть наш пайплайн:
New-Doors | Select-Door | Open-Door | Invoke-UserAction
New-Doors
генерирует новые двери, в команде Select-Door
игрок выбирает одну из дверей, в Open-Door
ведущий открывает дверь в которой точно нет козы и которая не была выбрана игроком, а в Invoke-UserAction
мы симулируем разное поведение пользователя.
Объект, описывающий двери, подается слева направо постепенно преобразовываясь.
Такой метод написания кода помогает разделять его на куски с четким разделением по ответственности.
В Powershell есть свои конвенции. В том числе, конвенции по правильному наименованию функций, их тоже нужно соблюдать и мы их почти соблюдаем.
Делаем двери
Так как мы собираемся симулировать ситуацию, подробно опишем еще и двери.
Дверь содержит либо козу, либо автомобиль. Дверь может быть выбрана игроком или открыта ведущим.
class Door {
<#
Модель данных, где описана каждая дверь.
Выбрана ли она игроком и открыта ли она ведущим.
#>
[string]$Contains = "Goat"
[bool]$Selected = $false
[bool]$Opened = $false
}
Каждую из дверей мы поместим в отдельное поле в отдельном классе.
class Doors {
<#
Модель данных, где описаны 3 двери
#>
[Door]$DoorOne
[Door]$DoorTwo
[Door]$DoorThree
}
Можно было их поместить все двери в массив, но чем подробнее все будет описано, тем, лучше. Кстати в Powershell 7, классы, их конструкторы, методы и все остальное ООП, которое работает почти как надо, но об этом в другой раз.
Генератор случайных дверей выглядит так. Сначала для каждого дверного косяка генерируется своя дверь, а потом генератор выбирает за которой из них будет стоят автомобиль.
function New-Doors {
<#
Генератор случайных дверей.
#>
$i = [Doors]::new()
$i.DoorOne = [Door]::new()
$i.DoorTwo = [Door]::new()
$i.DoorThree = [Door]::new()
switch ( Get-Random -Maximum 3 -Minimum 0 ) {
0 {
$i.DoorOne.Contains = "Car"
}
1 {
$i.DoorTwo.Contains = "Car"
}
2 {
$i.DoorThree.Contains = "Car"
}
Default {
Write-Error "Something in door generator went wrong"
break
}
}
return $i
Наш пайп выглядит так:
New-Doors
Игрок выбирает дверь
Теперь опишем изначальный выбор. Игрок может выбрать одну из трех дверей. Для целей симуляции большего количества ситуаций, пусть игрок сможет выбирать только первую, только вторую, только третью и случайную дверь каждый раз.
[Parameter(Mandatory)]
[ValidateSet("First", "Second", "Third", "Random")]
$Principle
Чтобы принимать аргументы из пайплайна, в блоке параметров нужно указать переменную, которая будет это делать. Делается это так:
[parameter(ValueFromPipeline)]
[Doors]$i
Можно писать ValueFromPipeline
без True
.
Вот так выглядит законченный блок выбора двери:
function Select-Door {
<#
Игрок выбирает дверь.
#>
Param (
[parameter(ValueFromPipeline)]
[Doors]$i,
[Parameter(Mandatory)]
[ValidateSet("First", "Second", "Third", "Random")]
$Principle
)
switch ($Principle) {
"First" {
$i.DoorOne.Selected = $true
}
"Second" {
$i.DoorTwo.Selected = $true
}
"Third" {
$i.DoorThree.Selected = $true
}
"Random" {
switch ( Get-Random -Maximum 3 -Minimum 0 ) {
0 {
$i.DoorOne.Selected = $true
}
1 {
$i.DoorTwo.Selected = $true
}
2 {
$i.DoorThree.Selected = $true
}
Default {
Write-Error "Something in door selector went wrong"
break
}
}
}
Default {
Write-Error "Something in door selector went wrong"
break
}
}
return $i
Наш пайп выглядит так:
New-Doors | Select-Door -Principle Random
Ведущий открывает дверь
Тут все очень просто. Если дверь не была выбрана игроком и если за ней коза, то меняем поле Opened
на True
. Конкретно в это случае называть команду словом Open
не корректно, вызываемый ресурс не читается, а изменяется. В подобных случаях используйте Set
, а Open
оставим для наглядности.
function Open-Door {
<#
Ведущий открывает дверь с козой, но не ту, что выбрал игрок.
#>
Param (
[parameter(ValueFromPipeline)]
[Doors]$i
)
switch ($false) {
$i.DoorOne.Selected {
if ($i.DoorOne.Contains -eq "Goat") {
$i.DoorOne.Opened = $true
continue
}
}
$i.DoorTwo.Selected {
if ($i.DoorTwo.Contains -eq "Goat") {
$i.DoorTwo.Opened = $true
continue
}
}
$i.DoorThree.Selected {
if ($i.DoorThree.Contains -eq "Goat") {
$i.DoorThree.Opened = $true
continue
}
}
}
return $i
Для пущей убедительности нашей симуляции мы «открываем» эту дверь, меняя поле .opened на $true
, а не удаляем объект из массива дверей.
Не забывайте про continue
в свитчах, сравнение не останавливается после первого совпадения. Coninue
выходит из свитча и продолжает выполнять скрипт, а оператор break
в свитче завершит работу скрипта.
Добавляем еще одну функцию в пайп, он он теперь выглядит так:
New-Doors | Select-Door -Principle Random | Open-Door
Игрок меняет выбор
Игрок либо меняет дверь, либо не меняет. В блоке параметров у нас только переменная из пайпа и булёвый аргумент.
Используйте слово Invoke
в названиях таких функций, потому что Invoke
означает вызов синхронной операции, а Start
асинхронной, соблюдайте конвенции и рекомендации.
function Invoke-UserAction {
<#
Ситуация, где игрок менят или не меняет свой выбор.
#>
Param (
[parameter(ValueFromPipeline)]
[Doors]$i,
[Parameter(Mandatory)]
[bool]$SwitchDoor
)
if ($true -eq $SwitchDoor) {
switch ($false) {
$i.DoorOne.Opened {
if ( $i.DoorOne.Selected ) {
$i.DoorOne.Selected = $false
}
else {
$i.DoorOne.Selected = $true
}
}
$i.DoorTwo.Opened {
if ( $i.DoorTwo.Selected ) {
$i.DoorTwo.Selected = $false
}
else {
$i.DoorTwo.Selected = $true
}
}
$i.DoorThree.Opened {
if ( $i.DoorThree.Selected ) {
$i.DoorThree.Selected = $false
}
else {
$i.DoorThree.Selected = $true
}
}
}
}
return $i
В операторах ветвления и сравнения, нужно первыми указывать системные и статические переменные. Вероятно, могут возникнуть сложности с приведением одного объекта к другому, но автор не сталкивался с такими трудностями, когда раньше писал по-другому.
Еще одна функция в пайплайн.
New-Doors | Select-Door -Principle Random | Open-Door | Invoke-UserAction -SwitchDoor $True
Преимущество такого подхода написания ясно, ведь разделять код на части с четким разделением функций никогда не было так удобно.
Поведение игрока
Как часто игрок меняет дверь. Предусмотрены 5 линий поведения:
Never
– игрок никогда не меняет свой выборFifty-Fifty
– 50 на 50. Количество симуляций делится на два прохода. Первый проход игрок не меняет дверь, второй проход меняет.Random
– в каждой новой симуляции игрок подкидывает монеткуAlways
– игрок всегда меняет свой выбор.Ration
– игрок меняет выбор в N% случаях.
switch ($SwitchDoors) {
"Never" {
0..$Count | ForEach-Object {
$Win += Invoke-Simulation -Door $Door -SwitchDoors $false
}
continue
}
"FiftyFifty" {
$Fifty = [math]::Round($Count / 2)
0..$Fifty | ForEach-Object {
$Win += Invoke-Simulation -Door $Door -SwitchDoors $false
}
0..$Fifty | ForEach-Object {
$Win += Invoke-Simulation -Door $Door -SwitchDoors $true
}
continue
}
"Random" {
0..$Count | ForEach-Object {
[bool]$Random = Get-Random -Maximum 2 -Minimum 0
$Win += Invoke-Simulation -Door $Door -SwitchDoors $Random
}
continue
}
"Always" {
0..$Count | ForEach-Object {
$Win += Invoke-Simulation -Door $Door -SwitchDoors $true
}
continue
}
"Ratio" {
$TrueRatio = $Ratio / 100 * $Count
$FalseRatio = $Count - $TrueRatio
0..$TrueRatio | ForEach-Object {
$Win += Invoke-Simulation -Door $Door -SwitchDoors $true
}
0..$FalseRatio | ForEach-Object {
$Win += Invoke-Simulation -Door $Door -SwitchDoors $false
}
continue
}
}
ForEach-Object
в Powershell 7 работает значительно быстрее цикла for
, плюс, может быть распараллелен, поэтому тут используется вместо цикла for
.
Оформляем командлет
Теперь нужно правильно дооформить командлет. Первым делом, нужно сделать валидацию входящих аргументов. Бонус не только в том, что человек не может ввести неверный аргумент в поле, но еще список всех доступных аргументов появляется в подсказках.
Так выглядит код в блоке параметров:
param (
[Parameter(Mandatory = $false,
HelpMessage = "How often the player changes his choice.")]
[ValidateSet("Never", "FiftyFifty", "Random", "Always", "Ratio")]
$SwitchDoors = "Random"
)
Так выглядит подсказка:
Перед блоком параметров можно сделать comment based help
. Вот так выглядит код перед блоком параметров:
<#
.SYNOPSIS
Performs monty hall paradox simulation.
.DESCRIPTION
The Invoke-MontyHallParadox.ps1 script invoke monty hall paradox simulation.
.PARAMETER Door
Specifies door the player will choose during the entire simulation
.PARAMETER SwitchDoors
Specifies principle how the player changes his choice.
.PARAMETER Count
Specifies how many times to run the simulation.
.PARAMETER Ratio
If -SwitchDoors Ratio, specifies how often the player changes his choice. As a percentage."
.INPUTS
None. You cannot pipe objects to Update-Month.ps1.
.OUTPUTS
None. Update-Month.ps1 does not generate any output.
.EXAMPLE
PS> Invoke-MontyHallParadox -SwitchDoors Always -Count 10000
#>
Вот так выглядит сама подсказка:
Запускаем симуляцию
Результаты симуляции:
Если человек никогда не меняет свой выбор, то он побеждает в 33,37% случаев.
В случае двух проходов, в половине которых мы отказываемся менять свой выбор, шансы на победу составляют 49.9134%, что очень близко к ровным 50%.
В случае подкидывания монетки ничего не меняется, шанс на победу остается в районе 50,131%.
Ну а если игрок всегда меняет свой выбор, шанс на победу повышается до 66,6184%, иными словами, скучно и ничего нового.
Производительность:
Что касается производительности. Скрипт кажется не оптимальным. String
вместо Bool
, много разных функций со свитчаим внутри, передающих друг другу объект, но тем не менее, вот результаты Measure-Command
по этому скрипту и скрипту от другого автора.
Сравнение проводилось на двух системах, везде стоял pwsh 7.1, 100 000 проходов.
▍I5-5200u
Этот алгоритм:
Days : 0
Hours : 0
Minutes : 0
Seconds : 4
Milliseconds : 581
Ticks : 45811819
TotalDays : 5,30229386574074E-05
TotalHours : 0,00127255052777778
TotalMinutes : 0,0763530316666667
TotalSeconds : 4,5811819
TotalMilliseconds : 4581,1819
Тот алгоритм:
Days : 0
Hours : 0
Minutes : 0
Seconds : 5
Milliseconds : 104
Ticks : 51048392
TotalDays : 5,9083787037037E-05
TotalHours : 0,00141801088888889
TotalMinutes : 0,0850806533333333
TotalSeconds : 5,1048392
TotalMilliseconds : 5104,8392
▍I9-9900K
Этот алгоритм:
Days : 0
Hours : 0
Minutes : 0
Seconds : 1
Milliseconds : 891
Ticks : 18917629
TotalDays : 2,18954039351852E-05
TotalHours : 0,000525489694444444
TotalMinutes : 0,0315293816666667
TotalSeconds : 1,8917629
TotalMilliseconds : 1891,7629
Тот алгоритм:
Days : 0
Hours : 0
Minutes : 0
Seconds : 1
Milliseconds : 954
Ticks : 19543236
TotalDays : 2,26194861111111E-05
TotalHours : 0,000542867666666667
TotalMinutes : 0,03257206
TotalSeconds : 1,9543236
TotalMilliseconds : 1954,3236
Преимущество 63 мс, но результаты все равно очень странные, учитывая сколько раз в скрипте сравниваются строки.
Автор надеется, что эта статья послужит убедительным примером для тех, кто считает что шансы всегда составляют 50 на 50, ну а ознакомиться с кодом вы можете под этим спойлером.
<#
Модель данных, где описаны 3 двери
#>
[Door]$DoorOne
[Door]$DoorTwo
[Door]$DoorThree
}
class Door {
<#
Модель данных, где описана каждая дверь.
Выбрана ли она игроком и открыта ли она ведущим.
#>
[string]$Contains = «Goat»
[bool]$Selected = $false
[bool]$Opened = $false
}
function New-Doors {
<#
Генератор случайных дверей.
#>
$i = [Doors]::new()
$i.DoorOne = [Door]::new()
$i.DoorTwo = [Door]::new()
$i.DoorThree = [Door]::new()
switch ( Get-Random -Maximum 3 -Minimum 0 ) {
0 {
$i.DoorOne.Contains = «Car»
}
1 {
$i.DoorTwo.Contains = «Car»
}
2 {
$i.DoorThree.Contains = «Car»
}
Default {
Write-Error «Something in door generator went wrong»
break
}
}
return $i
}
function Select-Door {
<#
Игрок выбирает дверь.
#>
Param (
[parameter(ValueFromPipeline)]
[Doors]$i,
[Parameter(Mandatory)]
[ValidateSet(«First», «Second», «Third», «Random»)]
$Principle
)
switch ($Principle) {
«First» {
$i.DoorOne.Selected = $true
continue
}
«Second» {
$i.DoorTwo.Selected = $true
continue
}
«Third» {
$i.DoorThree.Selected = $true
continue
}
«Random» {
switch ( Get-Random -Maximum 3 -Minimum 0 ) {
0 {
$i.DoorOne.Selected = $true
continue
}
1 {
$i.DoorTwo.Selected = $true
continue
}
2 {
$i.DoorThree.Selected = $true
continue
}
Default {
Write-Error «Something in selector generator went wrong»
break
}
}
continue
}
Default {
Write-Error «Something in door selector went wrong»
break
}
}
return $i
}
function Open-Door {
<#
Ведущий открывает дверь с козой, но не ту, что выбрал игрок.
#>
Param (
[parameter(ValueFromPipeline)]
[Doors]$i
)
switch ($false) {
$i.DoorOne.Selected {
if ($i.DoorOne.Contains -eq «Goat») {
$i.DoorOne.Opened = $true
continue
}
}
$i.DoorTwo.Selected {
if ($i.DoorTwo.Contains -eq «Goat») {
$i.DoorTwo.Opened = $true
continue
}
}
$i.DoorThree.Selected {
if ($i.DoorThree.Contains -eq «Goat») {
$i.DoorThree.Opened = $true
continue
}
}
}
return $i
}
function Invoke-UserAction {
<#
Ситуация, где игрок менят или не меняет свой выбор.
#>
Param (
[parameter(ValueFromPipeline)]
[Doors]$i,
[Parameter(Mandatory)]
[bool]$SwitchDoor
)
if ($true -eq $SwitchDoor) {
switch ($false) {
$i.DoorOne.Opened {
if ( $i.DoorOne.Selected ) {
$i.DoorOne.Selected = $false
}
else {
$i.DoorOne.Selected = $true
}
}
$i.DoorTwo.Opened {
if ( $i.DoorTwo.Selected ) {
$i.DoorTwo.Selected = $false
}
else {
$i.DoorTwo.Selected = $true
}
}
$i.DoorThree.Opened {
if ( $i.DoorThree.Selected ) {
$i.DoorThree.Selected = $false
}
else {
$i.DoorThree.Selected = $true
}
}
}
}
return $i
}
function Get-Win {
Param (
[parameter(ValueFromPipeline)]
[Doors]$i
)
switch ($true) {
($i.DoorOne.Selected -and $i.DoorOne.Contains -eq «Car») {
return $true
}
($i.DoorTwo.Selected -and $i.DoorTwo.Contains -eq «Car») {
return $true
}
($i.DoorThree.Selected -and $i.DoorThree.Contains -eq «Car») {
return $true
}
default {
return $false
}
}
}
function Invoke-Simulation {
param (
[Parameter(Mandatory = $false,
HelpMessage = «Which door the player will choose during the entire simulation.»)]
[ValidateSet(«First», «Second», «Third», «Random»)]
$Door = «Random»,
[bool]$SwitchDoors
)
return New-Doors | Select-Door -Principle $Door | Open-Door | Invoke-UserAction -SwitchDoor $SwitchDoors | Get-Win
}
function Invoke-MontyHallParadox {
<#
.SYNOPSIS
Performs monty hall paradox simulation.
.DESCRIPTION
The Invoke-MontyHallParadox.ps1 script invoke monty hall paradox simulation.
.PARAMETER Door
Specifies door the player will choose during the entire simulation
.PARAMETER SwitchDoors
Specifies principle how the player changes his choice.
.PARAMETER Count
Specifies how many times to run the simulation.
.PARAMETER Ratio
If -SwitchDoors Ratio, specifies how often the player changes his choice. As a percentage."
.INPUTS
None. You cannot pipe objects to Update-Month.ps1.
.OUTPUTS
None. Update-Month.ps1 does not generate any output.
.EXAMPLE
PS> Invoke-MontyHallParadox -SwitchDoors Always -Count 10000
#>
param (
[Parameter(Mandatory = $false,
HelpMessage = «Which door the player will choose during the entire simulation.»)]
[ValidateSet(«First», «Second», «Third», «Random»)]
$Door = «Random»,
[Parameter(Mandatory = $false,
HelpMessage = «How often the player changes his choice.»)]
[ValidateSet(«Never», «FiftyFifty», «Random», «Always», «Ratio»)]
$SwitchDoors = «Random»,
[Parameter(Mandatory = $false,
HelpMessage = «How many times to run the simulation.»)]
[uint32]$Count = 10000,
[Parameter(Mandatory = $false,
HelpMessage = «How often the player changes his choice. As a percentage.»)]
[uint32]$Ratio = 30
)
[uint32]$Win = 0
switch ($SwitchDoors) {
«Never» {
0..$Count | ForEach-Object {
$Win += Invoke-Simulation -Door $Door -SwitchDoors $false
}
continue
}
«FiftyFifty» {
$Fifty = [math]::Round($Count / 2)
0..$Fifty | ForEach-Object {
$Win += Invoke-Simulation -Door $Door -SwitchDoors $false
}
0..$Fifty | ForEach-Object {
$Win += Invoke-Simulation -Door $Door -SwitchDoors $true
}
continue
}
«Random» {
0..$Count | ForEach-Object {
[bool]$Random = Get-Random -Maximum 2 -Minimum 0
$Win += Invoke-Simulation -Door $Door -SwitchDoors $Random
}
continue
}
«Always» {
0..$Count | ForEach-Object {
$Win += Invoke-Simulation -Door $Door -SwitchDoors $true
}
continue
}
«Ratio» {
$TrueRatio = $Ratio / 100 * $Count
$FalseRatio = $Count — $TrueRatio
0..$TrueRatio | ForEach-Object {
$Win += Invoke-Simulation -Door $Door -SwitchDoors $true
}
0..$FalseRatio | ForEach-Object {
$Win += Invoke-Simulation -Door $Door -SwitchDoors $false
}
continue
}
}
Write-Output («Player won in » + $Win + " times out of " + $Count)
Write-Output («Whitch is » + ($Win / $Count * 100) + "%")
return $Win
}
#Invoke-MontyHallParadox -SwitchDoors Always -Count 500000
Автор: oldadmin