Escribir cmdlets de Powershell correctamente y simular la paradoja de Monty Hall

Habr definitivamente está familiarizado con la paradoja, pero probablemente no con algunas características del adoquín, así que aquí hay más información al respecto.









Usando la canalización en Powershell



El algoritmo es simple, el primero es el generador de puerta aleatorio, luego el generador de elección del usuario, luego la lógica de apertura de la puerta del presentador, otra acción del usuario y el conteo de estadísticas.



Y ayúdanos con esto es la herramienta eléctrica ValueFromPipeline



, que nos permite especificar el cmdlet uno a uno, transformando el objeto paso a paso. Así es como debería verse nuestra canalización:



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





New-Doors



genera nuevas puertas, en el equipo el Select-Door



jugador elige una de las puertas, el Open-Door



líder abre la puerta en la que definitivamente no hay cabra y que no fue elegida por el jugador, y en Invoke-UserAction



nosotros simulamos diferentes comportamientos del usuario.



El objeto que describe la puerta se mueve de izquierda a derecha, transformándose gradualmente.



Este método de escribir código ayuda a mantenerlo en pedazos con claras divisiones de responsabilidad.



Powershell tiene sus propias convenciones. Incluidas las convenciones sobre la denominación correcta de funciones , también es necesario observarlas y casi las cumplimos.



Haciendo puertas



Como vamos a simular la situación, también describiremos en detalle las puertas.



La puerta contiene una cabra o un automóvil. La puerta puede ser elegida por el jugador o abierta por el anfitrión.



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

      
      





Colocaremos cada una de las puertas en un campo separado en una clase separada.



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





Fue posible poner todas las puertas en una matriz, pero cuanto más detallado se describa todo, mejor. Por cierto, en Powershell 7, las clases, sus constructores, métodos y todo lo demás es OOP, que funciona casi como debería, pero más sobre eso en otro momento. 



El generador de puerta aleatorio se ve así. Primero, para cada jamba de puerta, se genera su propia puerta, y luego el generador elige en cuál de ellas se colocará el automóvil.



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

      
      





Nuestra pipa se ve así:



New-Doors
      
      





El jugador elige la puerta



Ahora describamos la elección inicial. El jugador puede elegir una de las tres puertas. Con el fin de simular más situaciones, deje que el jugador elija solo la primera, solo la segunda, solo la tercera y puerta aleatoria cada vez. 



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





Para aceptar argumentos de la canalización, debe especificar una variable en el bloque de parámetros que hará esto. Esto se hace así:



[parameter(ValueFromPipeline)]
[Doors]$i
      
      





Puedes escribir ValueFromPipeline



sin True



.



Así es como se ve el bloque de selección de puerta terminado:



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 

      
      





Nuestra pipa se ve así:



New-Doors | Select-Door -Principle Random
      
      





Liderar abre la puerta



Aquí todo es muy sencillo. Si el jugador no eligió la puerta y si hay una cabra detrás de ella, cambie el campo Opened



a True



. Específicamente, en este caso, no es Open



correcto nombrar el comando con una palabra , el recurso llamado no se lee, sino que se cambia. En tales casos, use Set



, pero Open



déjelo para mayor claridad.



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 hacer nuestra simulación más convincente, "abrimos" esta puerta cambiando el campo .opened a en $true



lugar de eliminar el objeto de la matriz de puertas.



No te olvides de los continue



interruptores, la comparación no se detiene después del primer partido. Coninue



sale del conmutador y continúa ejecutando el script, y el operador break



del conmutador termina el script.



Agregue una función más a la tubería, ahora se ve así:



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





El jugador cambia su elección 



El jugador cambia la puerta o no la cambia. En el bloque de parámetros, solo tenemos una variable de la tubería y un argumento booleano. 



Use la palabra Invoke



en los nombres de dichas funciones, porque Invoke



significa llamar a una operación sincrónica y Start



asincrónica, siga las convenciones y recomendaciones.



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

      
      





En los operadores de ramificación y comparación, primero debe especificar el sistema y las variables estáticas. Probablemente, puede haber dificultades para convertir un objeto en otro, pero el autor no encontró tales dificultades cuando antes escribió de una manera diferente.



Otra función en la tubería.



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





La ventaja de este método de escritura es clara, porque nunca ha sido tan conveniente dividir el código en partes con una clara separación de funciones.



Comportamiento del jugador



Con qué frecuencia el jugador cambia de puerta. Hay 5 líneas de comportamiento:



  1. Never



    - el jugador nunca cambia su elección
  2. Fifty-Fifty



    - 50 a 50. El número de simulaciones se divide en dos pasadas. El primer pase el jugador no cambia la puerta, el segundo pase cambia.
  3. Random



    - en cada nueva simulación, el jugador lanza una moneda
  4. Always



    - el jugador siempre cambia su elección.
  5. Ration



    - el jugador cambia su elección en un N% de 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



en Powershell 7 funciona mucho más rápido que un bucle for



, además de que se puede paralelizar, por lo que se usa aquí en lugar de un bucle for



.



Aplicar estilo al cmdlet



Ahora necesita corregir el cmdlet. En primer lugar, debe realizar la validación de los argumentos entrantes. La ventaja no es solo que una persona no puede ingresar un argumento no válido en el campo, sino que aparece una lista de todos los argumentos disponibles en las solicitudes.



Así es como se ve el código en el bloque de parámetros:



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

      
      





Esta es la pista:





Antes de que se pueda realizar el bloque de parámetros comment based help



. Así es como se ve el código antes del bloque 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
   
      #>

      
      





Así es como se ve el mensaje:





Ejecutando la simulación



Resultados de la simulación:





Si una persona nunca cambia su elección, gana el 33,37% de las veces.



En el caso de dos pases, en la mitad de los cuales nos negamos a cambiar nuestra elección, las posibilidades de ganar son del 49,9134%, que está muy cerca de exactamente el 50%.



En el caso de un lanzamiento de moneda, nada cambia, la posibilidad de ganar se mantiene alrededor del 50,131%.



Pues si el jugador cambia siempre de elección, la posibilidad de ganar se eleva al 66,6184%, es decir, aburrido y nada nuevo.



Rendimiento:



en términos de rendimiento. El guión no parece ser el óptimo. String



en cambio Bool



, muchas funciones diferentes con un interruptor adentro, pasándose un objeto entre sí, pero sin embargo, aquí están los resultados Measure-Command



para este guión y un guión de otro autor .



La comparación se llevó a cabo en dos sistemas, pwsh 7.1 estaba en todas partes, 100.000 pases.



▍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
      
      





Ese 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
      
      





Ese 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
      
      





63 ms de ventaja, pero los resultados siguen siendo muy extraños teniendo en cuenta la cantidad de veces que el script compara cadenas.



El autor espera que este artículo sirva como un ejemplo convincente para aquellos que creen que las probabilidades son siempre de 50 a 50, pero puede leer el código en este spoiler.



Todo el 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