LoginSignup
5
1

More than 1 year has passed since last update.

Powershellでも(手軽に)unfoldしたい

Last updated at Posted at 2021-06-08

これは何?

F#で知ったunfoldがとても便利だったのでPowershellで実装しました。
unfoldは高階関数の一つです。リストを生成する関数定義と初期値を引数にとってコレクションを生成します。
whileループや再帰関数などの処理は、unfoldが有効な場合があります。

Powershellでの実装

こんな実装に落ち着いています。

filter unfold ([scriptblock]$generator) {
    $state = $_
    do {
        $element, $state = $generator.InvokeWithContext(
            # functionsToDefine
            $null,
            # variablesToDefine
            ([psvariable]::new('_', $state)),
            # args
            $null
        )
        Write-Output $element
    } while ($null -ne $state)
}

パイプライン入力される値を初期値として、引数generatorに指定された[scriptblock]が実行されます。
generatorの返り値は要素数2の配列を想定しており、以下のように処理されます。

  • 要素1…パイプライン出力
  • 要素2
    • $nullだった場合…unfold終了
    • $nullじゃなかった場合…要素2を$_に指定してgeneratorを実行

※本来のunfoldgeneratorの返り値自体の有無で繰り返しを判断しますが、使い勝手の都合で今の仕様になりました。

使い方

基本

数値を5まで出力したい場合はこうします。

参考:三項演算子の構文の使用

1 | unfold {
    if ($_ -le 5) {
        # パイプライン出力される値, 次の入力値として使われる値
        return $_, ($_ + 1)
    }
    else {
        # $nullで生成終了
        return $null
    }
}

1 | unfold {
    # 返り値なし→$nullなので、return $nullは不要
    if ($_ -le 5) { $_, ($_ + 1) }
}

1 | unfold {
    # 三項演算子
    $_ -le 5 ? $_, ($_ + 1) : $null
}

1 | unfold {
    # これでも良い
    $_ -le 5 ? $_, ($_ + 1) : $null,$null
}

結果はすべて同じです。

result
1
2
3
4
5

パイプラインで複数の値が入力された場合は。入力された要素それぞれを初期値とした複数のシーケンスが生成されます。

3..5 | unfold {
    # <condition> ? <if-true> : <if-false>
    $_ -le 5 ? $_, ($_ + 1) : $null
}

結果

result
3
4
5
4
5
5

返り値が$someVar,$nullだった場合は、$someVarが出力されてからunfoldが終了します。
本来のunfoldには無い機能です、……多分。

3..5 | unfold {
    $_ -le 5 ? $_, ($_ + 1) : 'end.',$null
}

結果

result
3
4
5
end.
4
5
end.
5
end.

フィボナッチ数列

6/10: 内容を修正、加筆

再帰関数の例題で有名なフィボナッチ数列を考えてみましょう。
まずは要素の値がn以上になるまで数列を生成する場合です。

$n = 1000

[Tuple]::Create(1, 1) | unfold {
    if ($_[1] -gt $n) {
        return $null
    }
    else {
        return ($_[0] + $_[1]), [Tuple]::Create($_[1], $_[0] + $_[1])
    }
}

結果

2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597

数列の順番によって生成を制御したい場合はgeneratorで頑張るよりも、unfoldで無限シーケンスを生成して、Select-Objectで制御するほうが楽です。


# 最初から3番目まで
[Tuple]::Create(1, 1)
| unfold { ($_[0] + $_[1]), [Tuple]::Create($_[1], $_[0] + $_[1])}
| Select-Object -First 3

# 0から始めて10番目まで
[Tuple]::Create(0, 1)
| unfold { $_[0], [Tuple]::Create($_[1], $_[0] + $_[1])}
| Select-Object -First 10

# 0から始めて50番目と100番目だけ
[Tuple]::Create(0, 1)
| unfold {$_[0], [Tuple]::Create($_[1], $_[0] + $_[1])}
| Select-Object -Index 50,100

