ぱいぱいにっき

ぱいぱいにっき

Pythonが好きすぎるけれど、今からPerlを好きになりますにっき

YAPC::Hakodate 2024に行ってきました #yapcjapan

お疲れさまです! ブログ書くまでがYAPCということでやっていきます。

yapcjapan.org

前夜祭

前夜祭ではガラナを受け取って早速北海道に来た感が出ておりました。この後アンカファレンスという形で会が始まり、序盤はmoznionさんやpasta-kさんの話を聞いてたんですが、目線が合ってしまい私も壇上に上がることに。AIとか色々重いテーマでしたが、わりかし今自分が他人と話したいことが話せてよかったかと思います。文芸的プログラミングのくだりとかまさにそうですね。

そのあとは基本的には後ろで話したり(声がもし大きかったらすみませんでした)、別室に移動してきた人と話していました。

懇親会が終わり、たまには知らない人についていこうかなと思ってたら、結局ホテルに帰ってセコマのホットシェフを食ってました。知らない人の飲み会について行くスキルなのか度胸なのか身につけたいですね。

本編

バスに乗る前に爆速で食べた海鮮丼

当日の会場であるはこだて未来大学は市街地から離れたところにあるため、事前に購入しておいたバスチケットでの移動となります。その出発時間が早いのですが、函館朝市の場所が乗り場である函館駅から近くて助かりました。おかげで余裕が20分しかなかったものの、シュッと食べて「とにかく函館に来て海鮮食ったぞ!」という気分は味わえました。もちろん味も良かった。

会場に到着

会場のみらい大ですが、地上から入ったところが3階っていうのは、大学や高専あるあるの構造ではありますが、非常に綺麗かつでかい箱に吹き抜けと釣り廊下を駆使した構造で非常に面白かったです。入ってすぐのフロアに体育室的なものがあるのは、雪が降っても外に出ずに移動できるような工夫でしょうか。普通ああいう建物は他の大学だと独立した建物になってがちだと思ったので。

またブースも色々回って各社色々やっているなあという感じ。弊社もまた出したいなあ。

さてそうこうしているうちに本編が開始しました。ここからは私が聞いた各トークの感想について簡単に紹介してきます。

Perlで始めるeBPF: 自作Loaderの作り方

speakerdeck.com

eBPFは聞いたことはあるものの、まだ具体的に何に使うのかというのがわからない状態で話を聞きました。システムコールカーネル内でトラップして解析するような用途に使うのかなと思ったのですが、もっと広範囲に使える、つまりカーネルモジュール全般を作れる機構であることがここで初めてわかった気がします。

私はPerlはわりかしよく書いている方なのでpack/unpackが来ると興奮します。前回の私のパスキーを実装する発表でもやった感じですね。Perlは意外とバイナリに強いんだなあ(本当かな?) ここではELFのパースに使ったり、syscallでロードする時の引数を作ったりする時に大活躍。

このトークでは最初から「知らなかったを聞く」ができて大変良かったですね。

2024年秋のPerl

これがないとYAPCが始まらない、と私は思っているcharsbarさんの定番トークです。私は新機能が提案されるPPCsや前のバージョンとの差分を出しているperldeltaをちょいちょいウォッチしているんですが、海外の方でのPerl開発の微妙なニュアンスや進行中の状況などがキャッチアップできて、非常に実用的なトークです。

今回の話だと、やはりcpanmのhttps対応どうするかの話がありましたね。私自身もどうなるんだろうと注目していますが、どうなるんでしょうか(どうなるんでしょうか以上が何も言えない)。

PerlPerlによるPerlのための言語サーバーを作る

現代ではVSCodeをはじめ様々なエディタで使われるLanguage Server Protocolの概要について説明しつつ、PerlのLSPをPerlで実装する話でした。

こちらもeBPFの話と同様、技術的要素の入り口として捉えるのにぴったりな発表だったかと思います。LSPは言語関係の機能以外にもエディタに依存しないエディタの拡張を作るのに使える気がするので、何かしらやってみたい気持ちはあります。しかしやりたいことが多すぎるんだな。

perl for shell, awk, sed programmers

speakerdeck.com

私の発表です。現代においてPerlを使う場面としてsedawk, シェルスクリプトの代わりにPerlを使うのはどうかという提案の発表でした。

この発表をする背景について説明します。最近シェルスクリプトがWebプログラミング界隈で昔よりも多く見る気がしますが、それは2つの背景があるかと思います。

  • JVM言語やGo, Rust, TypeScriptなどコンパイルを要する言語によってWebサーバーを記述するようになったため
  • Webの世界でも分業が進み、SREやプラットフォームエンジニアリングの分野でアプリケーション記述言語に依存しないシェルスクリプトが採用されている

シェルスクリプトはどの環境でも入っており、またポータビリティがあるとされている理由から採用されていると私は考えていますが、これらのメリットは実は正しくないのではないか、Perlの方が適しているのではないかというのがこのトークの骨子です。

もちろんRubyPythonJavaScript等をアプリケーション記述言語として用いているのであれば、そちらを採用するのは私は良いかと思います。しかしコンパイル言語のようなそのような適当な手段がないケースでの選択肢にPerlはまず上がることがないと思いますが、選択肢に上げていいんじゃないかなと。シェルの文法とPerlの文法どちらが詳しいですか、どっちがあなたの慣れている言語に似ていますか、などなど言いたいことはたくさんあるのですが、とにかくPerlも忘れないで!っていう話でした。

これでPerlについて一人でも注目していただける方が増えると良いかと思います。この後の懇親会でも僕のトークを聞いてない方にさらりと概要を説明したら「確かに〜」というフィードバックをもらえたので、聞いてもらったら何割かの人はためになるとは思う話をできたかと思います。でもカンファレンスって時間が限られているから、自分が知っているか、隣接領域、もしくは仕事に役立ちそうな話、自分の周りの人が話題にしているような話に行っちゃいがちですよね? それは常識的な行動だと思うので、それに抗って「いかにPerlを書いていない人にPerlの話を聴きに来てもらえるか」っていうのがなんか今後の課題だなあと思っています。

言語ドキュメントを翻訳し、未来に向けて更新し続ける技術 ~perldocjpを例に~

perldoc.jpで公式Perlドキュメントを翻訳している白方さんのトークです。Perlっぽくpodで段落ごとに区切って翻訳して行くのはなるほどな〜と思いました。

