そういえば、Goには可変長引数でナントカオプションみたいなデータをわたす、というパターンがよくある。これってGoに特有のパターンだなとおもう。
とくに顕著な例として、たとえばgRPCがある。gRPC/Goには、たとえばDialOptionという型があって、gRPCサーバにつなぐときに、
conn, err := grpc.Dial(target, grpc.WithInsecure(), grpc.WithCompressor(grpc.NewGZIPCompressor()))
などのようにconnをカスタマイズするためにつかう。
DialOptionは型のなかみとしては関数になっていて、privateなdialOption型をうけとって何らかの処理をすることになっている。dialOptionは外からはみえないが実際にはただのstructで、いろんなオプション(たとえばSSLするかどうかとか、圧縮するかとか)をそれぞれフィールドとして持っている。個別のDialOptionはこれらのデータをセットする関数になっている。
func WithInsecure() DialOption { return func(o *dialOptions) { o.insecure = true } }
といった具合。こういうデータを受け取る関数の側では、
func Dial(target string, opts ...DialOption) (ClientConn, error) { opt := defaultOptions() for _, o := range opts { opts(opt) } ... }
といったふうに適用していく。
こういう “dialOptions” みたいなstructがほしいんだとすると、単純にその型をexposeしておいて、指定させておけばいいのではないか?とおもうかもしれない。つまり、
func Dial(target string, opt *DialOptions) (ClientConn, error) { ... }
こんなかんじで定義しておいて、
conn, err := grpc.Dial(target, &grpc.DialOptions{})
こんなふうにつかうイメージ。
この手法と比較すると、可変長引数のオプションをつらねる方式の利点はいくつか考えられる。
- そもそもオプションをとくに指定したくない場合には何もかかなくてもよい
- デフォルト値をゼロ値と想定しなくてよい
- それなりに記述的な表現ではある
いっぽう、欠点としては、
- gRPCの例でもそうなんだけど、なんとかオプションみたいなものが多種類あるとあつかいがめんどうになりうる。おなじような意味で対応するDialOptionとServerOptionを指定したいときに関数名をかえないといけなかったりとか(がんばればできるかもしれないが、型が微妙にちがう場合などもありうるとすると限度がある)
- 可変長引数としてつかえるのは一種類のデータだけなので、何種類も使えない
でもまあおもしろいイディオムだなーとおもう。Goのシンボル可視性、可変長引数の挙動、関数型の扱い、などが絶妙にくみあわさっているようにかんじる。
それにしても、おもしろいとおもうのにGo以外でぜんぜんみたことないパターンだよな、なぜだろうか……とつらつらかんがえてみたところ、なんのことはない、ほかの言語にはふつうにデフォルト値指定できるラベル引数があるからであった。どっちかというと言語レベルでラベル引数あればよかっただけで、いまはこういうパターンでしのいでいるとみるべきだろう。ガッカリおち。
JS は keyword 引数がないので hash + spread みたいな奇妙なかんじになっていて、あれはデフォルトの値を宣言的に書けないのでこの go の方法の方がちょっとよい気もします。Java だと更に悲惨で immutable な class を option に使って値は builder で指定しましょうみたいな。Data class がないつらみというか。
この go の idiom は知らなかったけれど、 Scala/Kotlin の data class や java の builder 的なのを functional にしてみた、というかんじ?
いいねいいね
たしかにJSにはキーワード引数はなかったですね……。ただlodashとかをつかえば opts = _.extend(defOpts, opts) とかできるんで、それほどつらみはなかった記憶。
Javaのbuilderにわりと近いな、てのはいまさっき気づきました。
いいねいいね