結果

2
3
5

0
1
1
2
3
5
8
13
21
34

12586269025
354224848179261915075

型指定をしない場合、powershellは暗黙的にエラーにならない数値型に変換するようです。
特に工夫しなければ最終的にひたすら[double]::IsInfinityをパイプラインに流し続けるので気を付けましょう。

# パイプラインに出力される値が途中から暗黙敵に
# オーバーフローエラーにならない[double]に変わっている
[Tuple]::Create(0, 1)
| unfold { $_[0], [Tuple]::Create($_[1], $_[0] + $_[1]) }
| Select-Object  -Index (0..5+100..105+1475..1480)
| ForEach-Object {"{0}:`t{1}" -f $_.gettype(), $_}

# パイプラインに流す値を[decimal]に指定
[Tuple]::Create(0, 1)
| unfold { [decimal]$_[0], [Tuple]::Create($_[1], $_[0] + $_[1]) }
| Select-Object  -Index (0..5+100..105+2000..2005)
| ForEach-Object {"{0}:`t{1}" -f $_.gettype(), $_}

# [double]::IsInfinityでシーケンスを終了させる
[Tuple]::Create(0, 1)
| unfold {
    if (-not [double]::IsInfinity($_[0] + $_[1])) {
        $_[0], [Tuple]::Create($_[1], $_[0] + $_[1])        
    }
} | ForEach-Object {
    $script:i=0
} {
    if($_){
        $script:i++
        "{0}:`t{1}:`t{2}" -f $script:i,${_}.gettype(), $_
    } 
}

結果

System.Int32:   0
System.Int32:   1
System.Int32:   1
System.Int32:   2
System.Int32:   3
System.Int32:   5
System.Double:  3.54224848179262E+20
System.Double:  5.73147844013817E+20
System.Double:  9.27372692193079E+20
System.Double:  1.5005205362069E+21
System.Double:  2.42789322839998E+21
System.Double:  3.92841376460687E+21
System.Double:  8.07763763215622E+307
System.Double:  1.3069892237634E+308
System.Double:  ∞
System.Double:  ∞
System.Double:  ∞
System.Double:  ∞


System.Decimal: 0
System.Decimal: 1
System.Decimal: 1
System.Decimal: 2
System.Decimal: 3
System.Decimal: 5
System.Decimal: 354224848179262000000
System.Decimal: 573147844013817000000
System.Decimal: 927372692193079000000
System.Decimal: 1500520536206900000000
System.Decimal: 2427893228399980000000
System.Decimal: 3928413764606870000000
InvalidArgument: 
Line |
   3 |  | unfold { [decimal]$_[0], [Tuple]::Create($_[1], $_[0] + $_[1]) }
     |             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Cannot convert value "8.105590009602353E+28" to type "System.Decimal". Error: "Value was either too large or too small for a Decimal."

1:      System.Int32:   1
2:      System.Int32:   1
3:      System.Int32:   2
4:      System.Int32:   3
5:      System.Int32:   5
6:      System.Int32:   8
~~中略~~
1469:   System.Double:  4.50151316958984E+306
1470:   System.Double:  7.28360130920163E+306
1471:   System.Double:  1.17851144787915E+307
1472:   System.Double:  1.90687157879931E+307
1473:   System.Double:  3.08538302667846E+307
1474:   System.Double:  4.99225460547777E+307

Select-Objectはインデックス系のオプションが指定された場合、出力するべき最後の要素をパイプラインに出力した時点でパイプライン処理を中断させる効果があります。
大変便利なのですが、中断されるとパイプライン上流のendブロックが行われなくなります。
注意が必要です。

[Tuple]::Create(0, 1)
| unfold { $_[0], [Tuple]::Create($_[1], $_[0] + $_[1]) }
| ForEach-Object { 'before begin' } { $_ } { 'before end' }
| Select-Object -First 10
| ForEach-Object { 'after begin' } { $_ } { 'after end' }

