LoginSignup
9
8

More than 3 years have passed since last update.

powershellでパーサコンビネータ

Last updated at Posted at 2015-07-16

JavaScriptでパーサコンビネータのコンセプトを理解するが、とても良記事だったので、powershellで写経した。

これを理解すると

  • クロージャがある言語ならパーサが簡単に書ける
  • その言語の知識内で実装できるので、パーサのための新たな構文を覚える必要がない
  • パーサを拡張しやすい

っとパーサの作成がとても身近に感じられるようになる。

なお、なぜpowershellかというと、固定長文字列を分解する良い方法(Perl,Ruby,Pythonの unpack のようなこと)がないかなあと思ったときにちょうどこの記事のことを思い出したからである。
例えば、それ用のパーサを定義すれば

  $x = $target[0..(0+4-1)]
  $y = $target[4..(4+3-1)]

なんて書かずに

  & seq (setvar x (tokenN 4)) `
        (setvar y (tokenN 3)) `
        $target 0

などと書けるのである。なんかLispっぽい。setvar や tokenN の実装は示さないがここでは、イメージをつかめてもらえれば良い。ただし、実際に数十桁、数百行ある固定長文字列をパースさせたところ非常に遅かった。これは追求はしていないが、パーサコンビネータではなくpowershell v2もしくはpowershellそのものの問題だと思う。

速度面以外でも、powershell でこの手のことをやってあーこの言語には向いてないなっと思った。
でも、

  $target |
    setvar x (tokenN 4) |
    setvar y (tokenN 3)

こんな風に書けるようかっこ良くインタフェースを設計するのも面白そう。

そんな夢を見つつ、以下、元記事のソースの写経である。長いが内容に関しては元記事がすばらしくわかりやすいのでそちらを参照してほしい。

function token {
  Param($str)

  $len = $str.Length

  return {
    Param($target, $position)

    if ($target.Length -ge ($position + $len) -and $target.Substring($position, $len) -eq $str) {
      return @($true, $str, ($position + $len))
    } else {
      return @($false, "", $position)
    }
  }.GetNewClosure()
}

# & (token 'foobar') 'foobar' 0 # => (True, 'foobar', 6)
# & (token 'foobar') 'foobar' 1 # => (False, '', 1)

function many {
  Param($parser)

  return {
    Param($target, $position)
    $result = [Collections.ArrayList]@()

    for (;;) { 
      $parsed = & $parser $target $position

      if ($parsed[0]) {
        $result.Add($parsed[1]) | Out-Null
        $position = $parsed[2]
      } else {
        break
      }
    }

    return ($True, (-join $result), $position)
  }.GetNewClosure()
}

# & (many (token 'hoge')) 'hogehoge' 0 # => (True, ('hoge', 'hoge'), 8)
# & (many (token 'hoge')) '' 0   # => (True, '', 0)

function choice {
  $parsers = $args

  return {
    Param($target, $position)

    for ($i = 0; $i -lt $parsers.Length; $i++) {
      $parsed = & $parsers[$i] $target $position
      if ($parsed[0]) {
        return $parsed
      }
    }

    return ($false, '', $position)
  }.GetNewClosure()
}


# $parse = many (choice (token 'hoge') (token 'fuga'))
#
# & $parse '' 0           # => (True, '', 0)
# & $parse 'hogehoge' 0   # => (True, ('hoge', 'hoge'), 8)
# & $parse 'fugahoge' 0   # => (True, ('fuga', 'hoge'), 8)
# & $parse 'fugafoo' 0    # => (True, 'fuga', 4)

function seq {
  $parsers = $args

  return {
    Param($target, $position)

    $result = [Collections.ArrayList]@()
    for ($i = 0; $i -lt $parsers.Length; $i++) {
      $parsed = & $parsers[$i] $target $position

      if ($parsed[0]) {
        $result.Add($parsed[1]) | Out-Null
        $position = $parsed[2]
      } else {
        return @($false, '', $position)
      }
    }
    return @($true, (-join $result), $position)
  }.GetNewClosure()
}

# $parse = seq (token 'foo') (choice (token 'bar') (token 'baz'))
#
# & $parse 'foobar' 0  # => (True, ('foo', 'bar'), 6)
# & $parse 'foobaz' 0  # => (True, ('foo', 'baz'), 6)
# & $parse 'foo' 0     # => (False, '', 3)

function option {
  Param($parser)

  return {
    Param($target, $position)

    $result = & $parser $target $position

    if ($result[0]) {
      return $result;
    } else {
      return ($true, '', $position)
    }
  }.GetNewClosure()
}

# $parser = option (token 'hoge')
#
# & $parser 'hoge' 0 # => (True, 'hoge', 4)
# & $parser 'fuga' 0 # => (True, '', 0)

function regex {
  Param($regexp)

  if ($regexp -is [String]) {
    $regexp = New-Object Regex ($regexp)
  }

  if (! $regexp.ToString().StartsWith("\G")) {
    $regexp = New-Object Regex (("\G" + $regexp.ToString()), $regexp.Options)
  }

  return {
    Param($target, $position)

    $regexResult = $regexp.Match($target, $position)

    if ($regexResult.Success) {
      $position += $regexResult.Length;
      return ($true, $regexResult.Value, $position)
    } else {
      return ($false, "", $position)
    }
  }.GetNewClosure()
}

# $parser = regex "hoge"
#
# & $parser 'hoge' 0 # => [true, 'hoge', 4]を返す

# $parser = regex ([regex]"([1-9][0-9]*)")
#
# & $parser '2014' 0 # => (True, '2014', 4)
# & $parser '01' 0   # => (False, '', 0)

なお、元記事の lazy の実装で止まっているが、これはpowershellのクロージャがなんかおかしいのかバグっているのか実装できなかったからである。
これは、以下のクロージャのサンプルが"BAR"ではなく"FOO"を表示してしまうことに起因している。

& {
  $foo = "FOO"

  $bar = {
    Write-Warning $foo
  }.GetNewClosure()

  $foo = "BAR"

  & $bar
}

powershellのGetNewClosure()は今ひとつ理解できない挙動をする。(powershell v4, v5 で確認)

追記: powershellのクロージャを理解するにpowershellのクロージャが普通と異なる点について記載した

9
8
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
9
8