ちょうど仕事でLLMとかいじってて段落で区切ってベクトル化するとかやってるんですが、これに向いているドキュメントとそうでないドキュメントがあるように感じます。多分公式ドキュメントなんかは向いているかと思います。コンテキストがちゃんと段落で区切られているとかそういう傾向があるとは思っています。そういった性質があるから段落ごとに翻訳でうまいこといけるんだなあとか、一人で考えていました。

令和最新版Perlコーディングガイド

speakerdeck.com

アナグラさんの発表でしたが、自分がこのテーマでも発表したかったとは思ってましたが、このトークでよかったなあと思いました。そもそも実際にはてな社でガンガン最新記法を採用していっているのが良いですね。Perlも部分的に新しいキーワードを有効にできるなどの機能があるので、そういうやり方に向いているとは思います。

今のPerlの開発はわりかりアグレッシブになっていると思っています。例えばtrue/falseやtry/catchがこのスピードでデフォルト有効になるとは思ってなかったです。そのスピードで発展していってる言語が、このカンファレンスに来るような人のほぼ全てのPCに最初から入っているってすごくないですか? 今、学ぶべき言語はPerlだと思うんですよね(ポジショントーク

ちなみにこの発表で挙げられなかった新機能で私が一番いいなと思うのは、iteration over multiple valueですね。

for my ($i, $j) (@array) {
    say join(" ", $i, $j);
}

こんな感じで一回のループで2個一気に取れます。それの何が嬉しいの?ってなるんですが、hashで回すときに便利で、

for my ($k, $v) (%hash) {
    say $k . '=>' . $v;
}

こんな感じでkey valueがペアで取れます。ハッシュはリストコンテキストで受けるとk, v, k, v ... のリストで返ってくるので「2個同時に取れる」がハッシュのリスト受けの性質と相まって「キーバリュー同時に取れる」に変化するんですよね。Perlっぽいなあと思います。

WebTransportは未来の技術?

speakerdeck.com

Perl関係じゃないですが、Web API大好きっ子でWebSocketに思い入れがある人間なので聴きに行きました。

やっぱりまだライブラリを含めて発展途上な感じはしますが、クラサバモデルでリアルタイムやる時にWebRTCよりは簡単に、WebSocketよりは早く、かつ切断に強くなるのは良いですよねえ。

CloudNative Meets WebAssembly: Wasm's Potential to Replace Containers

speakerdeck.com

WebAssemblyも気になるので聴きに行きました。そういえば去年から今年の序盤にかけてperlをWASM化するチャレンジをしていました。Emscriptenはいけるけれど、WASIはlongjumpの壁があり、WASIXはコンパイルできるがwasmerで動かないみたいな感じでした。

これを聞くとWebAssemblyで動くかどうかは結構今後の鍵になるかなと思います。質問にあったようにDockerからWebAssemblyにするにはサンドボックスとしては確かに一緒かもしれないが、既存コードのリフトアップには大きな飛躍が必要かと思います。

一方スクリプト言語はとにかくランタイムがWebAssembly化されていれば、コンパイル言語に比べたらワンチャンある気がします。perlのWebAssembly化また頑張るかなあ。ruby.wasmのAsyncifyを参考にすればいけるとは思っています。GCとかもRubyに比べたらシンプルですしね。

Lightning Talks

いやーこれぞLTっていうのがどんどん出てきてよかったですね。勢いって共通項はあるとは思うんですが、それぞれスタイルやテーマが違いつつも、オッと食いつくような奴が見れてよかったです。

moznionさんのPerlRubyと聞いてautoboxのこと思い出したんですけれど、メソッド呼び出しがアローとドットで違うので、あの問題には使えないか〜。

キーノート

speakerdeck.com

本人からLTとキーノートでテンションの差がって言ってたんですけれど、どっちかというとLTの方のテンションによっていたかと思いますが、これはこれで楽しいキーノートでよかったと思います。というかコロナ後のYAPCってコロナ前のしっとりって感じよりは結構淡々時々笑いありっていうスタイルになってませんか。大西さん、とほほさんどちらも笑いがあったかと思います。今後のキーノートはこんな感じになるのかもしれない。

moznionさんとは直接一緒に働いたことはないものの、比較的近くから見ることが多く、しかしスライドでそんな感じだったか〜というのはあります。小切手のくだりは、なんかグッとくるものがあります。こうやって他人の人生を俯瞰して見る、さらに身近な人物というのはなかなか、僕にとっても今までにないキーノートでした。

クロージング

次ある!!!よかった!!!! でもなんか大変そうなのでなんか手伝いたいな。しかしトークもしたいな??? どうすればいいんだーーー〜〜!!!! ところで福岡は好きな街です。今年もなんだかんだ博多に行ったんですわ。

というわけでまた次も会いましょう〜〜! 次のYAPCでもPerlに絡んだ話できるように目指します!

ところでこれは翌日に行ったラッキーピエロで食べたmoznion氏おすすめのチャイニーズチキン2段のり弁当です。ドカ食いセットなのは見逃してください。バーガーを結局食べれてないんですけれど、食べている時に「確かにチャイニーズチキン美味しいんだけれど、最初これでいいんだっけ?」となったのは秘密です。

tanukirpcというWebフレームワークを作っています

最近の盆栽ですけれど、tanukirpcというGoのWebフレームワークを書いています。ある程度やりたいことができはじめてきたので、どんなフレームワークかを紹介します。

github.com

TL;DR

  • Webアプリケーションでよくやるようなことを、最短手順で自然に書けるように設計したフレームワーク
    • リクエストをパースして構造体にマッピングする
    • リクエストの内容をバリデーションする
    • レスポンスの構造体をエンコードしてレスポンスとして書き込む
    • グローバルスコープもしくはリクエストスコープでの構造体のコントローラーへの依存性注入
      • DBコネクションやAPIクライアントの保持などに使う
  • 現在の責務範囲はWebアプリケーションのコントローラーだが、Webアプリを作る時によくやるようなことはできるだけやれるようにしてく
    • tanukiup 開発サーバー起動用コマンド。ファイル更新を監視してビルドおよびサーバープロセスの再起動を行う
    • gentypescript クライアントコードの生成コマンド。GoでもtRPCのような開発体験を得るのを目指している

ではそれぞれどういうことなのか見ていきましょう。

routerの作成

tanukirpc*tanukirpc.Routerを作成し、このrouterのGetPostなどのメソッドを使ってAPIエンドポイントを設定していくことでコントローラー全体を定義していきます。なのでまずは、routerを作るところから始めます。

package main

import (
    "github.com/mackee/tanukirpc"
)

func main() {
    router := tanukirpc.NewRouter(struct{}{})
    // do something
}

空struct struct{}{} を渡しているのが気になりますが、こちらは後述するRegistryの項目でお話しします。

APIエンドポイントの定義

まずは基本的なエンドポイントを定義していきます。次のコードではシンプルな GET /hello エンドポイントを定義しています。

package main

import (
    "fmt"
    "net/http"

    "github.com/mackee/tanukirpc"
)

func main() {
    router := tanukirpc.NewRouter(struct{}{})

    type helloRequest struct {
        Name string `query:"name"`
    }
    type helloResponse struct {
        Message string `json:"name"`
    }

    router.Get("/hello", tanukirpc.NewHandler(
        func(ctx tanukirpc.Context[struct{}], req helloRequest) (*helloResponse, error) {
            return &helloResponse{Message: fmt.Sprintf("hello, %s", req.Name)}, nil
        },
    ))

    http.ListenAndServe("127.0.0.1:8080", router)
}

これで完成しました。起動してみます。

$ go run main.go

ログを出していないので何も出力されませんが、HTTPサーバーとして起動しています。別のターミナルを立ち上げて、curlを用いてリクエストを行ってみます。

$ curl 'http://localhost:8080/hello?name=tanukirpc'
{"name":"hello, tanukirpc"}

JSONでレスポンスが帰ってきました。この通り、query parameterのnameとして渡したtanukirpchelloRequest.Nameにバインドされてハンドラー内で利用できます。

また、ハンドラーで返した*helloResponseJSONとしてクライアントに返却されます。

便利な開発用コマンド tanukiup

先ほどはgo run main.goで起動しましたが、ファイルを変更するたびにCtrl+cで終了させて立ち上げ直さなければなりません。そこでtanukirpcでは開発用の便利なコマンドとしてtanukiupを用意しています。

tanukiupは以下のように使います。

$ go get github.com/mackee/tanukirpc/cmd/tanukiup
$ go run github.com/mackee/tanukirpc/cmd/tanukiup -dir ./

-dir ././ 以下を監視対象に入れます。デフォルトでは監視対象のディレクトリのうち、.goで終わるファイルに更新があった場合にビルドとコマンドの再起動が行われます。

こういったWebアプリケーションサーバー開発でよくやるようなことのツール化もtanukirpcの守備範囲です。

リクエストのパース

tanukirpcは以下のリクエスト形式のstructへのバインドに対応しています。

  • クエリパラメーター
    • 例: /hello?name=tanukirpc
  • パスパラメーター
    • 例: /hello/{name}{name} に任意の文字列が入る形式
  • Form
    • 現在はapplication/x-www-form-urlencoded のみ
  • JSON

上記の例ではクエリパラメーターのやり方について解説しましたが、他の形式のケースを紹介します。

パスパラメーター

routerに渡すパスを/hello/{name}とし、request structのfieldのタグをurlparam:"name"とすると、{name}に入る任意の文字列をstructにバインドします。

type helloRequest struct {
    Name string `urlparam:"name"`
}
type helloResponse struct {
    Message string `json:"name"`
}

router.Get("/hello/{name}", tanukirpc.NewHandler(
    func(ctx tanukirpc.Context[struct{}], req helloRequest) (*helloResponse, error) {
        return &helloResponse{Message: fmt.Sprintf("hello, %s", req.Name)}, nil
    },
))
$ curl 'http://localhost:8080/hello/tanukirpc'
{"name":"hello, tanukirpc"}

POST Form

structのfieldのタグにform:"name"のように入れると、リクエストボディに入れられたapplication/x-www-form-urlencoded形式のリクエストの内容をバインドします。リクエストのContent-Typeヘッダーがapplication/x-www-form-urlencodedである必要があります。

type helloRequest struct {
    Name string `form:"name"`
}
type helloResponse struct {
    Message string `json:"name"`
}

router.Post("/hello", tanukirpc.NewHandler(
    func(ctx tanukirpc.Context[struct{}], req helloRequest) (*helloResponse, error) {
        return &helloResponse{Message: fmt.Sprintf("hello, %s", req.Name)}, nil
    },
))
$ curl -XPOST -d 'name=tanukirpc' -H 'Content-Type: application/x-www-form-urlencoded' http://localhost:8080/hello
{"name":"hello, tanukirpc"}

JSON

structのタグにjson:"name"のようにすると、リクエストボディに入れられたJSON形式をパースし、フィールドにバインドします。リクエストのContent-Typeヘッダーがapplication/jsonである必要があります。

type helloRequest struct {
    Name string `json:"name"`
}
type helloResponse struct {
    Message string `json:"name"`
}

router.Post("/hello", tanukirpc.NewHandler(
    func(ctx tanukirpc.Context[struct{}], req helloRequest) (*helloResponse, error) {
        return &helloResponse{Message: fmt.Sprintf("hello, %s", req.Name)}, nil
    },
))
$ curl -XPOST -d '{"name":"tanukirpc"}' -H 'Content-Type: application/json' http://localhost:8080/hello
{"name":"hello, tanukirpc"}

上記のタグは複数の種別を指定可能であり、例えばform:"name" json:"name"のように指定すれば、FormとJSON形式の両方を対応するようなエンドポイントを作成できます。

また、上記以外の形式もCodecというインターフェイスを実装した上で、router作成時にオプションとして渡すことで対応可能になっています。

リクエストバリデーション

tanukirpcではgithub.com/go-playground/validatorによるバリデーションが最初から組み込まれています。go-playground/validatorによるリクエストバリデーションを有効にするには、リクエストstructにvalidateタグを追加します。

以下はクエリパラメーターのnameが必須であることを表すハンドラーです。

type helloRequest struct {
    Name string `query:"name" validate:"required"`
}
type helloResponse struct {
    Message string `json:"name"`
}

router.Get("/hello", tanukirpc.NewHandler(
    func(ctx tanukirpc.Context[struct{}], req helloRequest) (*helloResponse, error) {
        return &helloResponse{Message: fmt.Sprintf("hello, %s", req.Name)}, nil
    },
))
$ curl http://localhost:8080/hello
{"error":{"message":"Key: 'helloRequest.Name' Error:Field validation for 'Name' failed on the 'required' tag"}}
$ curl 'http://localhost:8080/hello?name=tanukirpc'
{"name":"hello, tanukirpc"}

なお、このエラー出力に関しては、tanukirpc.ErrorHooker interfaceを実装し、routerの作成時に渡すことで、カスタマイズが可能です。

レスポンス

レスポンス形式は現在はJSON形式のみ対応しています。Codec interfaceを実装し、router作成時に渡すことで、その他の形式もサポート可能です。

Registry

Registryはこれまで紹介した型付きハンドラーと並ぶ、tanukirpcのユニークな機能です。

上記の例ではrouterの作成時に、空structを渡していますが、ここには任意の値を渡すことができます。ここで渡した値はハンドラー内で利用可能です。

一例として、DBコネクションを組み込んだstructを渡したアプリケーションを示します。以下のアプリケーションに対してPOST /usersにリクエストすると、Registryに組み込まれたdbコネクションを用いて、ユーザーを作成します。

package main

import (
    "database/sql"
    "fmt"
    "log"
    "net/http"

    "github.com/mackee/tanukirpc"
    _ "github.com/mattn/go-sqlite3"
)

type registry struct {
    db *sql.DB
}

func main() {
    db, err := sql.Open("sqlite3", "./users.db")
    if err != nil {
        log.Fatalf("failed to open database: %v", err)
    }

    reg := &registry{db: db}
    router := tanukirpc.NewRouter(reg)

    router.Post("/users", tanukirpc.NewHandler(postUsersHandler))

    http.ListenAndServe("127.0.0.1:8080", router)
}

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

type usersRequest struct {
    Name string `form:"name" validate:"required"`
}
type usersResponse struct {
    User User `json:"user"`
}

func postUsersHandler(ctx tanukirpc.Context[*registry], req usersRequest) (*usersResponse, error) {
    reg := ctx.Registry()
    row := reg.db.QueryRowContext(ctx, "INSERT INTO users (name) VALUES (?) RETURNING id", req.Name)
    var id int
    if err := row.Scan(&id); err != nil {
        return nil, fmt.Errorf("failed to insert user: %w", err)
    }

    return &usersResponse{User: User{ID: id, Name: req.Name}}, nil
}
$ curl -XPOST -d "name=tanukirpc" http://localhost:8080/users
{"user":{"id":1,"name":"tanukirpc"}}

この例ではグローバル変数とあまり変わりませんが、tanukirpc.WithContextFactoryを用いてrouterにオプションを渡すと、リクエストのたびにRegistryを生成することも可能です。DBの都度接続が必要なケースでは有用です。

Registry transformer

tanukirpcではエンドポイントをネストして定義することにより、上流から渡されたRegistryとは別の型のRegistryに変換ができます。この機能がどういう場合に有用なのか例を交えて紹介します。

上記のPOST /usersの例では、作成を行いましたが、次は参照と更新のエンドポイントを作成してみます。以下の例ではtanukirpc.RouteWithTransformerを用いて/users/{id}以下のパスを定義し、またこのパスに対応するuserテーブルの行をRegistryに組み込んでハンドラーで利用できるようにします。

type registryWithUser struct {
    *registry
    user *User
}

var usersTransformer = tanukirpc.NewTransformer(
    func(ctx tanukirpc.Context[*registry]) (*registryWithUser, error) {
        reg := ctx.Registry()
        id := chi.URLParam(ctx.Request(), "id")
        row := reg.db.QueryRowContext(ctx, "SELECT id, name FROM users WHERE id = ?", id)
        var user User
        if err := row.Scan(&user.ID, &user.Name); err != nil {
            if errors.Is(err, sql.ErrNoRows) {
                return nil, tanukirpc.WrapErrorWithStatus(http.StatusNotFound, errors.New("user not found"))
            }
            return nil, fmt.Errorf("failed to get user: %w", err)
        }
        return &registryWithUser{registry: reg, user: &user}, nil
    },
)

func getUsersHandler(ctx tanukirpc.Context[*registryWithUser], _ struct{}) (*usersResponse, error) {
    return &usersResponse{User: *ctx.Registry().user}, nil
}

func putUsersHandler(ctx tanukirpc.Context[*registryWithUser], req usersRequest) (*usersResponse, error) {
    reg := ctx.Registry()
    if _, err := reg.db.ExecContext(ctx, "UPDATE users SET name = ? WHERE id = ?", req.Name, reg.user.ID); err != nil {
        return nil, fmt.Errorf("failed to update user: %w", err)
    }

    return &usersResponse{User: User{ID: reg.user.ID, Name: req.Name}}, nil
}

func main() {
    // 省略
    tanukirpc.RouteWithTransformer(router, usersTransformer, "/users/{id}", func(r *tanukirpc.Router[*registryWithUser]) {
        r.Get("/", tanukirpc.NewHandler(getUsersHandler))
        r.Put("/", tanukirpc.NewHandler(putUsersHandler))
    })
    // 省略
}

usersTransformer内でパスに対応したuserを問い合わせ、ない場合は404を返しています。これにより、ハンドラー内ではuserの存在が前提のコードを書けます。以下は上記コードを書いたアプリケーションの挙動を示したものです。

$ curl http://localhost:8080/users/1
{"user":{"id":1,"name":"tanukirpc"}}
$ curl http://localhost:8080/users/2
{"error":{"message":"user not found"}}
$ curl -XPUT -d "name=go-chi" http://localhost:8080/users/1
{"user":{"id":1,"name":"go-chi"}}
$ curl http://localhost:8080/users/1
{"user":{"id":1,"name":"go-chi"}}

TypeScriptクライアント生成

tanukirpcを作る際に、最初のマイルストーンとして設定した機能です。tanukirpcを作成した動機として、もし私が一人でフロントエンドからバックエンドまで全てこなすとしたら、どんなフレームワークが効率的か、それが世の中にない場合にどういったものを作れば良いかを考えた、というのが始まりです。一人で作成する場合に、gRPCやGraphQL、OpenAPIといったスキーマ駆動の開発はむしろ足枷となります。TypeScriptでサーバーアプリケーションを作成する際の、tRPCのような開発体験を得る手段として、TypeScriptのコード生成をしてみてはどうかと考えました。

クライアントを生成する準備として、以下のgo:generate行を足します。

//go:generate github.com/mackee/tanukirpc/cmd/gentypescript -out ./frontend/src/client.ts ./

そして、生成対象のパスを含んだroutergenclient.AnalyzeTargetに渡します。

まとめると以下のようになります。

//go:generate go run github.com/mackee/tanukirpc/cmd/gentypescript -out ./frontend/src/client.ts ./

func main() {
    db, err := sql.Open("sqlite3", "./users.db")
    if err != nil {
        log.Fatalf("failed to open database: %v", err)
    }

    reg := &registry{db: db}
    router := tanukirpc.NewRouter(reg)

    router.Post("/users", tanukirpc.NewHandler(postUsersHandler))
    tanukirpc.RouteWithTransformer(router, usersTransformer, "/users/{id}", func(r *tanukirpc.Router[*registryWithUser]) {
        r.Get("/", tanukirpc.NewHandler(getUsersHandler))
        r.Put("/", tanukirpc.NewHandler(putUsersHandler))
    })

    genclient.AnalyzeTarget(router)

    http.ListenAndServe("127.0.0.1:8080", router)
}

go generate ./で生成しても良いのですが、上記で説明したtanukiupコマンドはgentypescriptコマンドにも対応しています。tanukiupコマンド起動時にファイル内にgentypescriptが含まれているかをみているので、tanukiupコマンドを再起動します。すると、指定したパスにclient.tsが出力されます。

その様子が以下です。

https://i.imgur.com/78CWrhW.gif

また、この生成したクライアントは、パスに対応したリクエストの型がマッピングされています。使用中の様子を以下に示します。

https://i.imgur.com/1rAJmcJ.gif

まとめ

tanukirpcはまだできたばかりでよちよち歩きのフレームワークですが、私個人が使う分には「これから使っていこう」という気になるような機能がすでに揃っています。私自身はこれから仕事も含めて責任を取れる範囲では使ってみて、足りない機能を見つけては足していこうと思います。

もしこれをご覧の中に、こういったフレームワークが欲しかったけれどあれが足りない、これが足りない、もしくはこういうケースはどう書けばいいかという方がいれば、GitHubのissueなどで言っていただければ何らかの反応はさせていただこうかと思います。フレームワークは実戦で鍛えてみてなんぼかと思います。あなたの実戦に組み込めるような提案ができるとベリベリハッピーです。

また、このフレームワークに関する話をAsakusa.go #3でさせていただきます。今のところ作った動機について話そうかと思いますが、それだとテクっぽくないので、今考えている機能や組み込もうとしている自作ライブラリについての話をするかもしれません。その場合は動機の話は懇親会でしようかなと思います。

そんな感じです。それでは良いDB読み書きJSON返しライフを。

digが入っていない環境で名前解決のテストをするのにperlが便利

これで名前解決できる

$ perl -E 'say join(".", unpack("W4", gethostbyname("example.com")))'

用途

  • EC2インスタンスやらコンテナから外にほんまに出られるんかな?みたいに調べたいことがある
    • VPC内でprivate subnetだとNAT Gatewayとかがないと外に通信できない
  • サービスメッシュとかで名前引けるかどうかを確認したい時がある
    • 特定の環境でしか引けない名前とかある
  • 今回の用途はGuardDuty ECS Runtime Monitoringを導入した際にちゃんと不審な動きが検知されるかどうかで使いたかった
  • digやらnslookupをaptやyumで入れるのは気が引ける or できない
    • 本番環境のコンテナイメージなのでroot取れないとか
  • perlは入っていることがそこそこある
    • debian:bookworm-slimなど
  • アドレス一つしか返ってこないとかそういうデメリットはありますが、使い所はありそう

perlシェルスクリプトの代わりに使うのはどうかという話を3/15か3/16にします

github.com

hachiojipm.connpass.com

2023年の振り返りと2024年の抱負

2023年もいろんなことがありましたね。2024年を迎えて皆様はいかがお過ごしでしょうか。というわけで去年の振り返りと今年の抱負について書いていきたいと思います。

前回

mackee.hatenablog.com

登壇やイベントなど

YAPC::Kyoto 2023

YAPC::Japanが久しぶりにオフラインで開催されて、京都に行ったり、それから登壇もしました。

mackee.hatenablog.com

そんでもって、このトークの内容はlogmiさんで書き起こししていただいているのがあります。

logmi.jp

書き起こしされると、動画やスライドと違い、検索で辿り着きやすくなるので良いですね。また、このトークは個人的には今まで自分がやった中で一番よかったなあというのがあります。今回はライブで解説しながらデプロイしていくというのをやったのですが、こういうライブ感のあるトークはオフラインじゃないと難しいじゃないかな〜と思うし、オフラインだからこういうのをやりたいなあと思います。

と、というわけで、2月にも広島でライブコーディング主体のトークやります。YAPC::Hiroshima 2024です。

yapcjapan.org

今回は、WebAuthnを認証として使うPerlアプリケーションをできるだけ専用のモジュールを使わずに書きます。パスワードレス認証の一つとしての、WebAuthnの仕組みなどもわかるトークにするのでチケット買って見に来てくれ!!!

fortee.jp

チケットはこちら

passmarket.yahoo.co.jp

湘南.pm #1

湘南在住(諸説あり)なので、湘南.pmに参加して喋ってきました。

speakerdeck.com

ずっとsqllaというORMを作っているのですが、ORMの課題であるJOINを含んだ複雑なクエリをビューを使って解決できないかという提案です。sqlla側にいろいろ実装はしております。

湘南.pmは今年も色々イベントをやるそうなので、どんどん参加していきたいですね。

Asakusa.go #1

浅草だいぶ遠いですけれど、Asakusa.goに参加して、LTもしてきました。

speakerdeck.com

趣味ではCloudflare Workersを使っていますが、こちらでもGoを使いたいので、syumai/workersというライブラリを使っています。CF WorkersにデプロイするにはGoのプログラムをWASMにビルドしないといけないのですが、オフィシャルGoコンパイラのWASMはでかいので、TinyGoという別のコンパイラを使っています。ただ、色々動かないのでその時の工夫などについて書いています。

CF WorkersとかWASM関係は今年も攻めていきたいですね〜。

仕事

相変わらずTonamelですが、テックリードは交代して、現在はアーキテクトと名乗っています。

Tonamel関連で書いた記事はこちら。

techblog.kayac.com

他も相変わらずトーナメント表のチューニングとかやっています。

趣味

ISUCON

負けた。来年は勝つ。

mackee.hatenablog.com

3Dプリンタ

色々やってた。デルタ型のエフェクターを自分で設計したやつに入れ替えたりしていた。あとフィラメントをPETG-CFにしてみたり。キーボードのケースなど実用品を作るようになってきた気がします。

旅行 前半

キャンプ。ラウンドグリドルパンで芽キャベツのアヒージョ。2月だったので伊豆の南の方のキャンプ場でした。

箱根の彫刻の森美術館。2022年に直島の地中美術館に行ってから彫刻とかハマってる気がする。

ピカソ館。漫画みたいにでかいPICASO

ステンドグラスの塔。「幸せをよぶシンフォニー彫刻」。映えるな〜。

山中湖近くの忍野八海。富士山が見えると間違いない感じある。

YAPC::Kyoto 2023の前日に京都鉄道博物館行ってました。SLアベンジャーズ感。

またキャンプ行ってた。今度は森の中です。確か設営中雨降ってたな。

西九州新幹線乗りにいってた。軍艦島に上陸するツアーにも行ってたんですが、海が荒れて上陸できず。再チャレンジしたい。

旅行 アメリカ横断編

5月に3週間ほどアメリカに行ってました。目的は車で西海岸から東海岸まで横断することと、インディ500を見ることでした。

ロサンゼルスから出発しましたが、ロサンゼルスにはSpaceXの工場があり、ファルコン9の1段目がありました。

モハヴェ砂漠あたりですが、めちゃくちゃまっすぐだし、スケール感じる。

ラスベガス。派手なアメリカが詰まっている感。

グランドキャニオン。マジで観光している。

アリゾナのメテオクレーター。こんなん残ってるのって感じ。

テキサスのバーベーキュー屋さん。今にも吹き飛びそうな小屋の外観で大丈夫か?と思ったが、めちゃくちゃうまい。他の二人が食ってた塊肉食えばよかったなと思うものの、このコンビーフとかもうまかった

ナッシュビル。ロックの街。ここよかった。

旅の目的のインディ500。熱気がすごい。ドラマチックな展開。優勝した人のファンが、目の前にいて、勝った時に気絶しかけていた。だいぶすごい光景だった。

シカゴはめちゃくちゃ大都会。摩天楼。

ワシントンDCのスミソニアン博物館。いや〜、とにかくよかった。そういえばインディアナポリス近郊の空軍博物館にも行ってます。SR-71見れたのよかった。

そしてたどり着く、目的地のニューヨーク。本場のブルーノートよかったっすねえ。この前に食ったステーキも美味かったし、その前に食ったニューヨークスタイルのピザも美味かったし、とにかくこの旅では美味いものをいっぱい食った感じ。

でした!

旅行 後半

サンライズ出雲出雲市駅まで行って、出雲大社行って帰るっていう予定だったんですけれど、大雨で松江で打ち切りを喰らって、スーパーまつかぜ鳥取スーパーはくとで姫路まで行き、そこから新幹線で帰りました。帰りはやくもの予定だったのですが、次乗る時には車両新しくなってそう。

鉄道と言えば、秩父鉄道パレオエクスプレスにも乗ってました。何気にSL初乗車だった。

富士山近くで夏キャンプ。木に囲まれていて、涼しかったのが幸い。

白馬八方池。車で行ったが遠い。旅館のご飯美味かったのでまた行きたい。今度は電車で。

尾瀬も行きました。尾瀬沼の方を一周した。いやー〜いい体験。

秦野の大山登ってたが、雨で何も見えなかった。帰りに食べたラーメンが美味かった。

ふもとっぱらでキャンプ。毎年行ってる気がする。

奥鬼怒温泉に途中から紅葉狩り目的で徒歩で行った。目的地の宿は自家用車では行けず宿のバスを使う必要がある。前回行った際は宿のバスを使ったが、今回はその部分を歩いた。だいぶよかったですね〜。

カーフェリーで横須賀から北九州まで。山口の実家に自分の車を見せに行くという旅行だったが、フェリーはだいぶ快適でした。

コンテンツ系

  • AC6出て俺たちのやつが来た!ってなったが、途中で止まってしまっている
  • 映画は色々みた気がする。なんだかんだ「君たちはどう生きるか」が一番残っている感じする
  • 怪奇!YesどんぐりRPGにハマってしまっている気がする

2024年の抱負

  • 仕事は求められていることはやりつつ、自分で仕事を作るみたいなのに挑戦したい
  • YAPCベストを尽くす
  • WebAssemblyとかCloudflare Workers関連の興味があるもの何か外に出したいな

ISUCON13に出場していました #isucon

こんにちは。「失敗から学ぶISUCONの正しい歩き方 - 葬送のPostgreSQL」チームとして、id:soudai@tetsuzawaと共に出ていました。言語はGoです。

とはいえ芳しくない結果に終わりました。その辺は id:soudai のブログにあります。

soudai.hatenablog.com

我々のアプローチ

過去にfujiwara組として優勝した時などは特に素振りなどはしていなかったわけですが、これはチームの3人とも普段から一緒に仕事しているから、お互いの動き方というのがわかっているというのがありました。ただ、今回組んだメンバーたちとは一緒に仕事をしていないので、月並みな言い方をすると仲良くなるところからのスタートでした。

というわけで、この1年間は大体毎月素振りをしていました。

結果的に我々の戦略というのは以下の3つに絞られます。

  1. どういう問題であってもPostgreSQLに移行する
  2. 事前準備で計測ツールやデプロイ方法を明確にする
  3. あとはその場で解く

1のPostgreSQL移行する、というのは id:soudai さんが得意だから、このチームの最大風速を目指すのであれば選びたい戦略という理由だと僕は考えています。

3回目とか4回目ぐらいまでの昔のISUCONというのは、俺の考えたロマン構成や、ISUCON決戦兵器をぶっ放す人も多かったように思えます。ユーザーランドで処理を行わずにカーネル上でアプリケーションを書いてどうにかするチームや、nginx module上のPerlを使う人など、尖ったアーキテクチャを採用するチームもありました。あとテストカバレッジを100%にするのを目指すチームもいましたね。

今回の我々のチームの戦略であるPostgreSQLに移行する、というのはロマンの塊ではあります。しかし、過去のロマン戦略を取ったチームと同じく、勝つことを目的としています。過去の素振りではPostgreSQLに移行した結果、同時にインデックスチューニングが既になされて高速化するということもありましたし、JSON型や配列型などPostgreSQLならではの手法も使えます。

自分たちの土俵に上げるために、この1年間はひたすらPostgreSQLに移行する訓練をしてきたと言えます。その結果、今回のISUCON13では開始から2時間後の12時台にはもうPostgreSQLに移行が完了していました。

2の事前準備に関しては@tetsuzawaが初期セットアップの自動化や手順書を色々書いてくれたのと、僕の方でVPS上にGrafanaとPrometheus、Tempo、Pyroscopeを用意して、アプリケーションから飛ばすようにしていました。tetsuzawaが用意してくれていたalpの結果を表示する君と合わせて、だいぶ解像度高くアプリケーションの実態を測ることができるようになっていました。また、この辺まで含めて12時台に入っているので、最初はまあまあ合格点かなと思っています。

なぜスコアが出なかったのか

なぜスコアが出なかったかといえば、3の当日やる部分が全くうまくいかなかったという点があります。僕がやった部分と言えば、

  • PowerDNSをMySQL backendからLMDB backendに切り替える => 名前解決数は増えたもののCPUはそこまで下がらず
  • user statisticsをフルRedis化 => 失敗
  • user statisticsをsingleflightによるオンメモリキャッシュ化 => スコア上がらず

結果的に言えば、これらは手をつける順番も間違っていたし、手法もやりきれてないということで、全然ダメダメだったなあという感じです。PowerDNSに関してはiptablesによるdropをやるべきでしたが、手法を知っていたのにも関わらずキャッシュに手をつけてしまって結果的に中途半端になってしまいました。フルRedis化も本来得意なところだったはずなのにバグらせて結局時間を溶かしただけなので、実力不足と言えます。また、その手前に他にやることがいろいろあったと思うので、この辺りは素振りで確認していきたいと思います。

振り返り

良かったところ

  • 過去の参戦ではできなかった計測について取り組むことができた
  • OpenTelemetry TraceやPyroscopeなど現代的な武器を活用できた
  • 全く知らないPowerDNSの設定変更について立ち向かってその場で対処できた

悪かったところ

  • 肝心の実装で精度を出せなかった
  • ベンチマーカーが行うシナリオの観測ができていなかった
  • シナリオの観測ができなかった結果、手をつける順番を間違えた

次へのトライ

  • 実装に集中してみる
    • いい結果が出ている時は実装に専念しているので、観測はそこそこに実装をやるのが一番バリューが出る
  • シナリオをちゃんと追いかける
    • OpenTelemetry Traceで追いかけることができないかやってみる
      • アプリケーションのコミットごとにトレースを分けることができていたのでもうちょっとこの辺サマれるようにして実用的にしたい
  • 仲間を信じる

おまけ: PostgreSQL移行 アプリ視点

ISUCONの参考実装はたいていMySQLなので、MySQLからPostgreSQLに移行することを考える。Goの場合、MySQLではgo-sql-driver/mysqlが使われるが、PostgreSQLのドライバーはいくつか選択肢がある。弊チームではjackc/pgx/stdlibを採用する。

MySQLPostgreSQLとではSQLにおいて主に以下の違いがある(これはライブラリの違いも含まれる)

  • プレースホルダ
  • カラム名を囲むクオート文字
  • AUTO INCREMENTのテーブルに対して挿入した行のIDを取得する方法
    • MySQLでは LastInsertID
    • PostgreSQLではINSERTの末尾に RETURNING id を付ける
  • Upsertのやり方の違い
    • MySQLでは INSERT ~ ON DUPLICATE KEY UPDATE ...
    • PostgreSQLでは INSERT ~ ON CONFLICT DO UPDATE ...

他にも型の厳密さが違ったり、標準でのトランザクション分離レベルが違ったり、いろいろあるが、頻出パターンは以上である。

前2項目についてはクエリを実行時に動的に書き換える以下のライブラリを制作し、実戦投入していた。

github.com

後者の2つに関しては、手で書き換えるようにしていた。今回のISUCON13ではLastInsertIDが5箇所ぐらいあったように思える。

github.com

また、行の衝突のエラーハンドリングも頻出パターンではある。MySQLのエラー番号とPostgreSQLのエラー番号は当然違うので対処が必要。これはあらかじめスニペットを用意していた。今回は出番はなかった。

DDLの変換や初期化スクリプト、データ移行に関しては id:soudai さんに完全にお任せしていた。DDLの変換はChatGPTに投げたやつを手直しして使っていたらしい。時代ですね。

というわけで次も(次があれば)PostgreSQLでやって、いいところまで行きたいですね。できればデータモデリングからやり直したり、ビューをうまいこと使うとかそういう感じでやりたい。なお仕事は完全にMySQL使っています。

また来月振り返りがあるので、反省していきます。ではまた。

Goで複数の引数を取る関数やメソッドをどう書くのがいいのか

普段Go書いているときにそこまで気にしてなかったが、ふと気になったので色々パターンを挙げてみる。なおこの記事には「答え」が書かれてないので、みなさんの意見を聞かせてください。

複数の引数を取るパターン一覧

  • そのまま引数を羅列する
  • 複数の引数をまとめたstructを取る
  • Functional Options Pattern

そのまま引数を羅列する

例えばHTTPリクエストを行うような関数があったとして、

func Request(ctx context.Context, method http.Method, _url string, query url.Values, formValues url.Values) error {
    // do something
}

というシグネチャが考えられる。

実際にnet/http.NewRequsetWithContextfunc NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error)というシグネチャになっている。