結果

after begin
before begin
0
1
1
2
3
5
8
13
21
after end

問題はリソース管理です。
この記事ではこの問題について扱っています。
関数内でのリソース解放処理 - PowerShell Scripting Weblog

なお、scriptblockcleanup{}というリソース解放のためのブロックを追加するというRFCがあり、これが採用されると根本的な解決となるようです。

余談:Powershellのタプル

今回は配列ではなく、Tupleを利用しました。
Powershellのタプルは以下のように使います。
各要素にインデックスアクセス可能です。
パイプライン入力等で展開されないので今回のようなケースでは重宝します。

# コンストラクタを使用する方法
$foo = 9
$tup = [Tuple[int, int]]::new($foo, ($foo + 1))
$tup[0]

結果

9

配列扱いなので複数の変数の割り当てが可能です。
実質デストラクタです。
Powershellにはタプルを分解する構文はないので関数を作っておくと便利でしょう。

using namespace System.Management.Automation
using namespace System.Runtime.CompilerServices

function Measure-TupleLength {
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [ITuple]
        $Tuple
    )
    $DUPLICATE_LENGTH = 1
    $MAX_TUPLE_LENGTH = 8
    if ($Tuple.Length -eq $MAX_TUPLE_LENGTH -and ($Tuple[-1] -is [ITuple])) {
        $AdditionalLength = (Measure-TupleLength $Tuple[-1]) - $DUPLICATE_LENGTH
    }
    $Tuple.Length + $AdditionalLength
}
function Split-Tuple {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Runtime.CompilerServices.ITuple]
        $Tuple,
        [int[]]
        $Length = $null
    )
    begin {
        $Script:counter = 0
    }
    process {
        if ($Script:counter -ne 0) {
            $PSCmdlet.ThrowTerminatingError(
                [ErrorRecord]::new(
                    ([ArgumentException]'Input is not Single.'),
                    'InvalidPipelineInput',
                    [ErrorCategory]::InvalidArgument,
                    $input)
            )
        }
        $Script:counter++
    }
    end {
        if (($null -ne $Length) -and ($ActualLength = Measure-TupleLength $Tuple) -notin $Length) {
            $message = "Tuple Length is $ActualLength. It should be $($Length -join ' or ')."
            $PSCmdlet.ThrowTerminatingError(
                [ErrorRecord]::new(
                    ([ArgumentException]$message),
                    'InvalidTupleLength',
                    [ErrorCategory]::InvalidArgument,
                    $Tuple)
            )
        }
        if ($Tuple.Length -gt 0) {
            foreach ($i in 0..($Tuple.Length - 1)) {
                if ($i -eq 7 -and ($Tuple[$i] -is [ITuple]) ) {
                    Split-Tuple $Tuple[$i]
                }
                else {
                    Write-Output $Tuple[$i] -NoEnumerate
                }
            }
        }
    }
}

現状、.NETでは長さが9以上のタプルを入れ子で表現していまが再帰関数で対応しました。
unfoldを使うことを考えましたが、将来この関数を使って(比較的)型安全なunfoldを書こうかなと思い、循環参照を回避するために見送りました。

普通に使うと想定される再帰呼び出し回数は2回ほどなので大丈夫でしょう……。
(参考:TupleExtensions.Deconstructでは21要素を持つタプルまで対応)

この関数を使うことで、変数へ分割代入時のバグを減らせます。

※以下のコード例では[ValueTuple]を使用してますが、[Tuple]でも動作します。

# 変数へ分割代入
$f, $g = [ValueTuple]::Create(4, 12) |  Split-Tuple
'$f, $g: {0}, {1}' -f $f, $g

# 要素数を制限可能
$f2, $g2 = [ValueTuple]::Create(6, 12, 32) |  Split-Tuple -Length (0..2)
'$f2, $g2: {0}, {1}' -f $f2, $g2

