変態的PowerShell書法入門 時代遅れひとりFizzBuzz祭り Windows PowerShell続編

2013年の年頭にあたり、プログラマらしくFizzBuzzをば。

Windows PowerShellは強力な反面癖もある言語で*1、基礎文法に関しても強力だけど普段使うことが少ない機能がある。

最近PowerShellでツールを作っていて、そんな「強力で面白いけど色々あって使わない」という(よくあるPowerShellのコードからすると幾分と変態的な)機能にようやく気づいたので、記念にFizzBuzzしておこうと思う。

ノーマルなFizzBuzzを見たいのなら、以前のエントリ id:eel3:20100704:1278237090 をどうぞ。

なお本エントリは「変態的」という言葉を冠しているとはいえ所詮は「変態度:低」の入門編なので、誰かが「変態度:高」な内容を書いてくれることを期待している。

switchでループ:その1

他言語経験者が気づきにくい機能No.1じゃないかと思う。

switch文の値にコレクションを指定すると、foreach文よろしく自動的にループしつつコレクションの各値を評価してくれる。

Set-StrictMode -version Latest

switch (1..100) {
{$_ % 15 -eq 0} {'FizzBuzz'; continue}
{$_ %  3 -eq 0} {'Fizz';     continue}
{$_ %  5 -eq 0} {'Buzz';     continue}
default         {$_}
}

switchによる、よくあるFizzBuzz。continueを記述している点に注意。continueを書かないと、default以外の残りの節の条件を評価してしまう。条件にマッチする場合は不要なはずの処理を実行してしまうし、マッチしない場合も不要な評価を実行しているのでコレクションが大きければ塵も積もれば山となるで実行速度に影響する可能性がある。

ちなみにcontinueのかわりにbreakを使うとループを抜ける。この挙動は、switchで評価するのが単一の値(スカラー)の場合は他の言語のswitch文のように「breakで抜ける」風*2となり、コレクションの場合は「breakでループ中断、continueで次の周回にスキップ」とループ構文っぽくなる。うまいなあ。

switch文の値は「コレクションを生成するパイプライン」でも構わないので、こんな記述もできる*3

Set-StrictMode -version Latest

switch (seq.exe 1 100 | %{[int] $_}) {
{$_ % 15 -eq 0} {'FizzBuzz'; continue}
{$_ %  3 -eq 0} {'Fizz';     continue}
{$_ %  5 -eq 0} {'Buzz';     continue}
default         {$_}
}

switchでコレクションを舐める方法は、実際には使う機会が少ないように思う。foreach(フロー制御文の方)もそうだけど、コレクションを舐める前にコレクションが完成していなくてはならないので、コレクションが大きくなるほどメモリが必要になる。

それに何よりもパイプで繋いでいく記述と相容れない点が大きい。パイプに繋げられないswitch文はただのswitch文だ……あれ?

switchでループ:その2

switchでループするとしたら、コレクションよりもファイルを扱う場合の方が有効ではないかと思う。

Set-StrictMode -version Latest

switch -file fizzbuzz.txt {
{$_ % 15 -eq 0} {'FizzBuzz'; continue}
{$_ %  3 -eq 0} {'Fizz';     continue}
{$_ %  5 -eq 0} {'Buzz';     continue}
default         {$_}
}

seq(1)が出力するような改行区切りの数列のテキストファイルfizzbuzz.txtを使用するFizzBuzz。微妙にAWK的かもしれない。

この方法では一度に1行ずつファイルを読み、評価していく。Get-Contentのオプション-ReadCountの値が1(規定値)の状態でファイルを読みつつパイプでForeach-Objectに繋いで処理を書く場合と同じだけど、オプション-fileはPowerShellの言語の一部*4なので、インタプリタを通じて最適化を実行できる可能性があるらしい。

部分式で「何でも式」化

PowerShellのフロー制御文は式ではないので、値は返すけど「値を返す式」のように扱うことはできない。ただし、部分式や配列部分式と組み合わせることで、フロー制御文をあたかも「値を返す式」のように扱うことができる。

