Escrever cmdlets Powershell corretamente e simular o paradoxo de Monty Hall

Habr está definitivamente familiarizado com o paradoxo, mas provavelmente não com alguns recursos da concha de pavimentação, então aqui está mais sobre ele.









Usando o pipeline no Powershell



O algoritmo é simples, o primeiro é o gerador de porta aleatória, depois o gerador de escolha do usuário, depois a lógica de abertura da porta do apresentador, outra ação do usuário e contagem de estatísticas.



E a ferramenta nos ajudará com isso ValueFromPipeline



, o que nos permite especificar o cmdlet um a um, transformando o objeto passo a passo. É assim que nosso pipeline deve ser:



New-Doors | Select-Door | Open-Door | Invoke-UserAction
      
      





New-Doors



gera novas portas, na equipa o Select-Door



jogador escolhe uma das portas, o Open-Door



líder abre a porta em que definitivamente não há cabra e que não foi escolhida pelo jogador, e Invoke-UserAction



aí simulamos diferentes comportamentos do utilizador.



O objeto que descreve a porta se move da esquerda para a direita, transformando-se gradualmente.



Esse método de escrever código ajuda a mantê-lo em pedaços com divisões claras de responsabilidade.



O Powershell tem suas próprias convenções. Incluindo as convenções sobre a nomenclatura correta das funções , elas também precisam ser observadas e quase as cumprimos.



Fazendo portas



Como vamos simular a situação, também descreveremos em detalhes as portas.



A porta contém uma cabra ou um carro. A porta pode ser escolhida pelo jogador ou aberta pelo anfitrião.



class Door {
    <#
     ,    . 
            .
    #>
    [string]$Contains = "Goat"
    [bool]$Selected = $false
    [bool]$Opened = $false
}

      
      





Colocaremos cada uma das portas em um campo separado em uma classe separada.



class Doors {
    <#
     ,   3 
    #>
    [Door]$DoorOne 
    [Door]$DoorTwo 
    [Door]$DoorThree
}
      
      





Foi possível colocar todas as portas em uma matriz, mas quanto mais detalhado tudo for descrito, melhor. A propósito, no Powershell 7, classes, seus construtores, métodos e tudo o mais é OOP, que funciona quase como deveria, mas mais nisso em outra ocasião. 



O gerador de porta aleatória se parece com isso. Primeiro, para cada batente de porta, sua própria porta é gerada, e então o gerador escolhe em qual delas o carro ficará.



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

      
      





Nosso cachimbo se parece com isto:



New-Doors
      
      





O jogador escolhe a porta



Agora vamos descrever a escolha inicial. O jogador pode escolher uma das três portas. Com o propósito de simular mais situações, deixe o jogador escolher apenas a primeira, apenas a segunda, apenas a terceira e porta aleatória de cada vez. 



[Parameter(Mandatory)]
[ValidateSet("First", "Second", "Third", "Random")]
$Principle
      
      





Para aceitar argumentos do pipeline, você precisa especificar uma variável no bloco de parâmetros que fará isso. Isso é feito assim:



[parameter(ValueFromPipeline)]
[Doors]$i
      
      





Você pode escrever ValueFromPipeline



sem True



.



Esta é a aparência do bloco de seleção de porta acabado:



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 

      
      





Nosso cachimbo se parece com isto:



New-Doors | Select-Door -Principle Random
      
      





Liderando abre a porta



Tudo é muito simples aqui. Se a porta não foi escolhida pelo jogador e se houver uma cabra atrás dela, altere o campo Opened



para True



. Especificamente, neste caso, não é Open



correto nomear o comando com uma palavra , o recurso chamado não é lido, mas alterado. Nesses casos, use Set



, mas Open



deixe para maior clareza.



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

      
      





Para tornar nossa simulação mais convincente, "abrimos" essa porta alterando o campo .opened para, em $true



vez de remover o objeto da matriz da porta.



Não se esqueça dos continue



interruptores, a comparação não termina após a primeira partida. Coninue



sai do switch e continua a executar o script, e o operador break



