watermint.org - Takayuki Okazaki's note

趣味のプログラム watermint toolbox と tbx

趣味とある程度の実用性を備えたプログラムとして watermint toolboxと最近始めたtbxというプロジェクトがあります。watermint toolboxは既に何度か紹介していますが、Dropbox向けのコマンドラインツールとして開発を始め、今はそれ以外にも多様なコマンドを備えるプログラムとして成長しました。執筆時点で、最初のコミットからおおよそ6年(最初のコミットが2016年11月)、toolboxとして集約する前のいくつかのサブプロジェクトも含めると6年半ほどになります。

Wicklow

プログラムの仕様想定として、WindowsやmacOSなどの環境で追加ライブラリ等を必要としない、いわゆるシングルバイナリ配布できることを重視していたのでプログラミング環境としてはGoを選択しました。当時も他の言語選択肢はありましたが、Better Cとして名高いこと、開発環境の成熟度(≒IntelliJ Goプラグインの成熟度)、学習環境(Stackoverflowや書籍等の情報源)の充実度を考慮して決めたのだと思います。

この6年間、Goでプログラムを作ってみた成果として「Goらしくプログラムする」こともそれなりに成功したと思っています。フルタイムの仕事としてプログラムをしていたときは、言語仕様を読んだり著名なライブラリ・プログラムの設計解説や、コードを読んだりして自身の設計や開発に適用したものですが、趣味としてプログラミングを継続させようとすると違ったアプローチが必要になります。

趣味として成立させるにはある程度短期的に達成できる成功体験が不可欠と考えています(仕事として成立させるにも重要ですが場合により、必須ではない)。具体的にはプログラミング言語のチュートリアルとしてHello Worldの出力に始まり、ファイル入出力、簡単なモックアップやプロトタイプ作成、少し本格的な設計の取り込み、開発規模拡大に従う課題への対応と進んでいくかと思います。この一つ一つの段階があまり飛躍し過ぎてしまうと趣味としての継続が難しいと考えています。仕事であれば、ある程度段階が飛躍したとしても時間や費用をかけ習得したり、先達の助けを借りてこの飛躍を乗り切ることもできるでしょう。

継続は力なりと言いますが、一方で継続を実現するにはある程度の成功体験を繰り返せるための計画性も必要になってくると思いますし、実際watermint toolbox開発でもそう実感しました。この実感には裏づけとなる失敗・成功を含む別例があります。

結果的には同じようなプログラムを何度も作っているのですが、2013年から2015年にかけてScalaで開発していたプロジェクトがあります。このプログラムはもともとDDDやScala言語の理解を深めるために始めたものですが、ある程度の複雑性がある具体的なユースケースが欲しいと思い日常的に必要な課題解決(チャット操作の効率化やクラウドストレージへのファイルアップロードなど)を実装したものです。DDDやScalaの理解という意味ではある程度進んだのですが、設計がやや壮大過ぎたこともありDDD・Scalaの理解がある程度進んだという最初の(やや曖昧な)ゴールを達成したことで自然消滅的にプロジェクトが終了しました。

当初ゴール達成という意味では成功なのですが、失敗だと思っているのはせっかく2弱年もかけて作ったプログラムがあまり自身の資産になっていないというところが大きな理由です。その点、watermint toolboxではプログラムとしての綺麗さやGo言語の習得という以上に、実用性をより重視して短期的な問題解決を優先したこともあり短期的に成功体験が得られ、より継続的な開発が進められるようになりました。

Dublin

今年2022年は、コロナ禍がある程度定常的なリスク・コストと認識され仕事上でも出張が再開された年でした。今年は海外へ行く機会があり、長時間フライト中は普段できない考えの整理ができるということでwatermint toolboxについても今後どうするか考えることにしました。

watermint toolboxは趣味と実用という意味ではなかなかの成功を収めたと思っています。これをさらに10年・20年とライフワーク的に開発し、資産として形成するにはどうすればよいか考えました。一つの議論はこのままGo言語で開発を進めるかということです。

Goは手軽さやエコシステムの充実といった意味で非常に優れていると思っています。一方でいくつかの理由によりある程度の大きさのプログラムを保守するのも難しそうだとも感じています。理由をある程度絞ると次の二つが挙げられます。

一つ目は型システムがJavaやScalaなどと比較しあまり充実しておらず、特にインタフェースの設計と実装ならびに保守がなかなか手間がかかることです。Go 1.18では待望のGenericsが導入されましたが、誤解を恐れず言えば適用範囲は限定的で関数の定義をマクロ的に複数型対応にコンパイル時に展開してくれる。という程度のもので、変数の宣言や構造体にGenericsのフィールドを定義できないなど型情報を資産として形成できるほどの機能はありません。このため、たとえばある型の配列から条件に合う値のみを抽出して別の配列を作成するという処理もGoでは毎回forループを書かなければなりません。そのforループにバグがあったりテストを書いたりしなければならないコストは趣味のプログラムには無視できない大きさです。