このやり方のメリットとしては、引数の記述を呼び出し側に強制できることである。例えば、methodという引数は必ず呼び出す時に書かないといけないため、http.MethodPostなのか、http.MethodGetなのかを呼び出し側が指定することを強制できる。これにより呼び出し側コードに現れない暗黙の挙動の出現を抑えられる。記述は冗長になるが、暗黙の挙動が少なければ、コードレビューのレビュワーからすると嬉しい。

一方で、引数は記述順が固定であり、それぞれ記述が必須になるため、呼び出す側は関数の定義を覚えておかなければならない。ただ、現代はgoplsによる関数シグネチャの補完が効くので、コードを書く側の環境さえ整っていれば、この心配もいらないと思われる。

複数の引数をまとめたstructを作る

先ほどと同じような関数を以下のように変形する。

type RequestInput struct {
    Method     http.Method
    URL        string
    Query      url.Values
    FormValues url.Values
}

func Request(ctx context.Context, input RequestInput) error {
    // Do Something
}

実際に、net/http.Client.Doというメソッドは、このようなパターンのメソッドであると言える。

func (c *Client) Do(req *Request) (*Response, error)

このパターンのメリットは、呼び出し側で引数を列挙する順番が固定でないことである。また、RequestInput自体が呼び出し側で入れ物として使えるので、以下のように、適宜判断しながら引数を詰めていくこともできるだろう。