Set-StrictMode -version Latest

1..100 | %{
    -join $(switch ($_) {
            {$_ %  3 -eq 0} {'Fizz'}
            {$_ %  5 -eq 0} {'Buzz'}
            default         {$_}
            })
}

このFizzBuzzでは、部分式を使用してswitch文の実行結果を取得し、-join演算子に渡している。評価する値が15の場合、switch文は文字列 'Fizz' と 'Buzz' を返し、部分式によって要素数2の配列となり、-joinで結合されて文字列 'FizzBuzz' となる。

配列部分式で「配列の内包表記」的なこともできる。

Set-StrictMode -version Latest

switch (@(for ($i = 1; $i -le 100; $i++) {$i})) {
{$_ % 15 -eq 0} {'FizzBuzz'; continue}
{$_ %  3 -eq 0} {'Fizz';     continue}
{$_ %  5 -eq 0} {'Buzz';     continue}
default         {$_}
}

こんな風に範囲演算子を使わずに数列を生成することも可能。もっとも範囲演算子が使える場面では素直に使用したほうが高速なようだ*5。単純な数列には範囲演算子を使用し、それではうまく生成できないコレクションが欲しい場合は内包表記的な手法を使う、という住み分けになる。

部分式と配列部分式の違いは、中に記述した文が何も返さない時と値を1つ返す時の扱いだ。何も返さない場合、部分式は何も返さず(事実上$Nullを返す)、配列部分式は空の配列を返す。値を1つ返す場合、部分式は単一の値(スカラー)を返し、配列部分式は「要素数1の配列」を返す。

Set-StrictMode -version Latest

1..100 | %{
    $tmp = @(switch ($_) {
             {$_ %  3 -eq 0} {'Fizz'}
             {$_ %  5 -eq 0} {'Buzz'}
             })
    if ($tmp.Length -eq 0) {
        $_
    } else {
        -join $tmp
    }
}

配列部分式を使用しているので、switch文が返す値が0個でも1個でも2個以上でも$tmpには配列が格納される。安心してプロパティLengthを使用できる。

これが部分式の場合、switch文が返す値が0個や1個の時に$tmpに格納される値は配列ではない――0個の場合は$Nullで、1個の場合はスカラー(ここでは文字列)だ。なので0個の時はstrictモード次第で、制限がゆるい場合は「何も表示されない」という妙な挙動となるし*6、制限が厳しい場合は未定義の変数$tmpを参照して例外が発生する。1個の時は偶然にも文字列にプロパティLengthが存在するので見た目上は問題ないかのように動作する(実際には限りなくバグに近い)。

部分式は文字列のリテラルやヒアドキュメントにちょっとした式を埋め込む時に重宝するし、配列部分式はコマンドレットの実行結果を配列化して取得する時に結構使う。しかしフロー制御文を埋め込むような使い方はPowerShellではあまり見ない気がする。

もっとスクリプトブロック

PowerShellでは、スクリプトブロック自体は誰しも多用しているはずだ。主にForeach-ObjectやWhere-Objectなどの引数として。GUIイベントハンドラの記述にも多用しているだろう。

Set-StrictMode -version Latest

1..100 | %{
    $s = ''
    switch ($_) {
    {$_ % 3 -eq 0} {$s += 'Fizz'}
    {$_ % 5 -eq 0} {$s += 'Buzz'}
    default        {$s = $_}
    }
    $s
}

ただ、それ以外の状況でスクリプトブロックを積極的に使うことは……正直少ないと思う。

Foreach-Objectは、例えば以下のスクリプトブロックで代用できる。

Set-StrictMode -version Latest

1..100 | & {process{
    $s = ''
    switch ($_) {
    {$_ % 3 -eq 0} {$s += 'Fizz'}
    {$_ % 5 -eq 0} {$s += 'Buzz'}
    default        {$s = $_}
    }
    $s
}}