# [iTuple]派生型以外は受け付けない
{} | Split-Tuple

# 要素数9以上のタプルも分解可能
$values = [ValueTuple]::Create(
    1, 2, 'あ'..'お', 4, 5, [ValueTuple]::Create('a', 'b'), 7,
    ([ValueTuple]::Create(
            [ValueTuple]::Create((get-date)), 9, 10, 11, 12
        )
    )
) | Split-Tuple

(0..$values.GetUpperBound(0)).foreach{
    "value{0} : {1}" -f ($_ + 1), $values[$_]
}

# 要素数制限も可能
[ValueTuple]::Create(
    1, 2, 'あ'..'お', 4, 5, [ValueTuple]::Create('a', 'b'), 7,
    ([ValueTuple]::Create(
            [ValueTuple]::Create((get-date)), 9, 10, 11, 12
        )
    )
) | Split-Tuple -Length 15

結果

$f, $g: 4, 12

Split-Tuple: 
Line |
   6 |  … , $g2 = [ValueTuple]::Create(6, 12, 32) |  Split-Tuple -Length (0..2)
     |                                               ~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Tuple Length is 3. It should be 0 or 1 or 2.
$f2, $g2: , 

Split-Tuple: 
Line |
  10 |  {} | Split-Tuple
     |       ~~~~~~~~~~~
     | The input object cannot be bound to any parameters for the command either because the command does not take pipeline input or the input and its properties do not match any of the parameters that take pipeline input.

value1 : 1
value2 : 2
value3 : あ ぃ い ぅ う ぇ え ぉ お
value4 : 4
value5 : 5
value6 : (a, b)
value7 : 7
value8 : (2021/06/10 18:25:19)
value9 : 9
value10 : 10
value11 : 11
value12 : 12

Split-Tuple: 
Line |
  32 |  ) | Split-Tuple -Length 15
     |      ~~~~~~~~~~~~~~~~~~~~~~
     | Tuple Length is 12. It should be 15.

RESTメソッド

powershellunfoldを実装しようと思った理由です。
欲しい要素をパイプラインに流しつつ複数回RESTメソッドを行います。

function Get-YoutubeVideoIds {
    param (
        $PlayListId
    )
    @{
        key        = $Script:api
        part       = 'snippet'
        playlistId = $PlayListId
        fields     = 'nextPageToken,items(id,snippet(title,resourceId))'
        maxResults = '50'
    } | unfold {
        $res = Invoke-RestMethod https://www.googleapis.com/youtube/v3/playlistItems -b $_
        $nextState = if ($res.nextPageToken) {
            $_.pageToken = $res.nextPageToken
            $_
        }
        return $res.items, $nextState
    }
}

Get-YoutubeVideoIds PLfeA8kIs7Cocir1-TuSN3mOnj3qzyRShA
実行結果