var input RequestInput
if isPost {
    input.Method = http.MethodPost
}
input.URL = something.URL()

if err := Request(ctx, input); err != nil {
    return fmt.Errorf("error Request: %w", err)
}

また、RequestInput自体にメソッドを生やすことも可能である。例えば引数にはURLを取るが中身はそのbasenameしか利用しない場合は、RequestInput自体にメソッドを生やすとRequest関数の中身の量を抑えられる。

func (r RequestInput) urlBasename() (string, error) {
    u, err := url.Parse(r.URL)
    if err != nil {
        return "", fmt.Errorf("error url.Parse: %w", err)
    }
    return path.Base(u.Path), nil
}

func Request(ctx context.Context, input RequestInput) error {
    basename, err := input.urlBasename()
    if err != nil {
        return fmt.Errorf("error RequestInput.urlBasename: %w", err)
    }
    // Do something
}

RequestInputのような引数のためのstructは他の関数やメソッドにも使いまわせる。個人的には、使いまわさない方が良い気がしているが、この言語化はうまくできていない。

また、Goにおいて、短い識別子は有限な資源である。「そのまま引数を羅列する」で述べたときの関数シグネチャ内のURLを取る仮引数の名前はあえて、_urlとしている。これは、packageであるnet/urlとの重複を避けるためである。実用的には一文字でuとしてしまうことが多いが、型がstringであるゆえ、uに何を入れればいいかをコメント等で指示しなければならない。urlという名前が使えれば、そこまでの配慮はいらないはずである。 一方、structを使う場合は、関数内ではinput.URLという名前を使えるため、このケースではフィールド名でわかるため、何を 入るべきかをあまり記述しなくて良いと思われる。また、structであれば、フィールド自体にコメントを詳細に書くことも可能である。