違いは1点だけ。Foreach-Objectでは、引数に指定したスクリプトブロックの中からForeach-Objectを実行しているスコープの変数を、スコープを明示しなくても変更することができる。しかし上のコードではそれは不可能だ。

よりForeach-Objectに近づけたい場合は、&呼び出し演算子ではなくドットソース演算子スクリプトブロックを実行すればよい。

Set-StrictMode -version Latest

1..100 | . {process{
    $s = ''
    switch ($_) {
    {$_ % 3 -eq 0} {$s += 'Fizz'}
    {$_ % 5 -eq 0} {$s += 'Buzz'}
    default        {$s = $_}
    }
    $s
}}

こうすると子スコープを生成せずに現在のスコープにてスクリプトブロックを実行するので、現在のスコープ中の変数をスコープを明示せずに変更できる。

実際、Foreach-Objectによるコードと上のドットソース演算子を使う例は結構似ていて、どちらもループ終了後に変数$sを参照してFizzBuzzの最後の値を取得することができる。

スクリプトブロックは変数に格納することができる。

Set-StrictMode -version Latest

$block = {process{
    $s = ''
    switch ($_) {
    {$_ % 3 -eq 0} {$s += 'Fizz'}
    {$_ % 5 -eq 0} {$s += 'Buzz'}
    default        {$s = $_}
    }
    $s
}}

1..100 | & $block

変数に格納して、後で実行することも可能だ。

変数に格納できるということは、引数経由で関数に渡すこともできる。

Set-StrictMode -version Latest

function Repeat-Times (
    [parameter(Mandatory=$true)]
    [ValidateScript({$_ -gt 0})]
    [int] $time,
    [parameter(Mandatory=$true)]
    [scriptblock] $block)
{
    1..$time | . {process{& $block}}
}

Repeat-Times 100 {
    $s = ''
    switch ($_) {
    {$_ % 3 -eq 0} {$s += 'Fizz'}
    {$_ % 5 -eq 0} {$s += 'Buzz'}
    default        {$s = $_}
    }
    $s
}

こうなってくると他の言語の無名関数の類のようにスクリプトブロックを多用する雰囲気になりそうなものだけど、現実は違う。

なぜか? PowerShellはレキシカルスコープではなくダイナミックスコープの言語なので、あるスクリプトブロックが実行時に参照する親スコープの変数が、スクリプトの記述者の意図したスコープの変数だとは限らないのだ。

実際、上の関数Repeat-Timesを使うFizzBuzzにて、スクリプトブロック中で変数$_を参照しているのだけど、この変数$_はどのスコープ由来の変数なのだろうか? おそらく関数Repeat-Timesの中で$blockを実行しているスクリプトブロックのprocess節のものだ。

またRepeat-Timesの引数として記述したスクリプトブロックの中から、Repeat-Timesの仮引数である$timeや$blockを参照できてしまう。これはつまり、仮に親スコープの変数$timeないし$blockを参照するスクリプトブロックを記述してRepeat-Timesの引数として渡した場合、実行時に実際に参照するのは意図したスコープのものではなく関数Repeat-Times内のスコープのもの(つまり仮引数の値)になってしまう、ということだ。

親スコープの変数を隠してしまう問題を避ける為に、例えば:

Set-StrictMode -version Latest

function Repeat-Times
{
    if ($Args.Length -ne 2) {
        throw 'need 2 arguments'
    }
    if ($Args[0] -isnot [int]) {
        throw '$Args[0] must be [int]'
    }
    if ($Args[0] -lt 1) {
        throw '$Args[0] must be greater than or equal to 1'
    }
    if ($Args[1] -isnot [scriptblock]) {
        throw '$Args[1] must be [scriptblock]'
    }
    1..$Args[0] | % $Args[1]
}

Repeat-Times 100 {
    $s = ''
    switch ($_) {
    {$_ % 3 -eq 0} {$s += 'Fizz'}
    {$_ % 5 -eq 0} {$s += 'Buzz'}
    default        {$s = $_}
    }
    $s
}

