PowerShell
パターンマッチ

PowerShellのswitch文でパターンマッチっぽいことをしてみる

More than 1 year has passed since last update.

下記の記事を読んで、最高だなって思ったので、思わず闇の深いコードを書いてしまった。ネタコードなので解説は書かない。

パターンマッチっぽいスイッチ文

$case = $CMaker.New(($CMaker.Scope|iex))
switch ((Get-Date "2017/09/30"))
{
    # 型の比較
    $case.Is([int]).Get
        {"Not Match"}

    $case.Is([DateTime]).Get
        {"Match:[DateTime]"}

    # 値の比較
    $case.Property(@{Year = 1990}).Get
        {"Not Match"}

    $case.Property(@{Year = 2017}).Get
        {"Match:Year = 2017"}

    $case.Property(@{Year = 2017; DayOfWeek = "Saturday"}).Get
        {"Match:Year = 2017, DayOfWeek = Saturday"}

    # 束縛
    $case.Property(@{Year = {y}; DayOfWeek = {dow}}).Get
        {"Match:Year = ${y}, DayOfWeek = ${dow}"}

    # ガード
    $case.If({$_.Year -lt 1990}).Get
        {"Not Match"}

    $case.If({$_.Year -gt 1990}).Get
        {"Match:Year > 1990"}

    # 組み合わせ
    $case.Is([DateTime]).Property(@{Year = {y}}).If({$y -gt 1990}).Get
        {"Match Year > 1990"}

    default {"Not Match"}
}

出力結果

Match:[DateTime]
Match:Year = 2017
Match:Year = 2017, DayOfWeek = Saturday
Match:Year = 2017, DayOfWeek = Saturday
Match:Year > 1990
Match Year > 1990

$CMakerの実装

v2で動かすことを念頭に置いた闇の深い実装。

$CMaker = New-Module -AsCustomObject -ScriptBlock {
    $Script:ScopeScript = {param([string]$Name, $Value) Set-Variable @PSBoundParameters -Scope 1}

    Set-Variable -Scope Script -Option ReadOnly -Name Scope -Value `
        "{$($Script:ScopeScript.ToString())}"

    function New([ScriptBlock] $ScriptBlock)
    {
        $exp = $ScriptBlock.ToString()
        if ($exp -ne $Script:ScopeScript.ToString())
            {throw "無効なスコープ ${exp} が入力されました。"}

        $Type     = $null
        $Property = $null
        $If       = $null
        $Scope    = $ScriptBlock
        $Make     = $Script:Make -as [String]
        $Make | Invoke-Expression
    }

    Export-ModuleMember `
        -Variable Scope `
        -Function New

    $Script:Make = {
        New-Module -AsCustomObject -ArgumentList @($Type, $Property, $If, $Scope, $Make) -ScriptBlock {
            Param ([Type]$Type, [HashTable]$Property, [ScriptBlock]$If, [ScriptBlock]$Scope, [String]$Make)

            $Script:Get          = {case $_}
            ${function:IS}       = {Param([Type] $Type)          $Make | Invoke-Expression}
            ${function:Property} = {Param([HashTable] $Property) $Make | Invoke-Expression}
            ${function:If}       = {Param([ScriptBlock] $If)     $Make | Invoke-Expression}

            Export-ModuleMember `
                -Variable Get `
                -Function Is, Property, If

            function case ($InputObject)
            {
                if ($Type -ne $null -and $InputObject -isnot $Type)
                    {return $false}

                if ($Property -ne $null)
                {
                    foreach ($p in $Property.GetEnumerator())
                    {
                        $value = $InputObject."$($p.Name)"
                        if ($p.Value -is [ScriptBlock])
                            {& $Scope $p.Value $value}

                        elseif($p.Value -ne $value -or $value -ne $p.Value)
                            {return $false}
                    }
                }

                if ($If -ne $null -and -not (& $If))
                    {return $false}

                $true
            }
        }
    }
}