デメリットとしては、呼び出し側にフィールドに値を明示的に入れることを強制できない。つまり、

Request(ctx, RequestInput{})

という呼び出しもコンパイルは通ってしまう。この場合、各フィールドにはゼロ値が入ってしまう。また、Goにおいて、ゼロ値をあえて指定した場合と、何も指定しなかった場合の区別は関数側ではできない。このケースではあまり考えられないだろうが、あえてURLに空文字を指定した場合と、何も入れなかった場合(もしくは記述し忘れた場合)の区別はできない。

Functional Options Pattern

Functional Options Patternは必須ではない引数を指定する際に用いられるが、指定した場合と指定しなかった場合の区別にも使える。

type requestInput struct {
    hasMethod bool
    method     http.Method
    hasURL bool
    url        string
    hasQuery bool
    query      url.Values
    hasFormValues bool
    formValues url.Values
}

type RequestOption interface {
    Apply(*requestInput)
}

type WithURL string

func (w WithURL) Apply(input *requestInput) {
    input.url = string(w)
    input.hasURL = true
}

func Request(ctx context.Context, options ...RequestOption) error {
    input := &requestInput{}
    for _, o : = range options {
        o.Apply(o)
    }
    // Do something
}

こうすれば、URLが指定されたかどうかをhasURLフィールドで区別できる。ただ、この区別は実行時の話であり、指定しなかったらコンパイルに失敗するという挙動を実現するには「そのまま引数を羅列する」を使わないといけないだろう。

