Goのスクリプト言語としてPrologを使う
はじめに
Goのスクリプト言語としてPrologを使えるようにする ichiban/prolog
を作ったので実際にGoのプログラムに埋め込む例を示す
埋め込みの例(kagome)
KagomeはGoのみで書かれた形態素解析器で、かんたんに日本語文を形態素に分割するものだが、これをPrologにする例を通して ichiban/prolog
の使い方を説明する
この例での go.mod
は以下のとおり
module github.com/ichiban/kagomelog
go 1.15
require (
github.com/ichiban/prolog v0.1.0
github.com/ikawaha/kagome-dict/ipa v1.0.2
github.com/ikawaha/kagome/v2 v2.4.4
)
まずPrologのインタプリタを作成する必要があり、これには prolog.NewEngine()
を呼び出す。引数にはインタプリタにとっての標準入力、標準出力に相当する io.Reader
, io.Writer
をそれぞれ渡せるが、今回は入出力をしないので nil
を渡している
// prologエンジンを作成
e, err := prolog.NewEngine(nil, nil)
if err != nil {
panic(err)
}
このインタプリタは(現時点で)114の組み込み述語がすでに登録されているが、埋め込み先プログラム特有の述語を追加することができる。追加する述語のアリティ毎に登録用のメソッドがあり、ここではアリティ3の述語のため、e.Register3()
を使う。この述語はGoで書かれた特定のシグネチャを持つ関数だが、その書き方については後述する
// 形態素解析の述語 analyze/3 を登録
e.Register3("analyze", Analyze)
当然だが、Prologのコードで述語を定義することもできる。そのためには e.Exec()
を使う
// 名詞だけを取りだす述語 nouns/2 を定義
if err := e.Exec(`
nouns([token(_, Noun, ['名詞'|_])|Tokens], [Noun|Nouns]) :- nouns(Tokens, Nouns), !.
nouns([_|Tokens], Nouns) :- nouns(Tokens, Nouns).
nouns([], []).
`); err != nil {
panic(err)
}
こうして追加した述語をもとに問い合わせをすることができる。問い合わせは e.Query()
で行うが、その第2引数はコールバック関数で、その引数はクエリ中の自由変数のスライスである。もし解が存在するならそれらの自由変数を値で束縛してコールバック関数が呼び出される。解は複数存在する可能性があるので、コールバック関数も複数回呼び出される。これを止めるにはコールバック関数の戻り値として true
を返す。ここではひとつの解で十分なので単純に return true
としている
// 与えられた文を形態素解析して名詞だけ取り出す
if _, err := e.Query("analyze('すもももももももものうち', normal, Tokens), nouns(Tokens, Nouns).", func(vars []*prolog.Variable) bool {
for _, v := range vars {
fmt.Printf("%s\n", e.Describe(v))
}
return true
}); err != nil {
panic(err)
}
さて、Prologインタプリタに追加する述語の書き方だが、引数としてアリティ分の prolog.Term
と継続 k func() (bool, error)
を取り、 (bool, error)
を返す関数として定義する。ここでは input
, mode
, tokens
の3引数の述語を追加するので3つの prolog.Term
を取り、その後継続 k
を取る。継続 k
は「このあとに続く処理」を表す関数で、バックトラッキングによって複数回「このあとに続く処理」が実行されうるので何度でも呼べる関数の形で渡されている
// analyze/3の定義
func Analyze(input, mode, tokens prolog.Term, k func() (bool, error)) (bool, error) {
prolog.Term
はProlog上でのデータ構造を表すインタフェースで、実際には *prolog.Variable
, prolog.Float
, prolog.Integer
, prolog.Atom
, *prolog.Compound
, *prolog.Stream
のいずれかの型である。入力用の引数(この例では input
と mode
)が実際にどの型かを判定するには v, ok := prolog.Resolve(引数).(期待する型)
のイディオムが使える
// 最初の引数 input が(変数かもしれないので、それをたどっていった先で)アトム(文字列みたいなもの)であるか確認
i, ok := prolog.Resolve(input).(prolog.Atom)
if !ok {
return false, errors.New("not an atom")
}
出力用の引数には単一化によって値を書き出すことができる。prolog.Float
や prolog.Integer
, prolog.Atom
, *prolog.Compound
で出力したい prolog.Term
を組み立てて出力用の引数と prolog.Unify()
することで出力できる
// トークンを表す複合項の入ったスライスをリストに変換して出力用の引数と単一化
// 継続 k はそのまま渡した先で処理してもらう
return prolog.Unify(tokens, prolog.List(ret...), k)
全体としては以下のようになる
package main
import (
"errors"
"fmt"
"github.com/sirupsen/logrus"
"github.com/ichiban/prolog"
"github.com/ikawaha/kagome-dict/ipa"
"github.com/ikawaha/kagome/v2/tokenizer"
)
func main() {
logrus.SetLevel(logrus.WarnLevel) // ちょっとログがうるさいので黙らせる
// prologエンジンを作成
e, err := prolog.NewEngine(nil, nil)
if err != nil {
panic(err)
}
// 形態素解析の述語 analyze/3 を登録
e.Register3("analyze", Analyze)
// 名詞だけを取りだす述語 nouns/2 を定義
if err := e.Exec(`
nouns([token(_, Noun, ['名詞'|_])|Tokens], [Noun|Nouns]) :- nouns(Tokens, Nouns), !.
nouns([_|Tokens], Nouns) :- nouns(Tokens, Nouns).
nouns([], []).
`); err != nil {
panic(err)
}
// 与えられた文を形態素解析して名詞だけ取り出す
if _, err := e.Query("analyze('すもももももももものうち', normal, Tokens), nouns(Tokens, Nouns).", func(vars []*prolog.Variable) bool {
for _, v := range vars {
fmt.Printf("%s\n", e.Describe(v))
}
return true
}); err != nil {
panic(err)
}
}
// analyze/3の定義
func Analyze(input, mode, tokens prolog.Term, k func() (bool, error)) (bool, error) {
// 最初の引数 input が(変数かもしれないので、それをたどっていった先で)アトム(文字列みたいなもの)であるか確認
i, ok := prolog.Resolve(input).(prolog.Atom)
if !ok {
return false, errors.New("not an atom")
}
// 形態素解析のモードを与えられたアトムから特定
var tm tokenizer.TokenizeMode
m, ok := prolog.Resolve(mode).(prolog.Atom)
if !ok {
return false, errors.New("not an atom")
}
switch m {
case "normal":
tm = tokenizer.Normal
case "search":
tm = tokenizer.Search
case "extended":
tm = tokenizer.Extended
default:
return false, errors.New("unknown mode")
}
// 形態素解析器を準備
t, err := tokenizer.New(ipa.Dict(), tokenizer.OmitBosEos())
if err != nil {
return false, err
}
// 形態素解析の結果得られたトークンを token(ID, Surface, [Feature, ...]) の形式の複合項に変換
var ret []prolog.Term
for _, t := range t.Analyze(string(i), tm) {
features := t.Features()
fs := make([]prolog.Term, len(features))
for i, f := range features {
fs[i] = prolog.Atom(f)
}
ret = append(ret, &prolog.Compound{
Functor: "token",
Args: []prolog.Term{
prolog.Integer(t.ID),
prolog.Atom(t.Surface),
prolog.List(fs...),
},
})
}
// トークンを表す複合項の入ったスライスをリストに変換して出力用の引数と単一化
// 継続 k はそのまま渡した先で処理してもらう
return prolog.Unify(tokens, prolog.List(ret...), k)
}
出力結果は以下のようになる。「すもももももももものうち」が形態素解析され、そのうち名詞だけがフィルターされている
$ go run main.go
Tokens = [token(36163, 'すもも', ['名詞', '一般', *, *, *, *, 'すもも', 'スモモ', 'スモモ']), token(73244, 'も', ['助詞', '係助詞', *, *, *, *, 'も', 'モ', 'モ']), token(74988, 'もも', ['名詞', '一般', *, *, *, *, 'もも', 'モモ', 'モモ']), token(73244, 'も', ['助詞', '係助詞', *, *, *, *, 'も', 'モ', 'モ']), token(74988, 'もも', ['名詞', '一般', *, *, *, *, 'もも', 'モモ', 'モモ']), token(55829, 'の', ['助詞', '連体化', *, *, *, *, 'の', 'ノ', 'ノ']), token(8027, 'うち', ['名詞', '非自立', '副詞可能', *, *, *, 'うち', 'ウチ', 'ウチ'])]
Nouns = ['すもも', 'もも', 'もも', 'うち']
おわりに
Goのスクリプト言語としてPrologを使えるようにする ichiban/prolog
を紹介し、実際にKagomeを使ったGoのプログラムにPrologを埋め込む例を示した。
ichiban/prolog
はまだ荒削りでドキュメントやテストも十分ではないが、現状でもほぼISO PrologでREPLも書けるので試してみてほしい。