二つ目はエラー処理です。前述の型とも関連しますが現状のGo Genericsでは Javaでいうjava.util.Optional<T>・ScalaでいうOption[+A]といったnullに頼らないライブラリ群の構築ができません。Goでのエラー処理は戻り値リストの最後にerrorを返すというのが慣例です。この、errorもerrorインタフェースを実装したポインタということで、毎回型を調べてキャストしたり、別関数で判定したりと統一感もなく注意深くドキュメントを読んだとしてもエラー処理にまつわる不具合を生じやすいことが大きな問題だと感じています。

たとえばファイルが指定パスに存在するかどうかはGoでは次のように判定します。

_, err := os.Lstat("/path/to/file")
if os.IsExist(err) {
   // 存在する場合の処理
}

このLstatが返すエラーは PathErrorという構造体のものですが、このエラーの詳細を知ろうとする場合は次のようにキャストして調べる必要があります。errorは実際にはどのような型のものかドキュメントやソースを見なければ分からず、Javaでいうところの、全てjava.lang.Exceptionとして例外を扱っているようなものです。議論の余地はあるでしょうけれど、Goで6年プログラムしてみて有益と感じたことはありませんでした。

_, err := os.Lstat("/path/to/file")
if os.IsNotExist(err) { // ファイルが存在しない場合の処理
  switch e := err.(type) {
    case *fs.PathError:
      fmt.Printf("Op[%s] Path[%s] Error[%s]\n", e.Op, e.Path, e.Err)
  }
}

Goのエコシステム、プログラミング環境の充実度はすばらしく、たとえばQRコードを作るプログラムを作りたいなと思ったとき、boombuler/barcodeのようなライブラリがすぐに見つかります。 短期的な成功体験を得るという趣味のプログラムを支えるにはぴったりです。

しかし、10年後に資産となるプログラムという意味では少し言語機能が不足していると感じるのと、ある程度プログラムが大きくなってきた時に駆られる「全部書き直したい」というモチベーションを考慮して並行して新しいプロジェクトを始めることにしました。

新しいプロジェクトを始めるにあたって、6年前と比べればプログラミングの環境も大きく変わったように見えます。GraalVMやKotlin Native、Scala Nativeなどの登場・成熟で実行ファイルのバイナリ配布の敷居が下がり、選択肢が増えました。TIOBEのプログラミング言語コミュニティ指標を見てみると、Pythonがこの5〜6年で急成長しトップになり、Javaは今月発表された結果ではついにトップ3から陥落し4位になるなど様変わりしたようです。

新しいプロジェクトの言語をどれにするかは2ヶ月ほど悩んだ結果、Rustを使うことにしました。選定理由はGoではない言語にしようと考えた理由である(1) 型周りが充実していることが最重要で、(2) エコシステムがある程度大きく必要なライブラリが探せること、(3) どうせなら本格的にプログラミングで使ったことのない言語といった理由からです。

趣味のプログラムとしてRustを始めるにあたっては、バランスの問題でwatermint toolboxを始めた頃と比べて違う計画を立てました。watermint toolboxは短期的な成功体験を継続することで成長させてきましたが、新しいプロジェクトで同じことをやると新しいプロジェクト側の方が当然楽しくなってしまい、旧プロジェクトを触らなくなってしまいます。

これを避けるために新プロジェクト側はある程度長期的なゴール設定をし2つのプロジェクトを並行して進めることにしました。watermint toolboxは引き続き短期的な問題解決のために、tbxは10年後を見据えた資産にしていくことに。tbxでは、具体的な実行可能プログラムよりはライブラリ群を最初に整備していくことでRustを習得しつつ、ある程度加速的に開発できる状態までライブラリ群が成熟した段階でwatermint toolboxの機能を逐次取り込み置き換えを目指すというものです。

Rustを習得するにあたっては、いくつか順番をおって実装していくことにしました。まずは文字列操作、続いてUUIDなど今後利用するであろうライブラリの実装、乱数など外部ライブラリをラッピングしたライブラリの構築といった順番です。細かくテストできる範囲から実装することで、所有権などRustならではのコンセプトを学びます。既存の優れたライブラリをラッピングすることは漢字の書き取りのような感じで、読むだけでは思いつかないテクニックや設計が学べます。

おそらくこのようなライブラリ群で文字列、数値、時間、KVSやデータベース、ログなどを実装またはラッパーを実装することで学びを深め、1〜2年後ごろから本格的な実装をしていくという予定です。果たしてこのような計画で進めるかはわかりませんが、継続を優先し楽しく来年もコードを書いていきたいと思います。

Dublin

皆様もよいお年をお迎えください。