こんな風な逃げ方をして関数内で仮引数や変数を使わないようにすれば、Repeat-Timesの引数に直接スクリプトブロックを記述するような使い方をしている限りは色々と誤魔化すことができる。できるけど手間だし、常にこの方法が適用できるとは限らない。

実はこういう時こそプライベート変数の出番で、仮引数や変数をプライベート変数にしてしまい、スクリプトブロックを子スコープで実行するようにすれば、誤って親スコープの変数を隠してしまう問題を回避できる*7

Set-StrictMode -version Latest

function Repeat-Times (
    [parameter(Mandatory=$true)]
    [ValidateScript({$_ -gt 0})]
    [int] $private:time,
    [parameter(Mandatory=$true)]
    [scriptblock] $private:block)
{
    1..$time | . {process{& $block}}
}

Repeat-Times 100 {
    $s = ''
    switch ($_) {
    {$_ % 3 -eq 0} {$s += 'Fizz'}
    {$_ % 5 -eq 0} {$s += 'Buzz'}
    default        {$s = $_}
    }
    $s
}

回避はできるものの、毎度忘れずにプライベート変数を使うことができるのか、という疑問は残るし、$Argsのようなシステムが自動的に用意する変数には適用できない。

まだ別の問題も残っている。スクリプトブロック内で親スコープの変数を書き換えたい場合、当該変数のスコープを明示しなくてはならない。

書き換えたい変数がグローバルスコープやスクリプトスコープに存在する場合はスコープ修飾子を使えばよい。問題はそれ以外の親スコープの変数だ。Set-Variableのオプション-scopeで番号付きスコープを使用して変更したい変数が存在する親スコープへの相対位置を指定するのだけど、スクリプトブロックの記述位置と実行位置とではカレントのスコープの位置が異なる。つまり親スコープへの相対位置が変化してしまうのだ。

この点については何か回避策がないか探しているものの、現時点では見つかっていない。

ダイナミックスコープ

前項にも書いたけど、PowerShellはダイナミックスコープの言語だ。ダイナミックスコープを全面的に採用している言語といえば古典LISPEmacs Lisp、LOGOあたりだろうか。Common Lisp*8Perl*9あたりは部分的にサポートしている。意外なところでJavaScript処理系のRhinoも1.5R1でダイナミックスコープをサポートしたらしい。昨今はレキシカルスコープの言語が多い印象があるので、PowerShellはある意味尖がった仕様の言語なのかもしれない。

しかし何でダイナミックスコープなのか? 『Windows PowerShellインアクション』7.2.5によると、元ネタはUNIXのシェルらしい。まあ確かにシェルスクリプトでは親プロセスの環境変数は子プロセスに継承されるし、親プロセスで環境変数を書き換えてexportすると、それ以降に生成される子プロセスには書き換えられた環境変数が継承される。似ているといえば似ている。しかしPowerShellでは関数やスクリプトブロックの単位でもスコープが生成されるので、もはや別物だ。

ダイナミックスコープなPowerShellでは、ある関数・スクリプトブロックにて参照している親スコープの変数の中身が文脈によって変化する。

Set-StrictMode -version Latest

$i = 101..200
& {
    param([scriptblock] $block)
    for ($i = 1; $i -le 100; $i++) {
        & $block
    }
} {
    switch ($i) {
    {$_ % 15 -eq 0} {'FizzBuzz'; continue}
    {$_ %  3 -eq 0} {'Fizz';     continue}
    {$_ %  5 -eq 0} {'Buzz';     continue}
    default         {$_}
    }
}

この例では、switch文で参照している$iは3行目の範囲演算子で生成した整数の配列ではなく、実行しているスクリプトブロックのfor文で使用している$iだ。

ちょっと分かりにくいかもしれないので、書き直すとこんな感じ。

Set-StrictMode -version Latest

function Repeat-Times (
    [parameter(Mandatory=$true)]
    [ValidateScript({$_ -ge 0})]
    [int] $private:time,
    [parameter(Mandatory=$true)]
    [ValidateScript({$_ -is [scriptblock] -or $_ -is [string]})]
    $private:block)
{
    for ($i = 1; $i -le $time; $i++) {
        & $block
    }
}

