はじめに
Goの標準パッケージが提供するstrconv
パッケージって便利ですよね。
ParseInt
やParseFloat
、ParseBool
を使えば、文字列で表された各型の値をパースし、それぞれの型の値に変換してくれます。
しかし、よく見ると複素数を表すcomplex64
型やcomplex128
型をパースする関数がありません。
10i
や1 + 10i
などをうまくパースする方法はないでしょうか?
そこでこの記事では、「簡単な式の評価機を作ってみる」で扱った式をパースする方法を基に、文字列から複素数型の値をパースする方法について考えてみます。
ParseComplexを作る
まずは、作成する関数のシグニチャを考えてみましょう。
strconv
パッケージにある他のParse
系の関数と合わせてみると以下のようになります。
func ParseComplex(s string, bitSize int) (complex128, error)
パース元の文字列s
とcomplex64
または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
を取得しています。
そして、生成したexpr
とbitSize
を基に、complexParser
という構造体を作り、parse
メソッドを呼んでいます。
go/ast
パッケージを使って、ASTのノードをトラバースしたり、変更するコードは再帰を使うことが多いです。
そのため、bitSize
などを関数の引数に渡して持ち回しても良いですが、ここは素直に構造体を作ると分かりやすいでしょう。
ここでは利便性を考えて、パッケージ外に公開するのはParseComplex
関数だけにしています。
しかし、パーサー自体の挙動をユーザが変えるようにしたい場合は、Parser
構造体などを作ってフィールドの値を変えることで、パーサーの挙動を変えられるようにすると良いでしょう。
その際にも具体的な実装は公開する必要はないでしょうし、私の場合はよく公開しない構造体(ここでいうとcomplexParser
)を作り、公開する構造体のメソッドから呼び出すようにしています。
また、フィールドを変更することで、メソッドのく挙動を変えられるようにしたとしても、デフォルトの挙動を行う関数(ここでいうとParseComplex
)のような関数は用意しておくとユーザによって便利だと思います。
パーサーを作る
さて、前置きが長くなりましたが、complexParser
を作っていきたいと思います。
この構造体の定義は以下のようになります。
type complexParser struct {
expr ast.Expr
bitSize int
}
expr
とbitSize
をフィールドに取ります。
正直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.Real
とconstant.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
など、リテラルにあたるのは、1
や2i
などです。
この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
パッケージを使ってライブラリや開発ルールを作ってみてください。