Posted at

【実践goパッケージ】文字列から複素数型の値をパースする #golang

More than 1 year has passed since last update.


はじめに

Goの標準パッケージが提供するstrconvパッケージって便利ですよね。

ParseIntParseFloatParseBoolを使えば、文字列で表された各型の値をパースし、それぞれの型の値に変換してくれます。

しかし、よく見ると複素数を表すcomplex64型やcomplex128型をパースする関数がありません。

10i1 + 10iなどをうまくパースする方法はないでしょうか?

そこでこの記事では、「簡単な式の評価機を作ってみる」で扱った式をパースする方法を基に、文字列から複素数型の値をパースする方法について考えてみます。


ParseComplexを作る

まずは、作成する関数のシグニチャを考えてみましょう。

strconvパッケージにある他のParse系の関数と合わせてみると以下のようになります。

func ParseComplex(s string, bitSize int) (complex128, error)

パース元の文字列scomplex64またはcomplex128に変換するか判断するために使うbitSizeを渡し、complex128の値とエラーを返します。

細かい実装は後で考えることにして、まずはParseComplex関数を書いてみましょう。

func ParseComplex(s string, bitSize int) (complex128, error) {

expr, err := parser.ParseExpr(s)
if err != nil {
return 0, err
}

p := &complexParser{
expr: expr,
bitSize: bitSize,
}

c, err := p.parse()

if err != nil {
return 0, err
}

return c, nil
}

関数の中は至ってシンプルです。

まず、parser.ParseExprを使って式をパースしてast.Exprを取得しています。

そして、生成したexprbitSizeを基に、complexParserという構造体を作り、parseメソッドを呼んでいます。

go/astパッケージを使って、ASTのノードをトラバースしたり、変更するコードは再帰を使うことが多いです。

そのため、bitSizeなどを関数の引数に渡して持ち回しても良いですが、ここは素直に構造体を作ると分かりやすいでしょう。

ここでは利便性を考えて、パッケージ外に公開するのはParseComplex関数だけにしています。

しかし、パーサー自体の挙動をユーザが変えるようにしたい場合は、Parser構造体などを作ってフィールドの値を変えることで、パーサーの挙動を変えられるようにすると良いでしょう。

その際にも具体的な実装は公開する必要はないでしょうし、私の場合はよく公開しない構造体(ここでいうとcomplexParser)を作り、公開する構造体のメソッドから呼び出すようにしています。

また、フィールドを変更することで、メソッドのく挙動を変えられるようにしたとしても、デフォルトの挙動を行う関数(ここでいうとParseComplex)のような関数は用意しておくとユーザによって便利だと思います。


パーサーを作る

さて、前置きが長くなりましたが、complexParserを作っていきたいと思います。

この構造体の定義は以下のようになります。

type complexParser struct {

expr ast.Expr
bitSize int
}

exprbitSizeをフィールドに取ります。

正直exprはフィールドにしなくても対して問題にはなりません。

さて、ParseComplex関数から呼んでいたparseメソッドから作っていきましょう。

私の場合、大きな単位をざっと作り、細かな実装は後回しにすることが多いです。

こうすることで、細かい部分を作り込みすぎて、途中で実装方針が変わった場合に変更コストがかかるのを防いでくれますし、頭の中のリソースをそんなに使わなくても実装していけるのでオススメです。

もちろん、方針がガッチリ決まった段階で、一気に細かい部分を深さ優先で作り込むこともありますが。

横道にそれましたが、parseメソッドは以下のようになりました。

まずexprフィールドのast.Expr型からconstant.Value型にパースするparseExprメソッドを呼び出し、constant.Value型を取得しています。

そして、constant.Value型の値をcomplex128に変換するcomplexValを呼び出し、複素数型へと変換しています。

func (p *complexParser) parse() (complex128, error) {

v, err := p.parseExpr(p.expr)
if err != nil {
return 0, err
}

if v.Kind() != constant.Complex {
return 0, errors.New("cannot parse")
}

return p.complexVal(v)
}

次にcomplexValメソッドをみておきましょう。

go/constantパッケージのconstant.Value型では、複素数型を表すことができますが、constant.Value型から直接complex128型やcomplex64型の値を取得する方法が提供されていません。

一方で、constant.Realconstant.Imagを使って、実数部と虚数部を別々に取り出してやります。

また、取り出した実数部や虚数部は、ビット数によってfloat32型かfloat64型に変換する必要があります。

なお、complex64型の場合、実数部と虚数部は共にfloat32型の値である必要があり、complex128型では、それぞれfloat64型の値である必要があります。

func (p *complexParser) complexVal(v constant.Value) (complex128, error) {

rv := constant.Real(v)
iv := constant.Imag(v)

// complex64
if p.bitSize == 64 {
r, ok := constant.Float32Val(rv)
if !ok {
return 0, errors.New("cannot parse real part")
}
i, ok := constant.Float32Val(iv)
if !ok {
return 0, errors.New("cannot parse imag part")
}

return complex128(complex(r, i)), nil
}

// complex128
r, ok := constant.Float64Val(rv)
if !ok {
return 0, errors.New("cannot parse real part")
}
i, ok := constant.Float64Val(iv)
if !ok {
return 0, errors.New("cannot parse imag part")
}

return complex(r, i), nil
}

続いて、parseExprメソッドをみていきましょう。

簡単な式の評価機を作ってみる」でも出てきた、ノードの種類によって型スイッチで処理を分岐する方法です。

deferでパニックをリカバーしているのは、go/constantパッケージの関数がパニックをする可能性があるからです。

