作業メモ:kagomeのシステム辞書を更新する

概要

辞書項目追加したので作業記録の自分用メモ.

作業

必要なもの

  • go-bindata コマンド
    • 自分のレポジトリにある改造したやつ

作業ディレクト

kagome/cmd/_dictool 以下で作業する

キーワードを追加する

  • IPADIC
    • mecab-ipadic-2.7.0-20070801 を用意する (github.com/taku910/mecab にあるものでもいい)
    • 差分がとれるように編集するファイルをコピーする
    • EUC-JP で項目を追加
    • diff -up <original> <updated> で差分をとって patch 以下に記録しておく
    • ファイルを更新
  • UniDIC
    • unidic-mecab-2.1.2_src を用意する
    • 差分がとれるように編集するファイルをコピーする
    • utf8 で項目を追加
    • diff -up <original> <updated> で差分をとって patch 以下に記録しておく
    • ファイルを更新

辞書ビルド

IPADIC

$ mkdir ./dic
$ mkdir ./dic/ipa
$ go run main.go  ipa -mecab ./mecab-ipadic-2.7.0-20070801 -output ./dic/ipa -z false
$ go run main.go ipa -mecab ./mecab-ipadic-2.7.0-20070801

UniDIC

$ mkdir ./dic/uni
$ go run main.go  uni -mecab ./unidic-mecab-2.1.2_src -output ./dic/uni -z=false
$ go run main.go uni -mecab ./unidic-mecab-2.1.2_src

ipa.dic と uni.dic を kagome/_sample にコピーして更新

go-bindata

go-bindata -o bindata.go -nomemcopy -separate -pkg=data dic/...

直下に gindata*.go というファイルが生成される.これを kagome/internal/dic/data 以下にコピーする

17:35 $ ls -lah ../../internal/dic/data/
total 432920
drwxr-xr-x@ 24 ikawaha  staff   768B  4 16 16:31 ./
drwxr-xr-x@ 23 ikawaha  staff   736B  2 16 13:09 ../
-rw-r--r--   1 ikawaha  staff   7.7K  4 16 16:31 bindata.go
-rw-r--r--   1 ikawaha  staff   5.9M  4 16 16:31 bindata00.go
-rw-r--r--   1 ikawaha  staff   7.7M  4 16 16:31 bindata01.go
-rw-r--r--   1 ikawaha  staff    20M  4 16 16:31 bindata02.go
-rw-r--r--   1 ikawaha  staff   9.4M  4 16 16:31 bindata03.go
-rw-r--r--   1 ikawaha  staff   7.5M  4 16 16:31 bindata04.go
-rw-r--r--   1 ikawaha  staff   3.0M  4 16 16:31 bindata05.go
-rw-r--r--   1 ikawaha  staff   3.9M  4 16 16:31 bindata06.go
-rw-r--r--   1 ikawaha  staff   4.3M  4 16 16:31 bindata07.go
-rw-r--r--   1 ikawaha  staff   3.6M  4 16 16:31 bindata08.go
-rw-r--r--   1 ikawaha  staff   3.5M  4 16 16:31 bindata09.go
-rw-r--r--   1 ikawaha  staff   3.7M  4 16 16:31 bindata10.go
-rw-r--r--   1 ikawaha  staff   8.6M  4 16 16:31 bindata11.go
-rw-r--r--   1 ikawaha  staff    21M  4 16 16:31 bindata12.go
-rw-r--r--   1 ikawaha  staff    18M  4 16 16:31 bindata13.go
-rw-r--r--   1 ikawaha  staff    18M  4 16 16:31 bindata14.go
-rw-r--r--   1 ikawaha  staff    16M  4 16 16:31 bindata15.go
-rw-r--r--   1 ikawaha  staff    10M  4 16 16:31 bindata16.go
-rw-r--r--   1 ikawaha  staff    12M  4 16 16:31 bindata17.go
-rw-r--r--   1 ikawaha  staff    16M  4 16 16:31 bindata18.go
-rw-r--r--   1 ikawaha  staff    14M  4 16 16:31 bindata19.go
-rw-r--r--   1 ikawaha  staff   4.2M  4 16 16:31 bindata20.go

念のため go fmt する

$ go fmt ../../internal/dic/data/

テスト

項目を追加/削除していれば項目の全体数が合わなくなるのでそのテストが失敗するはず.

$ go test ./...
?       github.com/ikawaha/kagome/cmd/kagome    [no test files]
?       github.com/ikawaha/kagome/cmd/kagome/lattice    [no test files]
?       github.com/ikawaha/kagome/cmd/kagome/server [no test files]
?       github.com/ikawaha/kagome/cmd/kagome/tokenize   [no test files]
ok      github.com/ikawaha/kagome/internal/da   (cached)
--- FAIL: TestSystemDic (1.58s)
    sysdic_test.go:30: got 392127, expected 392126
--- FAIL: TestSystemDicSimple (1.55s)
    sysdic_test.go:41: got 392127, expected 392126
--- FAIL: TestSystemDicIPAMorphs01 (1.42s)
    sysdic_test.go:65: got 392127, expected 392126
--- FAIL: TestSystemDicIPASimpleMorphs01 (1.32s)
    sysdic_test.go:75: got 392127, expected 392126
--- FAIL: TestSystemDicUniMorphs01 (8.42s)
    sysdic_test.go:85: got 756466, expected 756463
--- FAIL: TestSystemDicUniSimpleMorphs01 (7.43s)
    sysdic_test.go:95: got 756466, expected 756463
--- FAIL: TestSystemDicIPAContents01 (1.25s)
    sysdic_test.go:105: got 392127, expected 392126
--- FAIL: TestSystemDicUniContents01 (7.75s)
    sysdic_test.go:125: got 756466, expected 756463
FAIL
FAIL    github.com/ikawaha/kagome/internal/dic  113.520s
?       github.com/ikawaha/kagome/internal/dic/data [no test files]
ok      github.com/ikawaha/kagome/internal/lattice  17.673s
ok      github.com/ikawaha/kagome/splitter  (cached)
--- FAIL: TestAnalyze02 (0.00s)
    tokenizer_test.go:68: got 関西国際空港(0, 6)KNOWN[372978], expected 関西国際空港(0, 6)KNOWN[372977]
FAIL
FAIL    github.com/ikawaha/kagome/tokenizer 20.071s

テストを修正する.

  • 項目数が増えることによる修正
    • 全体のエントリ数
    • 単語IDのずれ

/以上

slackのメッセージをひたすら読み上げさせるコマンドを作った

概要

作業に没頭してると slack のメッセージを取りこぼしてしまうことが多いんですが,これを読み上げてくれば便利だなと雑に作ってみました.

ゲーム実況なんかで,来たメッセージを bot に読み上げさせて,プレイしながら回答してやつのイメージです.

github.com

必要環境

mac というか say コマンドが必要です.

インストール

go get github.com/ikawaha/slacksay/...

使い方