no switch encerra o script.



Adicione mais uma função ao tubo, agora se parece com isto:



New-Doors | Select-Door -Principle Random | Open-Door
      
      





O jogador muda sua escolha 



O jogador muda a porta ou não a muda. No bloco de parâmetros, temos apenas uma variável do tubo e um argumento booleano. 



Use a palavra Invoke



nos nomes dessas funções, pois Invoke



significa chamar uma operação síncrona, e Start



assíncrona, siga as convenções e recomendações.



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

      
      





Nos operadores de ramificação e comparação, você deve primeiro especificar o sistema e as variáveis ​​estáticas. Provavelmente, pode haver dificuldade em converter um objeto em outro, mas o autor não encontrou essas dificuldades quando escreveu de uma maneira diferente antes.



Outra função no pipeline.



New-Doors | Select-Door -Principle Random | Open-Door | Invoke-UserAction -SwitchDoor $True
      
      





A vantagem dessa abordagem de escrita é clara, porque nunca foi tão conveniente dividir o código em partes com uma separação clara de funções.



Comportamento do jogador



Quantas vezes o jogador muda a porta. Existem 5 linhas de comportamento:



  1. Never



    - o jogador nunca muda sua escolha
  2. Fifty-Fifty



    - 50 a 50. O número de simulações é dividido em duas passagens. Na primeira passagem o jogador não muda a porta, na segunda passagem muda.
  3. Random



    - em cada nova simulação, o jogador joga uma moeda
  4. Always



    - o jogador sempre muda sua escolha.
  5. Ration



    - o jogador muda sua escolha em N% dos casos.


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



no Powershell 7 ele funciona muito mais rápido do que um loop for



, além de poder ser paralelizado, então é usado aqui ao invés de um loop for



.



Estilizando o cmdlet



Agora você precisa corrigir o cmdlet. Primeiro de tudo, você precisa fazer a validação dos argumentos recebidos. O bônus não é apenas que uma pessoa não pode inserir um argumento inválido no campo, mas também uma lista de todos os argumentos disponíveis aparece nos prompts.



É assim que o código no bloco de parâmetros se parece:



param (
        [Parameter(Mandatory = $false,
            HelpMessage = "How often the player changes his choice.")]
        [ValidateSet("Never", "FiftyFifty", "Random", "Always", "Ratio")]
        $SwitchDoors = "Random"
    )

      
      





Esta é a dica:





Antes que o bloco de parâmetros possa ser feito comment based help



. Esta é a aparência do código antes do bloco de parâmetros:




  <#
      .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
   
      #>

      
      





Esta é a aparência do prompt:





Executando a simulação



Resultados simulados:





Se uma pessoa nunca muda sua escolha, ela ganha 33,37% das vezes.



No caso de dois passes, em que na metade nos recusamos a mudar de escolha, as chances de vitória são de 49,9134%, o que é muito próximo de exatamente 50%.



No caso de sorteio, nada muda, a chance de vitória fica em torno de 50,131%.



Pois bem, se o jogador sempre muda sua escolha, a chance de ganhar sobe para 66,6184%, ou seja, chata e nada de novo.



Desempenho:



Em termos de desempenho. O script não parece ser o ideal. String



em vez disso Bool



, muitas funções diferentes com um switch dentro, passando um objeto para o outro, mas, no entanto, aqui estão os resultados Measure-Command



para este script e um script de outro autor .



A comparação foi realizada em dois sistemas, o pwsh 7.1 estava em todos os lugares, 100.000 passes.



▍I5-5200u



Este algoritmo:



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
      
      





Esse algoritmo:



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



Este algoritmo:



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
      
      





Esse algoritmo:



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
      
      





Vantagem de 63 ms, mas os resultados ainda são muito estranhos considerando quantas vezes o script compara strings.



O autor espera que este artigo sirva como um exemplo convincente para aqueles que acreditam que as probabilidades são sempre de 50 a 50, mas você pode ler o código neste spoiler.



Todo o código
class Doors {

<#

, 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












All Articles