2020/01/20


昨年から Oracle Cloud の無料枠を使っています。2 vCPU な VM を2台も無料で使わせて頂けるという Oracle Cloud さんの大盤振る舞いに感謝しつつ、Oracle Cloud Function でしりとりをしてみました。

Oracle Cloud Function は Fn Project というサーバレスプラットフォームをベースにしており、同プロジェクトの fn というツールを使う事で、他の Fn Project を使うクラウドと同様に操作を行う事ができます。
Fn Project

Open Source. Container-native. Serverless platform.

https://fnproject.io/

fn コマンドは以下の手順でインストールする事ができます。

curl -LSs https://raw.githubusercontent.com/fnproject/cli/master/install | sh

MacOS の場合は homebrew からインストールが可能。

brew update && brew install fn

Windows の場合は僕が送っている pull-request を使わないとエラーになります。ソースを git clone して go build して下さい。

Fix atomicwrite for Windows by mattn - Pull Request #605 - fnproject/cli
https://github.com/fnproject/cli/pull/605

fn コマンドを使える様になるまでは sugimount さんが Qiita に書いている記事を参考にしました。

サーバレスな Oracle Functions (Fn) をやってみた - Qiita
https://qiita.com/sugimount/items/018e08f575ecefb1546c

注意点としては、Oracle Cloud はデフォルトで VCN が作られていますが、Function を使う場合は別途 VCN を作らないと DHCP オプションが一致しないというエラーが出てしまいます。新しく作って下さい。あと sugimount さんが書かれているリポジトリ名と異なる物が実際には用意されるので、詳細は Oracle Cloud のダッシュボードからファンクションを選択し、アプリケーション詳細の「開始」タブを見るとほぼやるべき事が書いてあります。

Oracle Cloud

アプリケーションを作る所まで出来たら、しりとりサーバを作ります。短いのでコード全体を載せます。

package main

import (
    "bufio"
    "context"
    "encoding/json"
    "errors"
    "io"
    "math/rand"
    "strings"

    _ "func/statik"

    fdk "github.com/fnproject/fdk-go"
    "github.com/rakyll/statik/fs"
)

var upper = strings.NewReplacer(
    "ぁ""あ",
    "ぃ""い",
    "ぅ""う",
    "ぇ""え",
    "ぉ""お",
    "ゃ""や",
    "ゅ""ゆ",
    "ょ""よ",
)