slack トークンを用意して,手元の計算機で下記を実行すれば ok です. トークンの取得はとりあえずレガシートークhttps://api.slack.com/custom-integrations/legacy-tokens を取得するか,過剰な権限が気になるなら https://api.slack.com/apps この辺で取得してください.

slacksay -t <slack_token> [-d (<json data>|@<file_name>|@-)]
  -d string
        json data. If you start the data with the letter @, the rest should be a file name to read the data from, or -  if you  want to read the data from stdin.
  -t string
        slack token

ex.

slacksay -t xoxp-your-token -d '{"bot_message": "true"}'

仕組み

slack に流れてきたメッセージを say コマンドに渡すだけです.

だだし,それだけだとカエルの合唱みたいになってしまうので,slack のメッセージを一端 queue にいれて,整列させてからコマンドに渡します. いろいろ試したんですが,喋らせるのは1つだけにしておくのが無難だというところに落ち着きました.

goroutine 使ったコード書くのは面白いですね!

あと,say コマンドが必ずしも正しく読みを当ててくれるわけではないので,ユーザー名とか読み替えを指定できるようにしてあります.

チャンネルやユーザー,キーワードでのフィルターも用意してみましたが使ってないのでこの機能が必要なのかよく分かっていません. ぼくの環境では,bot のメッセージ(slack では SubType が bot_messageとなるやつ)は除外しないとうるさくてしょうがなっかったので, これを無視するオプションを用意してあります.

say コマンドの代わりに他のコマンドを指定するオプションを用意してありますが,これも不要な気がしています. もしかしたら windows でも似たようなコマンドないかな・・・みたいな期待はあるのですが,実機がないので・・・.

未解決

say コマンドはオプション指定すると声色を変更できるのですが,exec.Command() で指定するとうまく実行できませんでした. なぜだろうか・・・.

Happy hacking!

goa でレスポンスを XML で返す

概要

goa のレスポンスはデフォルトでは json になっているが,これを XML で返したい.

ちなみに,入力の方は json, XML, gob (!!!) がデフォルトで受け取れるようになっている.

例で使ったコードはここにあるやつです :

https://github.com/ikawaha/cellar/tree/xml

方法

cellar の example をちょっと修正して動作を試してみます.

1. デザインで Produces を指定する

package design

import (
    . "github.com/goadesign/goa/design"
    . "github.com/goadesign/goa/design/apidsl"
)

var _ = API("cellar", func() {
    Title("The virtual wine cellar")
    Description("A simple goa service")
    Scheme("http")
    Host("localhost:8080")

    Produces("application/xml") // ★ これを追加
})

var _ = Resource("bottle", func() {
    BasePath("/bottles")
    DefaultMedia(BottleMedia)

    Action("show", func() {
        Description("Get bottle by id")
        Routing(GET("/:bottleID"))
        Params(func() {
            Param("bottleID", Integer, "Bottle ID")
        })
        Response(OK)
        Response(NotFound)
    })
})

var BottleMedia = MediaType("application/vnd.goa.example.bottle+xml", func() { // ★ レスポンスは XML なので適当に変更する
    Description("A bottle of wine")
    Attributes(func() {
        Attribute("id", Integer, "Unique bottle ID")
        Attribute("href", String, "API href for making requests on the bottle")
        Attribute("name", String, "Name of wine")
        Required("id", "href", "name")
    })
    View("default", func() {
        Attribute("id")
        Attribute("href")
        Attribute("name")
    })
})

2. goagen

goagen します.

3. 起動してアクセスしてみる