$i = 101..200
function Get-FizzBuzzValue
{
    switch ($i) {
    {$_ % 15 -eq 0} {'FizzBuzz'; continue}
    {$_ %  3 -eq 0} {'Fizz';     continue}
    {$_ %  5 -eq 0} {'Buzz';     continue}
    default         {$_}
    }
}

Repeat-Times 100 Get-FizzBuzzValue

レキシカルスコープ的な発想だと、関数Get-FizzBuzzValueのswitch文で参照している$iの中身は直前の「$i = 101..200」の$iのように見える。しかしRepeat-Timesの引数に渡して実行させたとき、$iの中身はRepeat-Times内のforループの$iとなる。

ダイナミックスコープな特徴を生かすと何ができるか? 例えば、ここまでの例でもみられるように、スクリプトブロックを引数にとる関数を記述する時に、その関数からスクリプトブロックにパラメータを渡す時に親スコープの変数への暗黙の参照を使用する、という方法が考えられる。

Rubyのブロックを参考にした発想では、スクリプトブロックを引数にとる関数からスクリプトブロック本体にパラメータを渡すのに引数を使用する。

Set-StrictMode -version Latest

function Repeat-Times (
    [parameter(Mandatory=$true)]
    [ValidateScript({$_ -gt 0})]
    [int] $private:time,
    [parameter(Mandatory=$true)]
    [ValidateScript({$_ -is [scriptblock] -or $_ -is [string]})]
    $private:block)
{
    for ($private:i = 1; $i -le $time; $i++) {
        & $block $i
    }
}

Repeat-Times 100 {
    param([parameter(Mandatory=$true)][int] $n)
    switch ($n) {
    {$_ % 15 -eq 0} {'FizzBuzz'; break}
    {$_ %  3 -eq 0} {'Fizz';     break}
    {$_ %  5 -eq 0} {'Buzz';     break}
    default         {$_}
    }
}

これを、引数を使うのではなく、Repeat-Times内の変数を参照するようにする。

Set-StrictMode -version Latest

function Repeat-Times (
    [parameter(Mandatory=$true)]
    [ValidateScript({$_ -gt 0})]
    [int] $private:time,
    [parameter(Mandatory=$true)]
    [ValidateScript({$_ -is [scriptblock] -or $_ -is [string]})]
    $private:block)
{
    for ($i = 1; $i -le $time; $i++) {
        & $block
    }
}

Repeat-Times 100 {
    switch ($i) {
    {$_ % 15 -eq 0} {'FizzBuzz'; break}
    {$_ %  3 -eq 0} {'Fizz';     break}
    {$_ %  5 -eq 0} {'Buzz';     break}
    default         {$_}
    }
}

もっともこの方法はソースコードの見た目として非常に紛らわしいし*10スクリプトブロックを引数にとる関数にて内部の変数名を変更した時にはスクリプトブロック側の変数名ももれなく変更しなくてはならない。個人的には、この手法は多用すべきではないし、使用する場合は公開する変数名を吟味するべきだと思う(例外はデフォルト変数として多用されている$_ぐらいか?)。

もうひとつ思いつくのは、グローバルスコープやスクリプトスコープにデフォルト値を格納した変数を定義しておき、その変数を参照する関数を記述する、というものだ。一時的に動作を変更したい場合、関数を呼び出す直前に設定値を格納した変数と同名のローカル変数に値を設定して実行する。

Set-StrictMode -version Latest

$fizzbuzz_repeat_times = 1000

function Get-FizzBuzzValue
{
    1..$fizzbuzz_repeat_times | %{
        switch ($_) {
        {$_ % 15 -eq 0} {'FizzBuzz'; break}
        {$_ %  3 -eq 0} {'Fizz';     break}
        {$_ %  5 -eq 0} {'Buzz';     break}
        default         {$_}
        }
    }
}