id                                                                   snippet
--                                                                   -------
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS41NkI0NEY2RDEwNTU3Q0M2 @{title=Logging in a DevOps world by Friedrich Weinmann; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS4wMTcyMDhGQUE4NTIzM0Y5 @{title=Basic To Boss: Customizing Your PowerShell Prompt by Thomas Rayner; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS41MjE1MkI0OTQ2QzJGNzNG @{title=Securing PowerShell: Hands-On Lab by Ashley McGlone; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS4wOTA3OTZBNzVEMTUzOTMy @{title=Firewall Evasion and Remote Access with OpenSSH by Anthony Nocentino; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS4xMkVGQjNCMUM1N0RFNEUx @{title=Doctor, Don't Defenestrate: What to Do with Legacy Scripts by Michael Lombardi; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS41MzJCQjBCNDIyRkJDN0VD @{title=Going Core with VMware PowerCLI! by Luc Dekens; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS5DQUNERDQ2NkIzRUQxNTY1 @{title=Working with PSGraph by Kevin Marquette; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS45NDk1REZENzhEMzU5MDQz @{title=Machine Learning Algorithms with H2o and PowerShell by Tome Tanasovski; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS5GNjNDRDREMDQxOThCMDQ2 @{title=CypherDog2.0 - Bloodhound Dog Whispering with PowerShell by Walter Legowski; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS40NzZCMERDMjVEN0RFRThB @{title=Don't do that, do this instead: PowerShell worst practices and how to solve them by Chris Gardner; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS5EMEEwRUY5M0RDRTU3NDJC @{title=Dungeons & Dragons & Development: How Playing Games Makes You a Better IT Pro by Michael Lombardi; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS45ODRDNTg0QjA4NkFBNkQy @{title=PowerShell in Azure Functions by Dongbo Wang & Joey Aiello; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS4zMDg5MkQ5MEVDMEM1NTg2 @{title=The Windows Subsystem for Linux by Tara Raj; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS41Mzk2QTAxMTkzNDk4MDhF @{title=Advanced JEA Configurations by James Petty; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS5EQUE1NTFDRjcwMDg0NEMz @{title=Introduction to Serverless Functions by Kirk Munro; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS41QTY1Q0UxMTVCODczNThE @{title=Monitoring Out, Observability In by Ebru Cucen; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS4yMUQyQTQzMjRDNzMyQTMy @{title=Lord of the Configurations by Friedrich Weinmann; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS45RTgxNDRBMzUwRjQ0MDhC @{title=Using PowerShell Core to automate application packaging...with Habitat by Matt Wrock; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS5ENDU4Q0M4RDExNzM1Mjcy @{title=F5 Declarative Configuration by James Arruda; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS4yMDhBMkNBNjRDMjQxQTg1 @{title=Testing, Testing, 1...2...3: Using Pester for Infrastructure Validation by Brandon Olin; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS5GM0Q3M0MzMzY5NTJFNTdE @{title=PowerShell + AutoRest + Swagger = Instant Modules by Adam Murray; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS4zRjM0MkVCRTg0MkYyQTM0 @{title=Demystifying Terraform - "Hardcore" to Core Infrastructure-as-Code Tool by Chris Hunt; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS45NzUwQkI1M0UxNThBMkU0 @{title=Secure PowerShell web tools with System Frontier by Jay Adams; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS5DNzE1RjZEMUZCMjA0RDBB @{title=Life after "git push" by Steven Murawski; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS43MTI1NDIwOTMwQjIxMzNG @{title=Ansible for the Windows Admin by Jeremy Murrah; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS5DQ0MyQ0Y4Mzg0M0VGOEYw @{title=Publishing and Managing Modules in an Internal Repository by Kevin Marquette; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS4yQUE2Q0JEMTk4NTM3RTZC @{title=Completely Automate Managing Windows Software...Forever by Daniel Franciscus; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS5DMkU4NTY1QUFGQTYwMDE3 @{title=Beyond Pester 102: Acceptance testing with PowerShell by Glenn Sarti; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS44Mjc5REFBRUE2MTdFRDU0 @{title=Moving Up the Monitoring Stack by Steven Murawski; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS43NDhFRTgwOTRERTU4Rjg3 @{title=Sipping psake: Creating a Build and Release Pipeline for PowerShell by Brandon Olin; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS41QUZGQTY5OTE4QTREQUU4 @{title=PowerShell Universal Dashboard from start to finish by Adam Driscoll; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS4zRDBDOEZDOUM0MDY5NEEz @{title=Look smarter: deliver your work in Excel by James O'Neill; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS42MTI4Njc2QjM1RjU1MjlG @{title=Turn your logs into actionable data at any scale with AWS by Andrew Pearce; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS45RjNFMDhGQ0Q2RkFCQTc1 @{title=Writing Clustered Applications with Windows PowerShell and Apache Zookeeper by Tome Tanasovski; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS5BRjJDODk5REM0NjkzMUIy @{title=0-60 with PowerShell on AWS by Andrew Pearce & Steve Roberts; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS4zQzFBN0RGNzNFREFCMjBE @{title=Automating Active Directory Health Checks by Mike Kanakos; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS45NkVENTkxRDdCQUFBMDY4 @{title=Deep Web: A Web Cmdlets Deep Dive by Mark Kraus; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS5DNkMwRUI2MkI4QkI4NDFG @{title=Bullet-proofing Patterns & Practices by Joel "Jaykul" Bennett; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS5DRUQwODMxQzUyRTlGRkY3 @{title=Unexplained phenomena: powerful tricks you likely didn't know were even possible by Kirk Munro; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS41MzY4MzcwOUFFRUU3QzEx @{title=PSScriptAnalyzer (PSSA) VS-code integration & customization... by Christoph Bergmeister; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS4yQjZFRkExQjFGODk3RUFD @{title=PowerShell Remoting Internals by Paul Higinbotham; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS4yQUJFNUVCMzVDNjcxRTlF @{title=Parselmouth - bending the PowerShell language by Mathias Jessen; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS40QzRDOEU0QUYwNUIxN0M1 @{title=Writing Compiled PowerShell Cmdlets by Thomas Rayner; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS41RTNBREYwMkI5QzU3RkY2 @{title="Piping" data between packaged scripts by Paul DeArment Jr; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS5ENjI1QUI0MDI5NEQzODFE @{title=Jenkins - User Interface for your Powershell tasks by Kirill Kravtsov; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS44QzVGQUU2QjE2NDgxM0M4 @{title=Finding Performance Bottlenecks with PowerShell by Mike F. Robbins; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS4xMzgwMzBERjQ4NjEzNUE5 @{title=Chocolatey For the Organizations: Easily Manage Software by Rob Reynolds; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS4zMEQ1MEIyRTFGNzhDQzFB @{title=PowerShell Error and Event Collection at Scale by Dakota Clark; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS42Qzk5MkEzQjVFQjYwRDA4 @{title=Better Ops Together: Practical PowerShell Pair Programming Patterns and Practices... by Mark Kraus; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS41NTZEOThBNThFOUVGQkVB @{title=Containers - You Better Get on Board! by Anthony Nocentino; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS43NERCMDIzQzFBMERCMEE3 @{title=Using Visual Studio Code as Your Default PowerShell Editor by Tyler Leonhardt; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS5GNjAwN0Y0QTFGOTVDMEMy @{title=Unleash your PowerShell with AWS Lambda and Serverless Computing by Andrew Pearce; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS5CQkEwRDA0MDkwNUM2MDY1 @{title=Microsoft Azure Policy Guest Configuration by Michael Greene; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS4wNEU1MTI4NkZEMzVBN0JF @{title=Wardley Maps Saved The Day - How Stack Overflow Enterprise automated all the things... by Chris Hunt; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS4wMTYxQzVBRDI1NEVDQUZE @{title=Continuously deploying SQL code using Powershell by Kirill Kravtsov; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS4zMUEyMkQwOTk0NTg4MDgw @{title=It’s PowerShell In the Cloud – Welcome to Azure Cloud Shell by Michael Bender; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS42QzdBMzlBQzQzRjQ0QkQy @{title=Demystifying Microsoft's Cloud Automation products by Jaap Brasser; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS4wRjhFM0MxMTU1MEUzQ0VB @{title=Using PowerShell in a Cross Platform World presented by Bill Hurt written by James Pogran; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS5CNTZFOTNGQzZEODg1RUQx @{title=How to become a SHiPS wright - Building with SHiPS by Glenn Sarti; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS5CNTcxMDQ0NThBNzMxODYz @{title=PSCache: simple strategies for magnificent performance by Mathias Jessen; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS5ERkUyQTM0MzEwQjZCMTY5 @{title=Using Pester & ScriptAnalyzer for Detecting Obfuscated PowerShell by Daniel Bohannon; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS4xM0YyM0RDNDE4REQ1NDA0 @{title=Malicious Payloads vs Deep Visibility: A PowerShell Story by Daniel Bohannon; resourceId=}
UExmZUE4a0lzN0NvY2lyMS1UdVNOM21PbmozcXp5UlNoQS42MjYzMTMyQjA0QURCN0JF @{title=Publishing and Managing Modules in an Internal Repository by Kevin Marquette; resourceId=}

F#unfold

F#の場合は様々なコレクション型が定義されており、それぞれにunfoldを含む操作用の関数が用意されています。
例えば、seq<'T>用のunfoldの定義は以下の通りです。

Seq.unfold generator state

parameters :
  // 現在の状態を取り込み、リストの次の要素と次の状態の値のオプションタプルを返す関数
  generator : 'State -> ('T * 'State) option
  
  // 初期値
  state : 'State

// 生成されたリスト
Returns: seq<'T>

読み方

  • Tin -> Tout...「Tinを引数にとり、Toutを返す関数」を定義するF#の組み込み型を意味します。この関数型はPowershellにおける[scriptblock]に近い存在です。
  • (T1 * T2)…タプルの表記法です。
  • T optionF#固有のジェネリック方の表記法です。C#と同様にoption<T>という表記も可能です。通常、この表機能はF#の組み込み型でのみ使用されます。

比較するとこうなります。
なぜF#ではこんな記法を採用しているのか伝わってくる気がします。

F# Powershel C#
Tin -> Tout [fun[Tin,Tout]] func<Tin,Tout>
(T1 * T2) [Tuple[T1,T2]] Tuple<T1,T2>
T option [Option[T] option<T>
'State -> ('T * 'State) option [fun[TState,[option[Tuple[T, TState]]]]] <fun<TState,<option<Tuple<T, TState>>>>>

関数定義をPowershell風に書くとこんな感じです。

unfoldの定義(Powershell風)
NAME
    Seq.unfold

SYNTAX
    Seq.unfold[-generator <fun<TState,<option<Tuple<T, TState>>>>>] [-state <TState>]

OUTPUTS
    seq<T>

以下はgeneratorの一例です。

generator(Powershell版)
# 20になるまで1ずつ増えるシーケンスを生成する場合
# 'T … int
# 'State … int
{
    [OutputType([Tuple[int, int]])]
    param (
        [int]$state
    )
    if ($state -gt 20) {
        return $null
    }
    else {
        return [Tuple[int, int]]::new($state, $state + 1)
    }
}

余談:楽な書き方

可読性、安全性を投げ捨て、楽な書き方をするとこうなります。
自動変数$argsをうまく使うとparamブロックは不要です。
条件を満たさないと$null→特定条件でのみ値を出力と考えます。

generator(Powershell版_手抜き)
{if ($args[0] -le 20) { $args[0], ($args[0] + 1) }}

コンソール上で書く場合など、書き捨て用の場合は大体こっちの書き方です。
コードを残す場合は履歴をコピペしてvscode等で良い感じに整形してます。

image.png

参考:公式ドキュメント(F#)

※日本語ドキュメントの翻訳が微妙なのでDeepLの力を借りました。

Seq.unfoldは、stateを受け取り、それを変換してシーケンスの後続の各要素を生成するコンピュテーション関数からシーケンスを生成します。stateとは、各要素を計算するために使われる値であり、各要素が計算されるたびに変化します。Seq.unfoldの第2引数は、シーケンスの開始に使われる初期値です。Seq.unfoldでは、stateOption型を使用しており、None値を返すことでシーケンスを終了させることができます。次のコードでは、unfold操作で生成されるシーケンスの例として、seq1fibを示しています。1つ目のseq1は、20までの数字を並べた単純なシーケンスです。2つ目のfibは、unfoldを使ってフィボナッチ数列を計算しています。フィボナッチ数列の各要素は、前の2つのフィボナッチ数の合計であるため、state値は、数列の前の2つの数からなるタプルとなります。初期値は(1,1)で、数列の最初の2つの数値です。

let seq1 =
    0 // Initial state
    |> Seq.unfold (fun state ->
        if (state > 20) then
            None
        else
            Some(state, state + 1))

printfn "The sequence seq1 contains numbers from 0 to 20."

for x in seq1 do
    printf "%d " x

let fib =
    (1, 1) // Initial state
    |> Seq.unfold (fun state ->
        if (snd state > 1000) then
            None
        else
            Some(fst state + snd state, (snd state, fst state + snd state)))

printfn "\nThe sequence fib contains Fibonacci numbers."
for x in fib do printf "%d " x

出力は以下の通りです。

The sequence seq1 contains numbers from 0 to 20.

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

The sequence fib contains Fibonacci numbers.

2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597

実装のこだわり

利用時の書きやすさを実現するため、以下の目標がありました。

  1. Filterで実装
  2. stateには$_でアクセス可能にする

2番目が特に大事でした。文字数が6もある$arg[0]から解放されたかったんです。
これは辛い……

3..5 | unfold {
    $arg[0] -le 5 ? $arg[0], ($arg[0] + 1) : 'end.',$null
}

これなら気になりません。

3..5 | unfold {
    $_ -le 5 ? $_, ($_ + 1) : 'end.',$null
}

こういう場合に使えるのがScriptBlock.InvokeWithContextメソッドです。

https://docs.microsoft.com/en-us/dotnet/api/system.management.automation.scriptblock.invokewithcontext?view=powershellsdk-7.0.0#System_Management_Automation_ScriptBlock_InvokeWithContext_System_Collections_IDictionary_System_Collections_Generic_List_System_Management_Automation_PSVariable__System_Object___

InvokeWithContext(IDictionary, List<PSVariable>, Object[])
スクリプトブロックのスコープで定義されるローカル関数と変数のセットの形式で、追加のコンテキストでスクリプトブロックを呼び出すことができるメソッド。変数のリストには、$inputの変数、$_$thisが含まれます。‎

この関数のオーバーロードはハッシュテーブルを受け取り、必要な辞書に変換して、PowerShell スクリプト内から API を使いやすくします。

使用例です。
[scriptblock]を引数にとる関数で役立ちそうです。

$sb={
    $bar
    $bar=100
    $bar
    Test-Func
    $_
    $this
    $input
    $args
}
"& `$sb"
& $sb
"`r`n`$sb.InvokeWithContext"
$sb.InvokeWithContext(
    # ハッシュテーブルで追加の関数定義
    @{'Test-Func'= {10000}},
    # 自動変数$_, $this, $inputを含めて変数を追加で定義
    @(
        [psvariable]::new('bar', 'bar')
        [psvariable]::new('_', '_')
        [psvariable]::new('this', 'this')
        [psvariable]::new('input', 'input')
    ),
    # 関数の引数($args)を指定
    (20,40)
)

結果

result
& $sb
100
Test-Func: 
Line |
   5 |      Test-Func
     |      ~~~~~~~~~
     | The term 'Test-Func' is not recognized as a name of a cmdlet, function, script file, or executable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.

$sb.InvokeWithContext
bar
100
10000
_
this
input
20
40

unfoldでは$_の内容を制御するために使用しています。

終わりに

もし機能を足すとしたら生成する要素数の数、インデックスを指定するパラメータの実装だと思います。
ただし、下手に機能を足すぐらいならF#の組み込み型のラッパーモジュールを作るほうが早いかもしれません。

蛇足

Powershellで実現したいF#の機能は他にもあります。
例えばカリー化です。

ただのカリー化ならいくらでも記事がありますが、引数が3つ以上の場合の対応や使い勝手の良さをASTを用いて実現しようとか考えてました。
もし進展があったら記事にするかもしれません。

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1