コントローラーは何も書いてないので,適当に返ってくるのを見るだけ(^^ゞ

$ curl -i localhost:8080/bottles/0
HTTP/1.1 200 OK
Content-Type: application/vnd.goa.example.bottle+xml
Date: Thu, 04 Apr 2019 09:08:28 GMT
Content-Length: 73

<GoaExampleBottle><href></href><id>0</id><name></name></GoaExampleBottle>

レスポンスボディは XML 形式になってます. Content-Type も MediaType で指定したものになっています.

Happy hacking!

2018年の買い物振り返り

はじめに

他の人のレビューですごくいい買い物できたので,自分の今年の買い物も振り返ってみました. もっといいのあるよ!とか教えてもらえればうれしいです.

よかった

体重計

他のブログで紹介されてたのをみて買いました.両親が毎日体重を量って手帳に記録してたのがすごく楽になっていい買い物でした.スマホのアプリで体脂肪とかもグラフ化されます.精度がどうなのかはよく分からないけど,目安だと思って細かいことを気にしないならいいと思う(両親にはそう言い聞かせている).

WiFi

いわゆるメッシュWiFi.一軒家で無線LANルーターを複数台置いていたのを置き換えました.ちょっと高い気もするけど,環境は劇的によくなりました(スプラトゥーンが落ちなくなった.我が家比).セキュリティとか気になるなら Google のやつの方がいいのかもしれない.ちょっと高いけど.

スライサー

こどもの朝ご飯にプログラマがつくる簡単なものと云えばハッシュ.ジャガイモを千切りにして炒めるやつがぼくの定番おかずなのだが,このスライサーはかなり細く切れてよかった.不満点は指ガードが小さくてちょっと頼りなく,なくなりそうなこと(実際なくなった).結局他のスライサーについてた指ガードをつかってる.なければ100均で買ってくるとよさそう.

冷蔵庫

今年一番の大物.真夏に壊れて急遽買いました.壊れた前の冷蔵庫は10年前のもので,470Lくらいだったやつですが,これは 550Lで大きさは同じでした. よく冷やせて,野菜のもちもよくなった気がします.慌てて買ったけどこれはよい買い物でした.

まぁまぁ

ディスプレイ

スプラトゥーンやりたくて買った.値段相応だがゲームする分には満足してる.仕事するにはやや不満がある.

スピーカー

スプラトゥーン用に(ry.特に不満はない.

コーヒーメーカー

使ってたコーヒーメーカーが壊れてしまったので買った.コーヒーミルは手動のがあったので全自動でないやつ.コーヒーが冷めないのがいい.手入れも簡単. だが,なんとなく前の機械の方が上手に淹れられた気がするんだよな・・・.何回か使わないと機械っぽいにおいが取れなかったので気になる人は最初はお湯を何回か通した方がいいかも.

ちょっとしたもの

HDMI ケーブル,こんなに細いのあるならだいぶすっきりするじゃん!ということを知った.

スプラトゥーンやってるとプロコン壊れやすいと聞いたので,プロコンの可動部に塗ってる.これのおかげか今のところ故障はない.

クラッシュアイスが作れる器.使い方が悪いのかこれは微妙だった.口の細い水筒に氷いれたいときにはいいかもしれない.

ゲームとか

宝石の煌き

2〜4人で遊べます.あんまりボドゲ好きじゃない妻もこれは参加してくれる.

宝石の煌き 日本語版

宝石の煌き 日本語版

カヴェルナ

これは1〜2人用.コマが多くて割と複雑だけど,ルールをのみ込めば子供(8歳)でもできる程度.プレイ時間はちょっと長め.ある程度の気合いが要る.コンボが決まったときが面白い.

カヴェルナ:洞窟対決 日本語版

カヴェルナ:洞窟対決 日本語版

パッチワーク

パッチ(端布)を順番に取り合って自分のマス目を埋めてくゲーム.ルールはわかりやすく,ちょっとした駆け引きもあるので子供ものってくれる.コマを並べるのにちょっと広めの机(場所)が必要.終わったときにお互いの成果物を見合ったりして「なかなかだね」とかいうのが面白い.2人用.

パッチワーク 日本語版

パッチワーク 日本語版

context.WithTimeout()を使うとタイムアウトが想像以上にきれいに書けた

概要

slack の Real Time Messaging を使って bot を書いてたんですが,しばらくすると websocket が切断されてて bot が落ちてしまってました.今回はこれにタイムアウト処理を追加してリカバリできるようにした防備録です.

問題のコード

func (c *Client) GetMessage() (Message, error) {
    var msg Message
    if err := websocket.JSON.Receive(c.socket, &msg); err != nil { // ← 切断されちゃうとこいつが沈黙する
        return msg, err
    }
    return msg, nil
}

タイムアウト処理を入れる

context.Context にはタイムアウト処理をうまくやってくれる仕組みがあるのでこれを使います.

func (c *Client) GetMessage() (Message, error) {
    ctx, cancel := context.WithTimeout(context.Background(), Timeout)  // ← タイムアウト付きの context を設定
    defer cancel()
    ch := make(chan error, 1)

    var msg Message
    go func() {
        ch <- websocket.JSON.Receive(c.socket, &msg)
    }()

    select {
    case err := <-ch: // websocket からメッセージがとれればここへ来る.関数を抜けるときに cancel() が defer で呼ばれて context が解放される
        return msg, err
    case <-ctx.Done(): // ← websocket が沈黙してても,時間が来たら Done() が close される
        return msg, fmt.Errorf("connection lost timeout")
    }
    return msg, nil
}

これで時間が来たら timeout でエラーが返るようになります.エラーが返ってきたら websocket をつなぎ直せば解決です.

が,これだと websocket が生きててもタイムアウトして再接続しちゃいます.タイムアウトする前に websocket を ping で叩いてみて,反応があったら単に関数を抜けるようにすれば無駄な再接続を防げそうです.

func (c Client) GetMessage() (Message, error) {
    ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
    defer cancel()
    ch := make(chan error, 1)

    go func(ctx context.Context, waiting time.Duration) {
        ctx, cancel := context.WithCancel(ctx)  // ← 親の context から新しい context を作ります
        defer cancel()
        select {
        case <-ctx.Done(): // ← 親の context が close したらここにメッセージが来て何もしないで抜ける
        case <-time.After(waiting): // timeout するちょっと前に ping を打つ
            websocket.JSON.Send(c.socket, &Message{Type: EventTypePing, Time: time.Now().Unix()})
        }
    }(ctx, c.timeout-time.Second)

    var msg Message
    go func() {
        ch <- websocket.JSON.Receive(c.socket, &msg)
    }()

    select {
    case err := <-ch: // ← メッセージが無くても websocket が生きてればタイムアウト前には pong が返ってくるはず
        return msg, err
    case <-ctx.Done():
        return msg, fmt.Errorf("connection lost timeout")
    }
    return msg, nil
}

とりあえずこれで安定して動いているようです.context.Context を使うと複雑な処理になりそうなところが想像以上に見通しのよいコードがかけて幸せです.

( '-`).oO( うまいことできた!と思ってるけど,もっとうまいことやれるような気もしてて,なにか方法があれば教えて欲しいです

f:id:ikawaha:20181018223422p:plain

Happy hacking!

Travis-CI で go の特定バージョンでだけテストカバレジを取りたい

概要

go の 1.11 がリリースされました 🙌.それに伴って,テストカバレッジの指定方法が変更されて,いままでパッケージひとつごとにしかテストカバレッジ取れなかったのが,パッケージをまとめて指定出来るようになりました.

いままでのテストカバレッジ,地味にめんどくさくて,今までは

#!/bin/bash

COV_FILE=coverage.txt
COV_TMP_FILE=coverage_tmp.cov
ERROR=""

echo "mode: count" > $COV_FILE

for pkg in `go list ./...`
do
    touch $COV_TMP_FILE
    go test -covermode=count -coverprofile=$COV_TMP_FILE $pkg || ERROR="Error testing $pkg"
    tail -n +2 $COV_TMP_FILE >> $COV_FILE || (echo "Unable to append coverage for $pkg" && exit 1)
done

rm $COV_TMP_FILE

if [ ! -z "$ERROR" ]
then
    echo "Encountered error, last error was: $ERROR"
    exit 1
fi

GOPATH=$HOME/gopath
GOVERALLS=$(echo $GOPATH | tr ':' '\n' | head -n 1)/bin/goveralls
$GOVERALLS -coverprofile=$COV_FILE -service=travis-ci

こんな感じのスクリプトを置いておいて,これを travis.yaml から呼び出してたんですが,1.11 では,

go test -covermode=count -coverprofile=$COV_TMP_FILE ./...

のようにまとめて指定出来ます.

がしかし,travis-ci では go のいくつかのバージョンをまとめて指定出来るので,前のバージョンもテストしてカバレッジを取るようにしておくと,go のバージョンを見て,どっちの方式でやるか,あるいは指定バージョンでのみカバレッジ取る,みたいな設定が必要になってきます.

やりたいことは

です.至極単純なことなんですが,設定方法をかなり試行錯誤したので,メモ代わりに置いておきます.

Travis-CI での go のバージョン判別法

変数 $TRAVIS_GO_VERSION に納められているのでこれで判別出来ます. travis-ci では 1.11.x のように末尾をワイルドカードに出来るのですが,このとき,$TRAVIS_GO_VERSION に入っているのは 1.11.x です.実際のバージョンが入っている訳ではないので気をつけてください.

どうやるか

結論から言うとこういう感じです.テストが終わったら,after_success でテストカバレッジを取って coveralls に送信してます.

language: go

go:
  - "1.9.x"
  - "1.10.x"
  - "1.11.x"
  - tip
sudo: false
cache: bundler

git:
  depth: 1

before_install:
  - pip install --user codecov
  - go get github.com/axw/gocov/gocov
  - go get github.com/mattn/goveralls
  - go get golang.org/x/tools/cmd/cover

install:

script:
  - go test ./...
  - cd tokenizer; go test -benchmem -bench .; cd ..
  - cd internal/dic; go test -benchmem -bench .; cd ../..

after_success:
  - |
    if [[ $TRAVIS_GO_VERSION = 1.11.x ]]
    then
      go test -covermode=count -coverprofile=coverage.txt ./...
      $GOPATH/bin/goveralls -coverprofile=coverage.txt -service=travis-ci
    else
      echo skip coverage test, $TRAVIS_GO_VERSION
    fi

うまくいかなかった方法

travis-ci で用意されてる condition とか on とか if を利用するのはうまくいかなかったです.こう云うのでうまくやれた方がスッキリしそうなんですが,もしもっといい書き方あるよ!というのをご存じの方がいらっしゃいましたらお教えください.

Happy hacking!

kagomeが go get できない問題

概要

kagome が go get できない!ということがある模様.

たしかに kagome は辞書をソースコードに変換して保持してるので,重いのは重いのですが,いつまでたっても終わらない・・・ということはないはずと思います. 手元でやってみたら2分〜3分,会社の回線で5分くらいでした.回線が細いと厳しいかもしれません. といっても kagome のレポジトリは 400MB 程度.今時の携帯アプリでも大きめのはこれくらいある時代ですし・・・

原因はレポジトリのサイズの他にもあるかもしれません.もし他にもいらっしゃったら教えてください.

申し訳なさ

go は go get でサクッと使えるように便利にできています.なのに kagome がこのユーザー体験を破壊していると思うと申し訳ない限り. kagome はかなり特殊な重たいライブラリなので,kagome の事を嫌いになっても go を嫌いにならないでください!みたいなお気持でいっぱいです.

回避策

1. go get コマンドの最後の ... は意味があります

$ go get -v github.com/ikawaha/kagome/...

基本はこれでいけるはずです.最後の ... がないと失敗するので注意してください.(でも失敗と分かるので落ちてこないというのは多分これではない

2. 直接 git clone する

$GOPATH/src 以下に github.com/ikawaha/kagome として git clone してみてください.このとき depth を 1 にしてください. (go get はデフォルトで depth 1 で clone してくれます)

$GOPATH設定してない場合は ~/go/src 以下に読みかえてください.

$ git clone --depth 1 https://github.com/ikawaha/kagome.git $GOPATH/src/github.com/ikawaha/kagome
Cloning into '/Users/ikawaha/go/src/github.com/ikawaha/kagome'...
remote: Counting objects: 116, done.
remote: Compressing objects: 100% (103/103), done.
remote: Total 116 (delta 21), reused 60 (delta 6), pack-reused 0
Receiving objects: 100% (116/116), 127.47 MiB | 1.61 MiB/s, done.
Resolving deltas: 100% (21/21), done.
Checking out files: 100% (93/93), done.

3. 軽量版 kagome を使う

kagome には IPADic と UniDic の2種類の辞書が入っています.が,ぶっちゃけ普通の人は UniDic は使わないでしょう. 利用方法が同じで IPADic だけを同梱した kagome.ipadic がありますのでこちらを利用してみてください. こちらは GAE でも動作するように調整してありますので,GAE で使いたい場合にもこちらをご利用ください.

$ go get github.com/ikawaha/kagome.ipadic/...

何かいい方法とかあれば教えて欲しいです・・・.

追記

前より遅くなったんじゃないかとの感想にコメントいただきました.なるほどなー

でも全く落ちてこないってのはやはり変.

追記2

github から直接落としても同じだけ時間がかかるという報告いただきました.なので落とせないときは方法の2を試しても,ダメな可能性がありそう. でもまぁ,そのときは github の転送量のせいだと問題が分離できるのでよしとする(ぉ.

追記3

解決したようです.ヤター

会社引っ越し前の新宿ランチ棚卸し

3年ほど,新宿高島屋前のオフィスで働いてきましたが, 今年の3月くらいに会社が引っ越すことになったので,新宿三丁目のランチでお世話になったお店を棚卸し.

ここも行っておいた方がいいよ!ってところがあったら是非教えてください.

( '-`).oO( とか言って次も新宿だったりするかもだけど

思いついたら追加してく.


tabelog.com

アイリッシュパブ.夜は行ったことないんですが,ランチでよくお世話になりました. ランチは950円で,週替わり3種類とカレーから選ぶ.コーヒー付く.


tabelog.com

夜は焼き鳥屋さん.新宿御苑の新宿側の入り口のところ. ランチは唐揚げかチキン南蛮を週替わりでやっていて,親子丼はレギュラーメニュー.970円.


tabelog.com

ベトナム料理のお店で,ランチはフォーのセットがおすすめ.練乳入りでとっても甘いベトナムコーヒーが付いてくるのも楽しみ.


tabelog.com

最近出来たシュラスコのお店.ランチは,一串にチキン,ソーセージ,牛肉2種x2とズッキーニ,ナスをひとかけずつ焼いたものを出してくれる. ローストビーフ丼もおいしい.どちらも1000円.時間があれば食べ放題のコースもあります.


tabelog.com

カレー2種類とナン,ドリンクが付いて950円ぐらいだった気がする.お腹いっぱいで午後は破滅するがカレー欲は満たされる.


tabelog.com

羊っぽいものが食べられる.夜も来たことあるけど,この店安いよね!


tabelog.com

パスタのお店.うまいです.定番のミートソースにするか,ピリ辛のアラビアータにするか,カルボナーラにするか迷う.麺の量をが200gの大盛りまで同じ金額で選べるので200gしか食べたことがない.+100円すれば 250g もいける.むしろ増量して食べようかな,と思うくらいおいしい.


tabelog.com

北海道スープカレー東京ドミニカ.固有表現抽出泣かせの店名.


tabelog.com

生醤油うどん,すだち,ちくわ天トッピングが大体いつものメニュー.大盛りにしてもお値段そのままだった気がする. おいしい.ただ,出汁の感じは関西風で讃岐うどんではない.とおもう.


tabelog.com

ちょっと遠いけどたまに行くイタリアン.お持ち帰りできるドリンクがサービスされてる.


tabelog.com

夜はオイスターバーで,ランチにも牡蠣入りのカレーとか食べられる. でも過去にノロのトラウマがあるので牡蠣は食べたことない. ランチは1000円くらいから.


tabelog.com

お肉が食べたいときに行くとちょっといい感じの肉が食べられる.


tabelog.com

ピザ,ハーフアンドハーフで頼んで,エスプレッソを飲む.しあわせ.

番外

tabelog.com

今はランチやってないけど,ときどき飲みに行くのに使ってた.やさい中心でおじさんの胃袋にも優しい.


tabelog.com

有名なカレー屋さん.らしい.おいしかったと思うけど,記憶が定かでない.何回かしか行ってなかった.


tabelog.com

牛カツのお店.出来た頃に何回か行ったが,いつも行列してるので通り過ぎるとき眺めるくらいになってしまった.


tabelog.com

知らないとたどり着けない感じの奥まった感じの場所にあるお店.なんとなく健康そうなものが食べられる. 夜もおすすめ.


tabelog.com

いつも混んでる.エビつけ麺のお店.


tabelog.com

電源が充実しているカフェ.


tabelog.com

ひとりの時に時々行くラーメン屋.15時にお店にいるとサービスでチャーシューの切れ端がもらえる.これマメな.


tabelog.com

もっと近くにあったら足繁く通うのにな.ちゃんとした(?)中華が食べられる.


tabelog.com

ちょっと変わった豚汁のお店.1回しか行ってない.もっと行きたかった.

形態素解析器 Sudachi の辞書が手に入ったので Go で遊んでみた

はじめに

Sudachi はワークスアプリケーションズが絶賛開発中の形態素解析器で,Java で書かれています.

今一番新しい注目すべき形態素解析器で,次のような機能が特徴としてあげられています.

  • 複数の分割単位の併用
    • 必要に応じて切り替え
    • 形態素解析と固有表現抽出の融合
  • 多数の収録語彙
    • UniDic と NEologd をベースに調整
  • 機能のプラグイン
    • 文字正規化や未知語処理に機能追加が可能
  • 同義語辞書との連携 (後日公開予定)

github.com

開発のステータスは公式には発表されていない(?)ようですが,開発中で本リリースはこれからかな・・・と思います.(実際のところ,スケジュールなどはどうなんでしょうか?)

最近,Sudachi の辞書がレポジトリからダウンロードできるようになったので,kagome の辞書に加工して少し遊んでみました. 以下,公式情報ではない&開発で変更されるかもしれないので,正しいところは元コードを当たってください.

辞書の構造

辞書構造は UniDic とほぼ同じです.

カラム 内容 備考
0 headword 新国立美術館 正規化された見出し語.アルファベットが小文字に統制されてるとか
1 left-id 5962 -1が入ってることがあるが,その場合は辞書項目として無効のようだ
2 right-id 5861
3 cost 21119
4 headword 新国立美術館 こっちの見出し語は正規化されてない見出し語っぽい
5-10 part-of-speech 名詞,固有名詞,一般,,,*
11 reading form シンコクリツビジュツカン
12 normalized form 新国立美術館 動詞だったら食べ -> 食べる みたいな
13 dictionary form word id * normalized form に相当する辞書項目があればその ID が入ってるっぽい
14 分割情報 (A/B/C) C
15 A ユニット分割 2008495/1573531/2528962/2843222 数値は形態素のIDっぽいので,これで分割される形態素を引けばよさそう
16 B ユニット分割 2008495/1573531/2528997
17 word structure 2008495/1573531/2528997 これは何に使うのかな?

見出し語

Neologd からも項目を持ってきてるみたいで,結構おもしろいエントリが入ってます. たとえば,

エターナルフォースブリザード,5144,5144,7882,エターナルフォースブリザード,名詞,固有名詞,一般,*,*,*,エターナルフォースブリザード,エターナルフォースブリザード,*,C,515901/962003/982867,515901/962003/982867,515901/962003/982867

なんてのも入ってます.UniDic では短単位に切れすぎていたので,固有名詞の長い形態素とかがキレイにとれるようになるのかもしれません.Neologd は機械的にエントリを作ってるとのことなので,Sudachi に取り込まれる際にはどのような基準で取り込まれるのか,機械的にやるのか,人目でチェックするのか,などなど気になります.まだこの辺は調整されるのかもしれないので今後の辞書開発に期待です.

あと,遊ぶ上での注意点としては見出し語にユニコードリテラルがコードポイントで表現されていることがあります.

!!\u0028 ; ロ\u0029゚ ゚,5977,5977,5000,!!\u0028 ; ロ\u0029゚ ゚,記号,一般,*,*,*,*,カオモジ,!!\u0028 ; ロ\u0029゚ ゚,*,A,*,*,*

こんなエントリがありますが,\u0028ユニコードリテラルとして解釈してやる必要があります.

分割単位

Sudachi では短い方から A, B, C の3つの分割モードが提供されています. A は UniDic 短単位相当,C は固有表現相当,B は A, C の中間的な単位とのことです.

README.md に掲載されていた例を示します。

A:医薬/品/安全/管理/責任/者
B:医薬品/安全/管理/責任者
C:医薬品安全管理責任者

A:自転/車/安全/整備/士
B:自転車/安全/整備士
C:自転車安全整備士

A:消費/者/安全/調査/委員/会
B:消費者/安全/調査/委員会
C:消費者安全調査委員会

A:新/国立/美術/館
B:新/国立/美術館
C:新国立美術館

辞書では長い(分割単位BとかCの)エントリに分割情報がついているので,表示する際に分割してやれば目的を達成できそうです.

とりあえず動かしてみる

kagome は UniDic を扱えるので,UniDic の辞書だと思って Sudachi を辞書をコンパイルして動かしてみました.

お約束の すもももももももものうち です.

f:id:ikawaha:20171018180742p:plain

Sudachi には すもももももももものうち というずばりそのもののエントリもあるのですが,そちらは選ばれませんでした. Sudachi 自体で動かしてみましたが,同じ解析結果になったので,やり方を外しているわけではないみたいです.

f:id:ikawaha:20171018180638p:plain

TODO

kagome にそのまま取り込んで,分割情報で調整してやれば大体うまく動きそうな感触を得ました. kagome ではやってない Sudachi の以下の機能は別途開発しないとだめかな・・・ という感じです.

  • [ ] 入力テキスト修正
    • 文字列正規化 (全半角、大文字/小文字、異体字)
  • [ ] 長音正規化 (「~」や長音記号連続の正規化.まだリリースされてない?)
  • [ ] 未知語処理
    • 1文字未知語
  • [ ] 単語接続処理 (品詞接続禁制)
  • [ ] 出力解修正
    • カタカナ未知語まとめ上げ
    • 数詞まとめ上げ
  • [ ] 分割粒度調整 (未知語/既知語の分割粒度の平滑化.まだリリースされてない?)
  • [ ] 数詞正規化 (漢数詞や位取りの正規化.まだリリースされてない?)
  • [ ] 人名補正 (敬称や前後関係から人名部を推定.まだリリースされてない?)

Sudachi の辞書使って遊んでみたいと思ってる人の一助になれば幸いです. Happy Hacking!

goa tips: 要素がポインタになるのを避ける

はじめに

Payload の Member が必須要素でない場合,生成される対応するコードはポインタで表現されます. 文字列なら,string のポインタで表されるわけで,ポインタを渡すために変数を用意してポインタで渡してやるみたいなひと手間が必要になってきます. これはややめんどくさいので,要素がポインタになるのを避けよう・・・というものぐさな方針です.

※ もっといい方法があればしりたいです・・・

Payload や MediaType の要素がポインタになるのはどんなときか

Payload や MediaType の要素が(生成されたコードで)ポインタで表現されるのは,要素が必須要素でないときです. たとえば,下記のような Payload では,bottleIDcategory は required ですが,comment は省略可能です.

BottlePayload = Type("BottlePayload", func() {
        Member("bottleID", Integer, "Bottle ID", func(){
                Minimum(0)
                Maximum(127)
        })
        Member("category", String, "Category", func(){
                Enum("red", "whilte", "rose")
                Default("red")
        })
        Member("comment", String, "Comment", func(){
                MaxLength(256)
        })
        Required("bottleID", "category") // ← 必須要素の指定
})

生成される対応するコードは,下記のようになります.Comment*string で表現されてます.

// BottlePayload user type.
type BottlePayload struct {
        // Bottle ID
        BottleID int `form:"bottleID" json:"bottleID" xml:"bottleID"`
        // Category
        Category string `form:"category" json:"category" xml:"category"`
        // Comment
        Comment *string `form:"comment,omitempty" json:"comment,omitempty" xml:"comment,omitempty"` // ← こいつがポインタあつかい
}

必須要素にしないときの振る舞いは?

Payload で要素を必須要素にしない場合,その要素は省略できます. つまり,json で渡そうと思ったとき,

{ "bottleID" : 1, "category": "white" }

の様に(省略可能な) comment は書かなくても大丈夫です.必須要素の bottleID を省略すると,goa が生成するバリデーションではねられます.

MediaType の場合は required でなければ,ゼロ値であればレスポンスから項目が省略されます.

必須要素でも記述を省略することは可能

大体問題になるのは,Payload の要素で省略したいものがある場合だと思います. しかし,単に記述を省略したいのでしたら,必須要素にして Default DSL を利用することで,あたかも要素を省略したように記述できます.

BottlePayload = Type("BottlePayload", func() {
        Member("bottleID", Integer, "Bottle ID", func(){
                Minimum(0)
                Maximum(127)
        })
        Member("category", String, "Category", func(){
                Enum("red", "whilte", "rose")
                Default("red")
        })
        Member("comment", String, "Comment", func(){
                Default("")                       // ← ★
                MaxLength(256)
        })
        Required("bottleID", "category", "comment") // ← 必須要素の指定
})

こうしておくと,comment の値の初期値は空文字列になります.なので,

{ "bottleID" : 1, "category": "white" }

という Payload を渡しても,初期値が設定されるので,required の要件を満たします(詭弁ぽいけど). なので,初期値を設定できる要素であれば,必須要素にしておいて,Default を指定することで記述は省略可能になります.

ただし,null と空文字列を区別したいような場合にはこの方法は出来ません.

逆に言えば,null と初期値値を区別したいような場合だけ,必須要素でなくす必要がある・・・という方針でとりあえずうまくいっています.

といっても,これは goa でポインタ使いたくないと云うためのものぐさな手法で, API の作法的にどうなのかと常々疑問に思っているので,何か知見がある方いらしたらコメントいただければ嬉しいです.

goa tips : v1.3 で追加された HashOf のバリデーションを使う

はじめに

goa のこと書くのずいぶん久しぶりになってしまいました. 仕事でも使ってて安定して動いていて,あんまり目新しいこともなくなってきて, API はこれで書くのが当たり前になりつつあり・・・.

まぁ,そんな言い訳はどうでもいいんですが, うかうかしているうちに v1.3 がリリースされました! 🙌

goa v1.3.0 · goa :: Design-first API Generation

v1.3 では,いくつか機能が追加されたんですが 密かに期待していた HashOf のバリデーションを設定する機能が追加されたのでこれを紹介しようと思います.

HashOf の機能

HashOf は key と value からハッシュマップをつくる DSL です.

使い方とかはこの辺を参照してください http://ikawaha.hateblo.jp/?page=1473951553

たとえばワインを評価するようなデータが,ワインの銘柄と5段階評価のペアの集合で受け渡されるとしたときに, goa だったら,こんな感じのエンドポイントが用意できます.

Action("updateRatings", func() {
    Payload(func() {
        Member("ratings", HashOf(String, Integer))  // ← ワインの銘柄(文字列)と評価(数字)のペアみたいなのを受け取りたい
})

いままでは,これはデータを受けとった後,コントローラーで入っている銘柄や評価をバリデーションしてました. たとえば,評価が,1〜5 に収まってるかどうか,とか.

でも,これって goa っぽくないですよね :p

v1.3 では,このバリデーションをデザインに書けるようになりました.

Action("updateRatings", func() {
    Payload(func() {
        Member("ratings", HashOf(String, Integer,
            func() { Pattern("[a-zA-Z]+") },  // key のバリデーション
            func() { Enum(1, 2, 3, 4, 5) },    // value のバリデーション
    ))
})

めっちゃスッキリです.

この機能,実は v1.2 の時点で既に ArrayOf に似たような機能が実装されてて,ArrayOf の要素はバリデーション出来るようになってました.HashOf についても同様に出来るようなことがドキュメントに書かれてて,これは使える!とウキウキしてたんですが,実際使ってみると,バリデーションは受けとるんだけど,生成されるコードにバリデーションする部分のコードがなかったのでした orz

そんなわけで,この機能の中身は自分で pr しました.なんとも OSS っぽい体験.

開発もだいぶ落ち着いてる感じのある goa ですが,v2 の開発がかなり進んで,もうだいぶ形になってるみたいです. v2 では gRPC にも対応してくとのことなので,今からリリースが楽しみですねー.

形態素解析器 kagome を Google App Engine の最も安いインスタンスで動かす

概要

前回までのあらすじ: kagome を GAE で動かしたいという話があり,kagome.ipadic という IPA 辞書だけを収めたコンパクト版を用意して,GAE 上で動作させることに成功したわけですが,メモリの消費量が多くて B4インスタンス(メモリ 512MB)以上じゃないと動作しなかったわけです.正直,kagome を GAE で動かしたいという話をチラホラ聞くものの,それは「動くかどうか試してみたい」的なやつで,実際使ってらっしゃるという話は聞こえてこなかったわけです.

ところが・・・

kagome はちと動作に敷居が高いとのご指摘を受け,

不幸にも黒塗りの高級車に追突してしまう後輩をかばいすべての責任を負った三浦に対し,車の主,暴力団員谷岡に言い渡された示談の条件とは…。

参考:

Instance Class Memory Limit CPU Limit
B1 128 MB 600 Mhz
B2 256 MB 1.2 Ghz
B4 512 MB 2.4 Ghz
B4_1G 1024 MB 2.4 Ghz
B8 1024 MB 4.8 Ghz
F1 128 MB 600 Mhz
F2 256 MB 1.2 Ghz
F4 512 MB 2.4 Ghz
F4_1G 1024 MB 2.4 Ghz

何が問題か

メモリ使用量です.Java 実装の gomoku (Igo の派生) では動くとのことなので,ここにヒントがありそうです.

github.com

gomoku の特徴にはこうあります

  • Igo( http://igo.sourceforge.jp/ )から派生した形態素解析
  • 辞書データがあらかじめjarファイルに含まれていることが特徴
  • デフォルトではIPADIC(mecab-ipadic-2.7.0-20070801)を使用している
  • common lispで書かれた辞書構築コマンドを使うことでカスタマイズは可能
  • 現状ではIPADIC以外に対応しようとする場合は(おそらく)ソースコードの修正が必要
  • 素性としては品詞情報のみを保持
  • 原型や読みの情報などの情報は破棄している
  • その他、形態素解析器としての特徴は、おおむねIgoと同様

なるほど.たしかに辞書の付加項目をなくせば使用メモリが減らせそうです.

品詞
品詞細分類1
品詞細分類2
品詞細分類3 ↑ ここまでは保持
活用型    ↓ ここから先は別管理にして必要なときだけロードする
活用形
基本形
読み
発音

削減結果

変更前

実は F2 インスタンスでもぎりぎり動いたらしい.253 MB 必要. f:id:ikawaha:20170602004106p:plain

変更後

97.5 MB まで削減され,B1/F1 インスタンスで動くようになりました 🙌 ヤター

f:id:ikawaha:20170602004321p:plain

実際に動作確認してくださったり報告してくださった Kazzz さん,ありがとうございました!

ぜひ GAE/Go で形態素解析してみてください.Happy Hacking ! github.com

golang で utf8 でない文字列を for range で回すとどうなるか

概要

golang の string は意識しなければたいていの場合 utf8 な文字列なわけですが, ファイルからテキスト読んできたりすると,非 utf8 な文字列が混じることとかあります. そんな 非 utf8 な文字列を for range で回した場合の挙動でハマったのでメモ.

端的に言うとRob Pike 氏のこれをよく読もう:

Strings, bytes, runes and characters in Go - The Go Blog

文字ごとに処理する

golang で string の長さをとると,それは byte 長です.たとえば len("日本語") の値は 9 になります. いわゆる文字()のことを golang では rune と定義していますが,文字ごとに処理したい場合には, for range で文字列を回してやればうまくいきます.(以下,文字とは rune の事と読み替えて下さい)

const s = "日本語"
for i, r := range s {
    fmt.Printf("%#U starts at byte position %d\n", r, i)
}

の結果は,

U+65E5 '日' starts at byte position 0
U+672C '本' starts at byte position 3
U+8A9E '語' starts at byte position 6

のようになります.文字の開始位置と文字が取れる仕組みです.

文字列が utf8 として正しくない場合はどうなるか?

golang のプログラムは utf8 で書くことになっているので,普段意識せずに string を使っていれば, string は utf8 のバイト列で表現されています.しかし,string は単に変更できない byte 列のようなものなので, utf8 として正しくないものも設定することが出来ます.また,ファイルから文字列を読み込んでくる場合など, 日本語環境では sjis などの文字列を読み込んで文字列に設定してしまうかも知れません.

そのようなとき,上の for range によるループでは返値はどうなるでしょうか?

const s = "\x93\xFA\x96\x7B\x8C\xEA" // "日本語" の sjis 表現
for i, r := range s {
    fmt.Printf("%#U starts at byte position %d\n", r, i)
}

答えはこうなります.

U+FFFD '�' starts at byte position 0
U+FFFD '�' starts at byte position 1
U+FFFD '�' starts at byte position 2
U+007B '{' starts at byte position 3               // ← ここだけたまたま文字として取れてる
U+FFFD '�' starts at byte position 4
U+FFFD '�' starts at byte position 5

for range の処理は,文字として取れなければ,1 byte ごと進める. そして,文字として取れないときは,RuneError という「文字」を返す,という動作になります. この RuneError は コードポイントとしては U+FFFD で表されて,utf8 の表現では, 0xEF 0xBF 0xBD という byte 列になり, 長さは 3 byte です.

なので,文字が返ってきたと思って,この文字の utf8 の長さ基準に処理を進めると,for は 1 byte しか進まないのに,文字の長さは 3 byte あって おかしな事になります.# そんな処理を書いていて,バグを生みました orz

これは,utf8.DecodeRune(String) などの文字列から文字をとる関数でも同様です.

実は,Rob Pike 氏の詳しい解説 Strings, bytes, runes and characters in Go にもこのことが触れられているのですが,

f:id:ikawaha:20170519144942p:plain

練習問題になっているのでした.答えを知りたかったです,先生.

ANTLR のターゲットに Go が追加されたので Gogland とあわせて遊んでみる

概要

ANTLR ってのは,いわゆるパーサジェネレーターです. 去年の年末に出たバージョン 4.6 からターゲットに Go が追加されました 🙌 . かなり昔に使ったことあったんですが,v4 になってだいぶ整理されて洗練された感じになってました.

この記事は結構適当にやってしまっていると思うので,ちゃんとやりたいならリファレンスを読んだ方がよさそうです(ぉ.

The Definitive ANTLR 4 Reference

The Definitive ANTLR 4 Reference

ANTLR の簡単な紹介

入力のあるプログラム書いていると,入力が well-formed であるかどうかをチェックする必要があることがあります. 正規表現とか使って自分で入力が正しいかどうかチェックする程度の時は良いんですけど,複雑になってくると フルスクラッチで書いてメンテしていくのはつらい感じになってきます.

そんなときのための,そういった parser を作るフレームワークANTLR です.

ANTLR 自体は Java で書かれていますが,生成する parser は

に対応しています.JavaScript とかかなりクールだと思うのですが,まったく詳しくないので 気になる記事だけ貼っておきます. きっと JavaScripter の id:takuya-a =san がそのうちなんかやってくれると思います.

ANTLR and the web: a simple example - Federico Tomassetti - Software Architect

ANTLR を特におすすめするのは,作った文法を試しながら修正したりできるというところです. v4 より前までは ANTLR Works という IDE が別に作られていたんですけど,v4 は EclipseIntelliJ 用のプラグインが配布されています. 勘のよい方ならお気づきかもしれませんが,IntelliJプラグイン使えるんなら Gogland でも使えて,Go の開発はかどるんじゃないかと思ったら, まさにいけました ╭( ・ㅂ・)و ̑̑ グッ !.

f:id:ikawaha:20170318161618p:plain:w300

インストール

ANTLR

本家でダウンロードするか,mac だったら brew でも入ります.

今回は Go をターゲットにするので,Go 用のプラグインをダウンロードします.もちろん go get でいけます

go get github.com/antlr/antlr4/runtime/Go/antlr

ここで注意しなきゃいけないのは,go get した ライブラリにタグが振ってあるので,こいつを 4.6 に合わせておかなければいけない ということです.これでずいぶんハマりました.ベンダリングして管理するのが吉かもしれません.

✔ ~/go/src/github.com/antlr/antlr4 [master ↓·57|…1]
$ git checkout tags/4.6

Gogland

いまは Eary Build が出てるのでこれをインストールしてください.

www.jetbrains.com

ANTLRプラグインを追加する

Gogland で ANTLR 用のプラグインを追加します.

preferenes >> plugins から ANTLR プラグインを選択するだけです.簡単.

f:id:ikawaha:20170318163636p:plain:w300

文法を書く

では,ANTLR で文法を書いてみましょう.四則演算を受理する簡単な文法を書いてみます.

ファイル名は grammar で指定する文法名と同じにする必要があります.下の例だと Calc.g4 としてください.

grammar Calc;

prog:   expr NEWLINE*;
expr:   '(' expr_=expr ')'
    |   left=expr op=('*'|'/') right=expr
    |   left=expr op=('+'|'-') right=expr
    |   atom=INT
    ;

NEWLINE : [\r\n]+ ;
INT     : [0-9]+ ;

パーサーを生成する

Gogland から Go で書かれたパーサーを生成できます. まずは,ALTLR の 設定をしておきます.

f:id:ikawaha:20170318200209p:plain:w300

f:id:ikawaha:20170318200555p:plain:w300

Language のところに出力言語である Go を指定します.出力されるコードのパッケージは parser がデフォルトのパッケージ名になるので, 出力パスを parser として適当なところに出力させます.ListenerVisitorチェックボックスは後で説明しますが,とりあえずチェックしておいてください.

この状態で,Generate ANTLR Recognizer を選ぶとコードが出力されます.

パーサーを使ってみる

上の設定だと parser フォルダ以下にファイルが出力されます.

package main

import (
    "fmt"

    "github.com/antlr/antlr4/runtime/Go/antlr"

    "github.com/ikawaha/antlr/parser"
)

func main() {
    input := antlr.NewInputStream("12+3*4") // ← 入力

    lexer := parser.NewCalcLexer(input)
    stream := antlr.NewCommonTokenStream(lexer, 0)
    p := parser.NewCalcParser(stream)
    tree := p.Prog()  // ← ルールのトップに対応する関数が用意されてるのでそれを起動すると解析して解析木を返す

    fmt.Println(tree.ToStringTree([]string{}, p)) ← 解析木を LISP-style で表示する,最初の文字列配列をなぜ与えるのかはよく分からん
}

出力

(prog (expr (expr 12) + (expr (expr 3) * (expr 4))))

Listener と Visitor

ANTLR では,構文解析木を作ってから解析をするのではなくて,Listener や Visitor と呼ばれるものをあらかじめ Parser にセットしておいて解析しながら処理を行うことができる仕組みがあります.Listener は解析木を深さ優先で処理していく場合に使い,解析木の形に合わせてたどるノードを変えたい場合には Visitor を使います. Listener も Visitor もひな形ができているのでそれを使うだけです.

解析しながら式を計算するように Listener を用意してみます.基本的には parser フォルダ以下のファイルは編集しなくて良いはずです. Listener はノードに入るときと出るときにあわせてメソッドが用意されているので,それを上書きして処理を書きます.

parser 下にできる calc_base_listener.go がそれです.

type CalcListener interface {
        antlr.ParseTreeListener

        // EnterProg is called when entering the prog production.
        EnterProg(c *ProgContext)

        // EnterExpr is called when entering the expr production.
        EnterExpr(c *ExprContext)

        // ExitProg is called when exiting the prog production.
        ExitProg(c *ProgContext)

        // ExitExpr is called when exiting the expr production.
        ExitExpr(c *ExprContext)
}

こいつを利用して処理を書きます.自前の CalcListener を用意してみます. (Antlr4 で電卓を作る : 0xdeadbeef を参考にさせていただきました)

package main

import (
    "fmt"
    "strconv"

    "github.com/ikawaha/antlr/parser" // ← ここは生成したコードがある場所を指定
)

type CalcListener struct {
    *parser.BaseCalcListener // ← こいつを上書きして処理を書く
    Stack []int // ← 計算用のスタック
    Error error // ← エラーの記録用(こうするのが正しいのかはよく分かってない)
}

func NewCalcListener() *CalcListener {
    return &CalcListener{Stack: []int{}}
}

func (l *CalcListener) ExitExpr(ctx *parser.ExprContext) { // 解析木の expr ノードを出るときに呼ばれる
    if l.Error != nil {
        return
    }
    if ctx.GetExpr_() != nil {
        return
    }
    if ctx.GetOp() != nil {
        rhs := l.Stack[len(l.Stack)-1]
        l.Stack = l.Stack[:len(l.Stack)-1]
        lhs := l.Stack[len(l.Stack)-1]
        l.Stack = l.Stack[:len(l.Stack)-1]

        switch ctx.GetOp().GetText() {
        case "*":
            l.Stack = append(l.Stack, lhs*rhs)
        case "+":
            l.Stack = append(l.Stack, lhs+rhs)
        case "/":
            l.Stack = append(l.Stack, lhs/rhs)
        case "-":
            l.Stack = append(l.Stack, lhs-rhs)
        default:
            l.Error = fmt.Errorf("unknown op, %v", ctx.GetOp().GetText())
        }
        return
    }
    if ctx.GetAtom() != nil {
        i, err := strconv.Atoi(ctx.GetAtom().GetText())
        if err != nil {
            l.Error = fmt.Errorf("invalid value, %v", err)
        }
        l.Stack = append(l.Stack, i)
        return
    }
}

main.go をチョット書き換える.

package main

import (
    "fmt"

    "github.com/antlr/antlr4/runtime/Go/antlr"

    "github.com/ikawaha/antlr/parser"
)

func main() {
    input := antlr.NewInputStream("1+(2+3)*4-5")

    lexer := parser.NewCalcLexer(input)
    stream := antlr.NewCommonTokenStream(lexer, 0)
    p := parser.NewCalcParser(stream)

    listener := NewCalcListener() // Listener を parser にセットする
    p.AddParseListener(listener)

    tree := p.Prog()
    fmt.Println(tree.ToStringTree([]string{}, p))
    fmt.Printf("---> %+v\n", listener.Stack) // Listener の中身を出力する
}

結果

$ go run *.go
(prog (expr (expr (expr 1) + (expr (expr ( (expr (expr 2) + (expr 3)) )) * (expr 4))) - (expr 5)))
---> [16]

Visitor を使えばもっと凝ったことができると思います.使い方はおおむね Listener と同じです. Vistor や Listener を使う場合には,結果としての解析木を構築しなくてもいいわけなので,これを off にする方法が提供されています. parser の BuildParseTreesfalse にセットしてください.

Gogland で ANTLR preview を使う

GUI で任意の入力をあたえてインタラクティブに解析木をチェックすることができます. チョットわかりにくいのは,文法ファイルを開いて,テストの対象とするノードを選択することです. これが選択されてないと赤字で select from navigator or grammar とエラーが出ると思います.

ノードを選択したら,入力欄に好きなように文字列を入れてください.右側にどんどん解析木が組み上がってくと思います.

f:id:ikawaha:20170319000506p:plain:w300

僕もチョット触ってみただけで,たいしたこと書いてない割に長くなってしまいましたが,いろいろ遊べるツールだと思うので,是非リファレンスをあたってみてください.#そして便利な使い方を教えて欲しい

Happy Hacking !