func kana2hira(s stringstring {
    return strings.Map(func(r runerune {
        if 0x30A1 <= r && r <= 0x30F6 {
            return r - 0x0060
        }
        return r
    }, s)
}

func hira2kana(s stringstring {
    return strings.Map(func(r runerune {
        if 0x3041 <= r && r <= 0x3096 {
            return r + 0x0060
        }
        return r
    }, s)
}

func search(text string) (stringerror) {
    rs := []rune(text)
    r := rs[len(rs)-1]

    statikFS, err := fs.New()
    if err != nil {
        return "", err
    }
    f, err := statikFS.Open("/dict.txt")
    if err != nil {
        return "", err
    }
    defer f.Close()
    buf := bufio.NewReader(f)

    words := []string{}
    for {
        b, _, err := buf.ReadLine()
        if err != nil {
            break
        }
        line := string(b)
        if ([]rune(line))[0] == r {
            words = append(words, line)
        }
    }
    if len(words) == 0 {
        return "", errors.New("empty dictionary")
    }
    return words[rand.Int()%len(words)], nil
}

func shiritori(text string) (stringerror) {
    text = strings.Replace(text, "ー""", -1)
    if rand.Int()%2 == 0 {
        text = hira2kana(text)
    } else {
        text = kana2hira(text)
    }
    return search(text)
}

func handleText(text string) (stringerror) {
    rs := []rune(strings.TrimSpace(text))
    if len(rs) == 0 {
        return "", errors.New("なんやねん")
    }
    if rs[len(rs)-1] == 'ん' || rs[len(rs)-1] == 'ン' {
        return "", errors.New("出直して来い")
    }
    s, err := shiritori(text)
    if err != nil {
        return "", err
    }
    if s == "" {
        return "", errors.New("わかりません")
    }
    rs = []rune(s)
    if rs[len(rs)-1] == 'ん' || rs[len(rs)-1] == 'ン' {
        s += "\nあっ..."
    }
    return s, nil
}

func main() {
    fdk.Handle(fdk.HandlerFunc(myHandler))
}

type Siritori struct {
    Word string `json:"word"`
}

func myHandler(ctx context.Context, in io.Reader, out io.Writer) {
    var s Siritori
    json.NewDecoder(in).Decode(&s)
    var err error
    s.Word, err = handleText(s.Word)
    if err != nil {
        s.Word = err.Error()
    }
    json.NewEncoder(out).Encode(&s)
}

辞書ファイルは statik を使ってバイナリに埋め込みました。ソースコードは GitHub に置いておきます。

mattn/oracle-cloud-function-siritori
https://github.com/mattn/oracle-cloud-function-siritori/

デプロイは以下の手順で行います。

$ fn  --verbose deploy --app [your-app]

デプロイが完了すると標準入力で JSON を受け取り、標準出力で JSON を出力するコマンドが動く様になっています。

Oracle Cloud

VCN を作らないといけない事に気付くまで結構時間を使ってしまったけど、動く事が分かってからは結構サクサク操作できる様になりました。これだけ遊んでもまだ無料範囲内らしいので、もう少し遊んでみたいと思います。



/ (1970年01月01日)
 
発送可能時間:

Posted at by



2019/10/10


8月に Google Developers Expert となり、新米の様にオロオロとしています。過去の GDE ミーティングの議事録を見せて頂いているのですが Google Document に保存されており、Go だけでなく他のカテゴリの GDE に関する物も含めると全てに目を通すのはなかなか骨が折れます。技術者なので問題は技術で解決すべく、これらの資料を grep 検索できる様にしました。

Google Document はエクスポートすると Microsoft Word の形式となるので、Microsoft Word から Markdown に変換するプログラムを書けばテキスト検索もできるし、なんならそのまま GitHub に貼り付けてしまう事もできます。

GitHub - mattn/docx2md

docx2md Convert Microsoft Word Document to Markdown Usage -1 docx2md NewDocument.docx Installation -1 ...

https://github.com/mattn/docx2md

出力は UTF-8 のテキストファイルなので、grep コマンドを使って検索できます。Windows から日本語を使って検索するのであれば jvgrep を使って頂く事もできます。

docx2md

ぜひ御活用ください。また、まだ未対応の書式が幾つかあるのでプルリクエストをお待ちしています。

そうそう、GitHub Sponsors を開設したのでよろしくお願いします!

Sponsor @mattn on GitHub Sponsors
https://github.com/users/mattn/sponsorship
改訂2版 みんなのGo言語 改訂2版 みんなのGo言語
松木 雅幸, mattn, 藤原 俊一郎, 中島 大一, 上田 拓也, 牧 大輔, 鈴木 健太
技術評論社 Kindle版 / ¥2,350 (2019年08月01日)
 
発送可能時間:

Posted at by



2019/08/06


Go の標準パッケージのコードには稀に意図的にそうなっているのか分からない、速度に寄与するのかどうか確かめたくなる物が入っている事があります。

先日も見つけました。まだマージされてないですが、os.Mkdir に NUL という文字列を渡した時にファーストパスでエラーを返す変更です。

186139: os: return an error when the argument of Mkdir on Windows is os.DevNull
https://go-review.googlesource.com/c/go/+/186139/5/src/os/file.go#563

ここで出てくる以下のコード。

func isDevNull(name stringbool 
    if len(name) != 3 {
        return false
    }
    if name[0]|0x20 != 'n' {
        return false
    }
    if name[1]|0x20 != 'u' {
        return false
    }
    if name[2]|0x20 != 'l' {
        return false
    }
    return true
}

3文字の NUL を大文字小文字無視で比較しています。ビットマスクで大文字小文字を同一視しつつ、1つでも条件にマッチしない物があれば即 false を返すという古き良きC言語的なハックが使われています。

さて、このコードは本当に速度に寄与するのでしょうか?

package lowercase

import (
    "strings"
    "testing"
)

func isDevNull1(name stringbool {
    if len(name) != 3 {
        return false
    }
    if name[0]|0x20 != 'n' {
        return false
    }
    if name[1]|0x20 != 'u' {
        return false
    }
    if name[2]|0x20 != 'l' {
        return false
    }
    return true
}

func isDevNull2(name stringbool {
    if len(name) != 3 {
        return false
    }
    if name[0!= 'n' && name[0!= 'N' {
        return false
    }
    if name[1!= 'u' && name[1!= 'U' {
        return false
    }
    if name[2!= 'l' && name[2!= 'L' {
        return false
    }
    return true
}

func isDevNull3(name stringbool {
    return strings.ToLower(name) == "nul"
}

var tests = []struct {
    in     string
    result bool
}{
    {"nul"true},
    {"Nul"true},
    {"nui"false},
    {"lun"false},
    {"nulllllllllllllll"false},
    {"nuuuuuuuul"false},
    {strings.Repeat("N"3000), false},
}

func test(b *testing.B, f func(string) bool) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        for _, test := range tests {
            if got := f(test.in); got != test.result {
                b.Fatalf("want %v but got %v for %v", test.result, got, test.in)
            }
        }
    }
}

func BenchmarkS1(b *testing.B) {
    test(b, isDevNull1)
}

func BenchmarkS2(b *testing.B) {
    test(b, isDevNull2)
}

func BenchmarkS3(b *testing.B) {
    test(b, isDevNull3)
}

isDevNull1 が今回のコード、isDevNull2 が改良前のコード、isDevNull3 が入力文字を予め小文字に変換し比較するコードです。ベンチマークの実行結果は以下の通り。

goos: windows
goarch: amd64
pkg: github.com/mattn/go-sandbox/b3
BenchmarkS1-4           47997120                25.5 ns/op
BenchmarkS2-4           41377027                28.5 ns/op
BenchmarkS3-4             136354              8786 ns/op
PASS
ok      github.com/mattn/go-sandbox/b3  3.840s

Windows 64bit Core i7 16GB の結果です。今回改良されるコードが微妙ながら速度に寄与している事が分かりました。逆に言えばこの程度しか寄与していないので、可読性を優先する様なコードであれば isDevNull2 で充分かなとも思います。

改訂2版 みんなのGo言語 改訂2版 みんなのGo言語
松木 雅幸, mattn, 藤原 俊一郎, 中島 大一, 上田 拓也, 牧 大輔, 鈴木 健太
技術評論社 Kindle版 / ¥2,350 (2019年08月01日)
 
発送可能時間:

Posted at by