番外編

関数を分けてしまう

上記の関数でmethodは数パターンしかなく、またRequest関数自体がGetとPostしかサポートしないのであれば、引数で取るのではなく、関数自体を分けてしまうことも考えられる。

func GetRequest(ctx context.Context, _url string, query url.Values, formValues url.Values) error
func PostRequest(ctx context.Context, _url string, query url.Values, formValues url.Values) error

中身の記述が冗長であるのであれば、別のプライベート関数に委譲すれば問題がなさそうである。

func GetRequest(ctx context.Context, _url string, query url.Values, formValues url.Values) error {
    return request(ctx, http.MethodGet, _url, query, formValues)
}

func PostRequest(ctx context.Context, _url string, query url.Values, formValues url.Values) error {
    return request(ctx, http.MethodPost, _url, query, formValues)
}

func request(ctx context.Context, method http.Method, _url string, query url.Values, formValues url.Values) error {
    // Do something
}

また、GETとPOST以外のメソッドは記述すらないわけであるから、サポートしていないDELETEやPUTなどを投げようとしても記述しようがないし、無理やり書いてもコンパイルエラーになるだけである。なので一種のバリデーションとしても機能している。

私はコードレビューでこういった状況で、パターンが少なく、またif文でバリデーションを行うぐらいであれば、関数でわけてしまった方が良いという旨のコメントをよく行う。

まとめる引数、まとめない引数

この記事の例では、context.Contextは一貫してまとめない対象である。context.Contextはドキュメントで明示的にstructに含めずにひとつ目の引数として関数に渡しなさいと書かれている。

Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx

pkg.go.dev

よって、この例でもそのようにした。net/http.Requeststructについては歴史的経緯でcontext.Contextが含まれている。Go 1.7より前はcontext.Contextが存在しなかったが、net/http.Requestnet/http.Client.Doは存在したためである。

その他にもstructにまとめない方が良い引数はあるかもしれない。例えば拙作のORM sqllaを使う際にcontext.Contextとともに渡すsqlla.DBは必ず2つ目の引数として渡すようにしている。この引数はDBの接続情報を持つものであるが、トランザクションを使う場合はある特定かつ、その呼び出しでしか使ってほしくないコネクションが含まれている。なのでstructに入れて使いまわされるよりは、その危険性が少ないシグネチャにしている。

テストでよく見かける*testing.Tもそのような対象に見える。こういった変数には何らかのパターンがあるかもしれない。