func (p *complexParser) parseExpr(expr ast.Expr) (rv constant.Value, rerr error) {

defer func() {
if r := recover(); r != nil {
switch err := r.(type) {
case error:
rv, rerr = constant.MakeUnknown(), err
default:
rv, rerr = constant.MakeUnknown(), fmt.Errorf("%v", err)
}
}
}()

switch expr := expr.(type) {
case *ast.UnaryExpr:
return p.parseUnaryExpr(expr)
case *ast.BinaryExpr:
return p.parseBinaryExpr(expr)
case *ast.BasicLit:
return p.parseBasicLit(expr), nil
}

return constant.MakeUnknown(), errors.New("cannot parse")
}

さて、ここでは以下の3種類のノードについてパースしています。



  • *ast.UnaryExp(単項演算式)


  • *ast.BinaryExpr(2項演算式)


  • *ast.BasicLit(リテラル)

単項演算式にあたるのは、-1などで、2項演算式にあたるのは1+2iなど、リテラルにあたるのは、12iなどです。

この3種類をパースすれば複素数をパースすることができます。

まずは、parseUnaryExprメソッドからみてみましょう。

func (p *complexParser) parseUnaryExpr(expr *ast.UnaryExpr) (constant.Value, error) {

x, err := p.parseExpr(expr.X)
if err != nil {
return constant.MakeUnknown(), err
}

return constant.UnaryOp(expr.Op, x, 0), nil
}

parseExprメソッドを使って、constant.Valueに変換し、constant.UnaryOp関数によって単項演算を行っています。

続いて、parseBinaryExprメソッドをみてみましょう。

func (p *complexParser) parseBinaryExpr(expr *ast.BinaryExpr) (constant.Value, error) {

x, err := p.parseExpr(expr.X)
if err != nil {
return constant.MakeUnknown(), err
}

y, err := p.parseExpr(expr.Y)
if err != nil {
return constant.MakeUnknown(), err
}

return constant.BinaryOp(x, expr.Op, y), nil
}

parseUnaryExprとほとんど同じで、オペランドをparseExprメソッドによって、constant.Value型にパースし、constant.BinaryOp関数によって2項演算を行います。

最後に、parseBasicLitメソッドをみてみましょう。

func (p *complexParser) parseBasicLit(expr *ast.BasicLit) constant.Value {

return constant.MakeFromLiteral(expr.Value, expr.Kind, 0)
}

こちらは、単純にconstant.MakeFromLiteralを使って、リテラルからconstant.Value型に変換しているだけです。

これで文字列を複素数型に変換するプログラムを書くことができました。

最後に、すべてのコードを載せておきたいと思います。

package main

import (
"errors"
"fmt"
"go/ast"
"go/constant"
"go/parser"
)

type complexParser struct {
expr ast.Expr
bitSize int
}

func (p *complexParser) parse() (complex128, error) {
v, err := p.parseExpr(p.expr)
if err != nil {
return 0, err
}

if v.Kind() != constant.Complex {
return 0, errors.New("cannot parse")
}

return p.complexVal(v)
}

func (p *complexParser) complexVal(v constant.Value) (complex128, error) {
rv := constant.Real(v)
iv := constant.Imag(v)

// complex64
if p.bitSize == 64 {
r, ok := constant.Float32Val(rv)
if !ok {
return 0, errors.New("cannot parse real part")
}
i, ok := constant.Float32Val(iv)
if !ok {
return 0, errors.New("cannot parse imag part")
}

return complex128(complex(r, i)), nil
}

// complex128
r, ok := constant.Float64Val(rv)
if !ok {
return 0, errors.New("cannot parse real part")
}
i, ok := constant.Float64Val(iv)
if !ok {
return 0, errors.New("cannot parse imag part")
}

return complex(r, i), nil
}

func (p *complexParser) parseExpr(expr ast.Expr) (rv constant.Value, rerr error) {
defer func() {
if r := recover(); r != nil {
switch err := r.(type) {
case error:
rv, rerr = constant.MakeUnknown(), err
default:
rv, rerr = constant.MakeUnknown(), fmt.Errorf("%v", err)
}
}
}()

switch expr := expr.(type) {
case *ast.UnaryExpr:
return p.parseUnaryExpr(expr)
case *ast.BinaryExpr:
return p.parseBinaryExpr(expr)
case *ast.BasicLit:
return p.parseBasicLit(expr), nil
}

return constant.MakeUnknown(), errors.New("cannot parse")
}

func (p *complexParser) parseUnaryExpr(expr *ast.UnaryExpr) (constant.Value, error) {
x, err := p.parseExpr(expr.X)
if err != nil {
return constant.MakeUnknown(), err
}

return constant.UnaryOp(expr.Op, x, 0), nil
}

func (p *complexParser) parseBinaryExpr(expr *ast.BinaryExpr) (constant.Value, error) {
x, err := p.parseExpr(expr.X)
if err != nil {
return constant.MakeUnknown(), err
}

y, err := p.parseExpr(expr.Y)
if err != nil {
return constant.MakeUnknown(), err
}

return constant.BinaryOp(x, expr.Op, y), nil
}

func (p *complexParser) parseBasicLit(expr *ast.BasicLit) constant.Value {
return constant.MakeFromLiteral(expr.Value, expr.Kind, 0)
}

func ParseComplex(s string, bitSize int) (complex128, error) {
expr, err := parser.ParseExpr(s)
if err != nil {
return 0, err
}

p := &complexParser{
expr: expr,
bitSize: bitSize,
}

c, err := p.parse()

if err != nil {
return 0, err
}

return c, nil
}

func main() {
fmt.Println(ParseComplex("1 + 10i", 128))
}

上記のコードは、The Go Playgroundでも動かすことができます。


おわりに

この記事では、実践的なgoパッケージの使い方として、文字列から複素数型をパースするプログラムについて解説しました。

ぜひ、みなさんもgoパッケージを使ってライブラリや開発ルールを作ってみてください。