& {
    $fizzbuzz_repeat_times = 100
    Get-FizzBuzzValue
}

スクリプトブロック中で一時的に$fizzbuzz_repeat_timesを変更している。この変更はGet-FizzBuzzValueの動作に影響を与える。ただしスクリプトブロックを抜けると$fizzbuzz_repeat_timesの値は元に戻るので、Get-FizzBuzzValueの動作も元に戻る。試しにスクリプトブロックの前後で値を表示させてみるとよいだろう。

この手法の興味深いところは、「変更するパラメータと実行する処理」を1つのスクリプトブロックにまとめて保持しておくことが可能な点だ。またドットソース演算子の存在を考えれば、一時的に変更したいパラメータのセットをスクリプトブロックにまとめておく使い方もありだろう。

特に後者に関しては、Emacs Lispはよく分からないのだけど、なんとなく.emacsで知らない間に使っている手法のような気がする。例えば各言語用のフック(c-mode-common-hookとか)に登録する無名関数で「(setq tab-width 4)」とか「(setq indent-tabs-mode nil)」とか書くやつ。

こういう「ダイナミックスコープ!」なPowerShellのコードってあまり見かけない気がする。私の視野が狭いだけだろうか。

まあおそらくレキシカルスコープの言語に慣れた人が相対的に多いので、PowerShellユーザにおける割合も同じ感じになっている(つまり、見た目上はレキシカルスコープの作法に慣れ親しんだPowerShell使いが多い)だけなのだろう。なら、例えばEmacs Lispバリバリの人がPowerShellを操ったらすごいことになるのかも。

まとめ

  • switch文によるループは便利といえば便利だけど、そのままでパイプと連結できないので使う機会が少ない。
    • 他言語経験者の場合、そもそもswitch文でループできること自体が盲点なのかも。
    • テキストファイルのループは意外と重宝するかも?
  • 部分式を使ってフロー制御文を式のように扱ったり、配列部分式で内包表記っぽい書き方をすることも少ないかも。
    • これは単に私自身の習慣の問題なだけ?
  • 既存のコマンドレット(Foreach-Objectなど)の引数やGUIイベントハンドラ以外でスクリプトブロックを使う機会も意外と少ない気がする。
    • もしPowerShellがレキシカルスコープな言語だったなら、スクリプトブロックが大流行したかも。
  • ダイナミックスコープを大々的にフューチャーしたコードもあまり見かけないような。

関係ないけど、PowerShell 2.0や3.0対応の言語仕様解説本(『PowerShellインアクション』みたいなやつ)が出てくれないだろうか? どうもPowerShell 1.0の頃の本が圧倒的に多いのだよなあ。

*1:私が好きなプログラミング言語って癖のある実用言語ばかりだ。AWKBourne ShellC言語GNU Make、JavaScriptPowerShell、Tcl……。

*2:ただしFALLTHROUGHの挙動は異なる。無条件に落下するのではなく、次の条件を評価する。

*3:わざわざForeach-Objectでキャストしているけど、seq.exeの部分だけでも問題ない。

*4:つまりコマンドレットではない。

*5:範囲演算子は言語機能の一部なので、実行時に最適化されるのだろう。

*6:$tmpの中身が$Nullとなり、$tmp.Lengthは$Nullを返す。$Nullは0ではないので「-join $tmp」が実行され、$tmpの中身が$Nullなので何も返さない。

*7:というのもプライベート変数は作成したスコープ内でしか参照や変更ができないから。でも当然ながら、プライベート変数を作成したスコープでForeach-Objectやドットソース演算子スクリプトブロックを実行したら意味がない。

*8:基本はレキシカルスコープだけど、defvarで定義したスペシャル変数はダイナミックスコープになるらしい。

*9:local変数。

*10:ダイクストラによる構造化プログラミングの基本的発想が「プログラムの正当性の検証の為にソースコードを読む必要があるから、ソースの見た目(静的構造)と実行時の制御の流れ(動的構造)を極力一致させよ」であることに留意すること。