Don't Repeat Yourself

Don't Repeat Yourself (DRY) is a principle of software development aimed at reducing repetition of all kinds. -- wikipedia

『RustによるWebアプリケーション開発 設計からリリース・運用まで』という本を共著で書きました

RustのWebアプリケーション開発に関する書籍を共著で執筆しました。1年くらい執筆していましたが、出版時期などが定まってきたので内容の紹介を込めて告知の記事を書きます。9/26刊行予定です。予約よろしくお願いします。

RustによるWebアプリケーション開発

AmazonのURL(アフィリエイトなし) www.amazon.co.jp

数年前に書籍を執筆した際に、「次はWebアプリケーションの実装に関する本を書きたい」と記事に書き残していたのを今見つけたのですが、有言実行できたようです。

どんな本か?

RustでWebアプリケーションの実装や運用を行うには?という疑問に答える一冊です。より応用的なRustの使い方、使えるクレートの紹介など、「現場で実際に使える」トピックを盛り込むことを意識しました。

まず大前提として、Rustの文法や機能の入門書ではありません。Rustの文法や機能に関する基本的な解説は一切含まれていません。なので、取り掛かる前にRustの入門書を一冊以上ないしは公式のチュートリアルをこなすなどして、基本的な使い方はキャッチアップすることをお勧めします。

本書はRustでいわゆるWebアプリケーションの「バックエンド」を開発する書籍です。HTTPをしゃべるサーバーを実装する話がメインです。axumというRustでは最近よく用いられるHTTPサーバーを実装できるライブラリを使って実装します。なお、以下本書で扱う開発の範囲を便宜的に「バックエンド開発」と称します。

バックエンド開発では、たとえば実装スピードを向上させたり保守性を高めたりするためによく利用されるテクニックがいくつかあると思います。しかし、Rustのコミュニティには、そうしたテクニックを使いながらRustでバックエンド開発を行うためにはどうすればよいのか?といった情報や、まずまずな規模のアプリケーションを実装し切るような知見がまだまだ不足しているのではないかという問題意識がありました。このバックエンド開発における「Rust入門以後」を進んでいくにあたり、ガイドとなるような一冊を目指しています。そういうわけで、すでに会社でRustでのバックエンド開発を経験している方には、あまり新しい発見はないかもしれません。

「Webアプリケーション」というとフロントエンドも思い浮かべるかもしれませんが、フロントエンドを扱わなかったのは、単純に著者らの実装や執筆の労力の都合と、おそらく仕事でRustを導入するとなるとバックエンドに導入することのほうがユースケースとしては多いのではないかと考えたためです。たしかにRustを使ってフロントエンドを実装することもできるといえばできるのですが、正直なところどれほど旨味があるでしょうか。また、開発現場の大多数ではReactやVueなどのフレームワークを用いたフロントエンド開発がすでに行われているはずです。こうした事情から、Rustによるフロントエンドの側面は意図的に弾いています。同様に「Webアプリケーション」の文脈で言及されがちなWebAssemblyに関する話もまったくといっていいほど登場させていません。

なので、「Webアプリケーション」と言ってもかなり限定的な範囲を扱った書籍であることに留意いただきたいと思います。あくまで、バックエンド開発にRustを用いるならという話に重点を置いています。

ところで、海外のRustコミュニティには『Zero to Production in Rust』というすばらしい「Rust入門以後」を支える一冊があります。この書籍は非常に詳しく極めて実践的な内容をバランスよく扱っており、私も多くの影響を受けています。ただこの書籍は英語で書かれたものであり、日本語圏のソフトウェアエンジニアには少しハードルが高く感じられるものでもあります[*1]

本書はまさに、『Zero to Production in Rust』にインスパイアされて書かれた一冊でもあります。この書籍を読んでおくと一旦、バックエンド開発の現場にRustを持ち込めるという状態を目指したものです。Rustの普及にはさまざまな障壁がありますし、私個人の意見としても、JavaPHPのような巨大な採用実績を持つことはなかなか難しいだろうなという肌感はあります。それでも、本書の出版によってRustがとりいれられる現場が増えるといいなと思っています。

『Zero to Production in Rust』もそうですが、本書もこれからRustでバックエンド開発を始めていきたい方々向けの内容を中心的に盛り込んでいます。すでに開発されている方はもしかすると新しい発見はそこまで多くないかもしれませんが、上記の中で何か気になるトピックがありましたらぜひお手に取っていただけますと幸いです。

Rustってバックエンド開発に向いてるの?

この問いはよく聞かれますが、裏の意図や意味をよく考えています。「Rustはみんなのバックエンド開発のファーストチョイスになりうるの?」という問いが、実際のところ意味的には近いでしょうか?とすると、答えは今のところはノーになります。一方で、「Rustでバックエンド開発って大変じゃないの?」という問いだとすると、答えは「そうでもないかもしれない」になると思います。色々な方向の問いの可能性がありすぎて、聞かれるたびに都度どういう意味かを聞き返しています😂。

あまり深く考えずに字面通り答えるなら、本書を書いた以上は「向いている」と言いたいところですが、私個人としては「Rustでも実装できるし運用できる」くらいの感覚を持っています。上述した通り、現代におけるJavaPHPRuby on Railsなどの立ち位置になるのはちょっと難しいのではないか?と思っています。そもそもシステムプログラミング言語ですしね。

詳しい議論はぜひ書籍を読んでいただきたいと思っていますが、書籍内では下記の観点から議論をしています。

  • Rustを入れるメリット
    • Rustの高いパフォーマンスの恩恵を受けることができる。
    • 非常に優れた開発者体験の恩恵を受けることができる。
  • Rustを入れる際の課題点
    • 知見の流通が少なく、知見を持つ人がいないと導入に苦労をしやすい。
    • 一通り開発し運用するのに十分なものは揃っているが、エコシステムそれ自体は細かい点においてはまだまだ発展途上である。
    • 「Rustのような低レイヤー向け言語の導入はオーバーキルなのでは?」という疑義にストレートに答えにくい。

どのプログラミング言語でもそうだとは思いますが、正直なところ慣れてしまえば書くこと自体に抵抗はなくなりますし、すらすら書けるようになってきます。時折コンパイラに怒られてライフタイムの調整に走ることはままありますが、その対処が実装時のかなりの時間を占めているかというとそうでもありません。

開発中は普通に他のプログラミング言語を書いているのと変わりないと感じています。むしろRustのShared XOR Mutability[*2]の原則などにより変数のスコープの整理が言語側の仕組みによって強制されるようになるので、結果的に見通しのいいコードになりやすいとすら感じます。

加えてrust-analyzerやclippyをはじめとするエディタやツールの支援も多いです。rust-analyzerは個人的には他のLanguage Serverと比較しても非常に完成度が高いと感じており[*3]IDEとまではいきませんがそれに近い開発体験を得られるはずです。clippyを使うとRustのidiomaticな記法を教えてもらうことができます。よく言われる話ですがコンパイルエラーも非常に読みやすいです。そういうわけで開発者体験は他のプログラミング言語と比べると群を抜いてるかなという印象です。

上記から、私個人はバックエンド開発ではRustはファーストチョイスになりえます。しかし、もうかれこれ7年近くRustを使っているため完全にバイアスがかかっています。この辺りをどう感じられたかは、ぜひ本書で実際にいろいろ手を動かしてみて、感想を寄せていただけると嬉しいかなと思っています。そうした感想の集まりが、結局のところ今後のRustの立ち位置を決めていくのだと思っています。

著者について

期間の長短はあれ、共著者の3名はRustを使ったバックエンド開発の経験を持っています。私自身は現在は実務ではRustから離れてしまっていますが、Rustで開発していた頃に本書の大多数は執筆されています。実務からは離れているものの、最近もOSSのコードをメンテナンスしているなど完全にRustから離れているわけではありませんが。その他の著者は今でも実務でRustを使用して開発を行っています。

共著者3名は『実践Rustプログラミング入門』という書籍を4年ほど前に執筆した関係性にあたります。出版後しばらくしたのちに吉川さんのWEB+DB PRESSのRust回でレビュワーとして少しお手伝いをしました。今回は講談社さん側からの私への声掛けで、当初は単著で取り組もうか悩んだのですが、単著ではなかなかモチベーションの維持が難しいだろうと思い共著で執筆しようと声をかけたのがきっかけです。

また、吉川さんの伝手でrust-jpや『実践Rust入門』の共著者でお馴染みの河野達也さんにレビューをお願いしています。河野さんには数多くの貴重なレビューやフィードバックをいただきました。中には考慮しないと致命的なものであったり、ときには実際にベンチマークをとっていただいたりなど大変ありがたいものも含まれています。

目次とトピックの簡単な紹介

目次は下記の通りです。

  • はじめに
  • 第1章 本書で開発するアプリケーション
  • 第2章 開発環境の構築
  • 第3章 最小構成アプリケーションの実装
  • 第4章 蔵書管理サーバーアプリケーションの設計
  • 第5章 蔵書管理サーバーの実装
  • 第6章 システムの結合とテスト
  • 第7章 アプリケーションの運用
  • 第8章 エコシステムの紹介

ちなみにですが、5章が一番長いです。

以下で各章の概略を説明します。

はじめに

Rustでバックエンド開発をするにあたり、どのようなメリットやデメリットが考えられるのかなどを簡単に記載しています。いわゆる導入の章にあたります。

第1章 本書で開発するアプリケーション

今回開発予定の蔵書管理アプリケーションの要件を整理する章です。

第2章 開発環境の構築

Rustの開発環境を準備するための章です。Visual Studio CodeないしはRust Roverを使った開発が期待されますが、どのエディタを用いたとしても基本的に問題はありません。なお私はNeovimですべて開発しました。

第3章 最小構成アプリケーションの実装

本書ではaxumというHTTPサーバーを実装するためのライブラリを用いて開発を進めます。まずはaxumで簡単なHTTPサーバーを立ててみる章になります。基本的に後続の章はこの章で実装した内容をベースに進行します。

また、HTTPサーバーを実装した後はsqlxを用いて簡単なデータベース接続をチェックできる機能を実装します。

アプリケーション本体の実装だけでなく、ユニットテストやCIの準備までを含みます。アプリケーション構築の最初の方からCIパイプラインを構築しておくと、ビルドやテストの成功/失敗のフィードバックサイクルを素早く回せるだろうという狙いから、この辺りから導入しています。

第4章 蔵書管理サーバーアプリケーションの設計

「設計」とタイトルに出てしまっていますが、ここで3章の実装内容をいわゆるレイヤードアーキテクチャを用いた実装に切り替えていきます。本書にレイヤードアーキテクチャを入れた意図としては、多くの開発現場でこうしたアーキテクチャが採用されているだろうという予測と、いわゆる「real world」な、現実のアプリケーションの設計に即した実装を用意しておきたかったからです。ここにはDIを含む現場で用いられるような設計手法がいくつかとりいれられています。

第5章 蔵書管理サーバーの実装

この章で蔵書管理アプリケーションを一気に実装しきります。実はコード量がかなり多くなってしまい、相当な機能数があるといえばあります。単純に蔵書を管理するだけでなく、蔵書管理アプリケーションに登録されているユーザー管理などもさせています。

第6章 システムの結合とテスト

テストに関する細かい話や結合テストに関する話題が書かれています。モックやデータベース周りのテストに関して、5章では一旦飛ばした内容が再度解説されます。

第7章 アプリケーションの運用

いわゆる「オブザーバビリティ」、「CI」、そして最後に残ったOpenAPIに関する話題を扱います。アプリケーションの運用効率を高めるためによく利用される技術が、Rustの開発においてはどう実現されるかという話を中心にまとめています。CIについては、とくに高速化の際使えそうなtipsを雑多にとりあげています。

第8章 エコシステムの紹介

この章は参照用の章です。本書で紹介したクレート以外に使えるクレートを紹介しています。

書籍を書いた感想など

今回は完全に日本のRustによるバックエンド開発シーンが盛り上がるとよいなという気持ちで書いています。現時点では本書に類似するような書籍はないと思っており、この書籍が市場に放たれることで、Rustによる本格的なバックエンド開発の知見が巷に流通するとよいなと思っています。

一方で実装がかなり大変だったかなと思っています。仕様を盛り込んでしまった関係で思ったより実装を完了させるのが大変でした。それに伴って書籍のある章のページ数が大きくなり過ぎており、この辺りを読者のみなさんが果たしてやり切れるのかどうか…は、著者としてはよくわからないなと思っています。

また、書籍の帯では「ベストプラクティス」などと銘打たれていますが、私個人の意見としては、本書の内容はあくまで「実装の一手段」として捉えて欲しいです。そもそもベストプラクティスという言葉はそんなに好きではないです。というのも、どの手段を採用するかは前提によるのが実務だろうと思っているからです。そこに存在するのは、トレードオフの検討を通じた「ベターな選択」の積み重ねであり、「ワースト」を選ばないことが大事なのです。何かが他の選択肢を否応なく排斥しうる「ベスト」になることはまずないからです。

そういうわけで、本書を読んで「こういう手があるんだな」と思っていただき、さらに自身で改良した手法を編み出して行ってもらえることを願っています。そして可能なら改良した手法をぜひインターネット上の記事に知見として放流してください!「This Week in Rust」にてお待ちしております。[*4]

ところで、Rustを入れる検討をしている会社さんがありましたらぜひお声掛けください。何か導入の支援などできますと嬉しいと思っています。

*1:もちろん原著の英語は非常に平易かつ明確で、そこまで読みこなしが難しいものでもないとは思いますが。

*2:ある参照の共有はできる。ある参照の変更もできる。ただしそれらを共存させられないというRustの仕組みです。

*3:これは私がkotlin-analyzerというKotlin向けのLanguage Serverを最近作ったり、作るための調査をしていたりした際に感じたことです。rust-analyzerのレベルの速度や安定性を持ったサーバーがなかなかないなと思っています。それもそのはず、相当な工夫が裏側には隠されています。このブログに結構おもしろいエピソードがたくさんあります。

*4:This Week in Rustは、GitHub上のリポジトリに記事のURLを追加するPull Requestを投げると記事を掲載することができます。普段はRedditやHacker Newsなどの有名どころから、編集者がキュレーションするなどして収集されていますが、読者からの応募も受け付けしています。英語の記事が多いですが、一応日本語の記事も投稿可能です。実は私は日本語記事のレビュワーを務めています。ほとんど日本語記事が来ることはありませんが…

Zedの設定をちゃんとやってみる

ここ数回の記事を見返してみると、書評ばかりしていてコード書いてるのか…?となったので、久しぶりにちゃんとコード(設定ファイル)を書く記事を書こうと思いました。いえ、コードは書いてるんですが、まとまった成果になっていないか、あまり新しいことをやっていないだけです。

Zed

Zed(ゼッド)というエディタが最近話題ですね。私も実は最近会社のPC上のNeovimが壊れてしまって、直している時間がないので一旦Zedを使ってその場しのぎをしています[*1]VS Codeを使わなかったのは、あんまりVimバインディングが強くないとわかっていたからです。あとで説明しますが、ZedはVimバインディングがまずまずよくできています。

zed.dev

ZedはRust製のエディタで、今のところ非常に高速に動作するのがウリです。プラグインを入れだすと遅くなるんじゃない?という話はあるとは思うんですが、プラグインを全然入れていない状態のZedとVS Codeを比較しても、VS Codeの方はちょっともっさりしていると感じることはある一方で、Zedはもっさりすることはまずないですね。

Rust製のツールはZed以外にも、たとえばCLIやTUIツールなどで人気ですが、特徴としてとにかく高速に動作し、画面の止まる感覚がないというのはあると思います。既存ツールをPythonやGoなどの言語からRustに置き換えたようなものを比較でいくつか使ってみていますが、圧倒的に画面が止まることが少ないです。こういうところで、やっぱりRustって速いんだなあと感じることが多いですね。

とってもどうでもいい話ですが、個人的には「ゼッド」と言われるとZeddの方が思い浮かんでしまって、開くたびにZeddを聴いてしまいます。チクタク。

www.youtube.com

Zedの設定方法と今回目標

settings.jsonとkeymap.json

設定ファイルの記述方法についてはドキュメントに詳しくまとまっているので、そちらを参照すると良いでしょう。

zed.dev

settings.jsonはZed全体の設定を記述するファイルです。たとえばテーマやフォント、vimモードをオンにするかといった設定を行うことができます。

keymap.jsonはキーマッピングの設定を記述するファイルです。私は今回は自分のNeovimに近い挙動をさせようとしていますが、たとえばIntelliJVS Codeなど使い慣れたエディタがあり、そのキーマップを引き継ぎたい場合は、サンプルとなるkeymap.jsonが用意されているようなので、自分の手元に落として使うとよいと思います。

snippet

この記事では一旦スキップしようと思いますが、いわゆる「スニペット」も設定可能なようです。要するに、よく書くコードのパターンをカスタマイズして登録しておくことができるというものです。throw Error(...)のような頻出イディオムを登録しておくことができます。最近はCopilotに完全に委ねていますが、あると便利なのかもしれません。

設定の目標

目標は手元のNeovimとほとんど同じキーマッピングであらゆる操作をできるようにすることです。テーマ設定をはじめとする普段の開発状況も可能な限り揃えておきたいと思います。

私の設定

今回は下記をやってみました。

  • Vimモードをオン。
  • いくつかの必要な設定をオン。
    • inlay hints
  • テーマをcatppuccinに変更。
  • フォントの調整。
    • サイズを変更する。
    • JetBrainsMono Nerd Font (JetBrainsMono NF)にする。
    • リガチャは有効にする。
  • キーマッピング

最終形態はこんな感じです。

セットアップ完了後

完成させると次のような設定ファイルになりました。まずはsettings.jsonからです。

{
  "theme": "Catppuccin Mocha - No Italics",
  "ui_font_size": 14,
  "buffer_font_family": "JetBrainsMono Nerd Font",
  "buffer_font_size": 14,
  "vim_mode": true,
  "relative_line_numbers": true,
  "scrollbar": {
    "show": "never"
  },
  "buffer_line_height": {
    "custom": 1.5
  },
  "inlay_hints": {
    "enabled": true,
    "show_type_hints": true,
    "show_parameter_hints": true,
    "show_other_hints": true,
    "edit_debounce_ms": 700,
    "scroll_debounce_ms": 50
  },
  "lsp": {
    "rust-analyzer": {
      "check": {
        "extraArgs": ["--target-dir", "target/ra"]
      },
      "initialization_options": {
        "check": {
          "command": "check"
        }
      }
    }
  },
  "terminal": {
    "alternate_scroll": "off",
    "blinking": "terminal_controlled",
    "copy_on_select": true,
    "font_family": "JetBrainsMono Nerd Font",
    "toolbar": {
      "title": true
    },
    "line_height": {
      "custom": 1.5
    },
    "working_directory": "current_project_directory"
  }
}

vim_mode、フォント、テーマ

最初のUIに関連する設定についてまずは見ていきます。主にはテーマ、フォント、そしてvim_modeに関する設定を施しています。

{
  "theme": "Catppuccin Mocha - No Italics",
  "ui_font_size": 16,
  "buffer_font_family": "JetBrainsMono Nerd Font",
  "buffer_font_size": 14,
  "vim_mode": true,
  "relative_line_numbers": true,
  "scrollbar": {
    "show": "never"
  },
  "buffer_line_height": {
    "custom": 1.5
  },
...

themeではCatppuccin Mochaというテーマを指定しています。私はNeovimやVS Code、ターミナルなどが軒並み全部Catppuccinを使っています。かわいくて好きです。

デフォルトでOne Darkは入っていそうですが、Catppuccinは追加でいろいろ設定が必要でした。Extensionとして配布されているCatpuccinをインストールしておく必要があります。ちなみにですが、settings.jsonに設定内容があれば何もせずとも読み取ってくれそうではあるものの、たとえばsettings.jsonで何も設定していない初回のセットアップの際などは、下記のようにテーマを手で選んでやる必要があります。

Select Themeでテーマを選ぶ。裏でsettings.jsonが修正されます。

フォントにはJetBrainsMono Nerd Fontをいつも使っています。リガチャありで使っていますが、普通の文字の方にはあまり飾りが多くなくて好きかなあという印象です。

"vim_mode": trueVimモードを利用できます。最初はこれだけsettings.jsonに書いて、あとでいろいろ調整しても遅くないと思います。Vimモードですが、定義ジャンプなど必要な機能はだいたい標準のキーマップで実行できるように設定されています。後で若干キーマップは調整を加えます。

relative_line_numbersで相対的な行番号表示に切り替えられます。これもNeovim側でオンにしていたのでこちらも合わせてオンにしています。

inlay hints

inlay hints利用シーンとして代表的なのは、変数の型情報の表示でしょう。近年のプログラミング言語は、型を明示せずとも型を推論して解決できるようになってきている関係で、変数に対する型の情報をコードに落とす必要がなくなってきています。しかし一方で、コードに型情報が落ちてこないと言うことは、ぱっと見でその変数にどのような型付けがされているかわかりにくくなることもあります。代表的なのはTypeScriptやRustでしょうか。私は普段Rustを使いますが、Rustも比較的激し目にいろいろ型付けする言語で、最終的にどのような型が生成されているのかをコードから追うのが難しい時があります。

inlay hintsを利用すると、変数の横に型情報が表示されるようになります。他には関数のパラメータ名を表示してくれたり、メソッドチェーンが連なる横に今どのような型付けになっているかを表示してくれたりします。メソッドチェーンの多いRustでは手放せない機能でもあります。

inlay hints自体は実はLSPに定義があります。したがって、使っているLanguage Serverがinlay hintsに対応している必要があります。Zedはその情報を受け取って表示しているに過ぎません。

私はinlay hintsそれ自体は普通に便利だと思っているので、NeovimでもVS CodeでもJet Brainsでもオンにしていた派です。したがって下記のように設定しました。基本的に全部オンにしています。

  "inlay_hints": {
    "enabled": true,
    "show_type_hints": true,
    "show_parameter_hints": true,
    "show_other_hints": true,
    "edit_debounce_ms": 700,
    "scroll_debounce_ms": 50
  },

注意点ですが、Zedのinlay hintsの表示はVS Codeのそれと比べると結構ノイジーです。TypeScriptなどを書いていると、型情報が100文字くらい平気でいくことがありますが、現状のZedの実装ではそれをすべて表示してしまいます。一応Issueは立てられていますが、まだ進む気配はないですね。試してはいないんですが、Language Server側に設定があるようであればそちらで表示数を削る設定を有効化しなければならないかもしれません。

rust-analyzer

Zedとは直接関係がないんですが、lspというセクションにLSPごとの設定も仕込めるようです。Zedそれ自体は正直RustかGoを書く時くらいしか使わないかなと思っているのですが、一旦rust-analyzerの設定だけ仕込んであります。

  "lsp": {
    "rust-analyzer": {
      "check": {
        "extraArgs": ["--target-dir", "target/ra"]
      }
    }
  },

Rustacean.nvimなんかではcargo clippyをrust-analyzerの解析で走らせてたりするみたいなんですが、そうしたい場合には下記のようにclippyを設定します。私は重くて嫌なのでcargo checkを走らせています。たしかデフォルトはcargo checkだったと思ってるんですが、念の為設定を上書きしています。extraArgs--target-dirを設定してやると、rust-analyzer専用の成果物を置くディレクトリを用意させられます。これをやっておくと、rust-analyzerの解析とターミナル等で走らせたcargo runなどがバッティングして、ビルドがブロックされてしまう問題を回避することができます。

      "initialization_options": {
        "check": {
          "command": "check"
        }
      }

キーバインディング

さてキーバインディングですが、下記のように設定しています。ひとつひとつつまんでいると記事を書くのが大変そうなので、要所だけ押さえておきます。

[
  {
    "context": "Editor && (vim_mode == normal || vim_mode == visual) && !VimWaiting && !menu",
    "bindings": {
      "ctrl-h": ["workspace::ActivatePaneInDirection", "Left"],
      "ctrl-l": ["workspace::ActivatePaneInDirection", "Right"],
      "ctrl-k": ["workspace::ActivatePaneInDirection", "Up"],
      "ctrl-j": ["workspace::ActivatePaneInDirection", "Down"],
      "space c": "pane::CloseActiveItem",
      "space b c": "pane::CloseInactiveItems",
      "space b C": "pane::CloseAllItems",
      "space e": "workspace::ToggleLeftDock",
      "space f f": "file_finder::Toggle",
      "space l a": "editor::ToggleCodeActions",
      "space l d": "diagnostics::Deploy",
      "space l f": "editor::Format",
      "space l s": "outline::Toggle",
      "space l r": "editor::Rename",
      "space o": "tab_switcher::Toggle",
      "space t h": "workspace::OpenInTerminal",
      "space t f": "workspace::NewCenterTerminal",
      "space /": "editor::ToggleComments",
      "g d v": "editor::GoToDefinitionSplit",
      "g r": "editor::FindAllReferences",
      ">": "vim::Indent"
    }
  },
  {
    "context": "Terminal",
    "bindings": {
      "ctrl-k": ["workspace::ActivatePaneInDirection", "Up"],
      "ctrl-j": ["workspace::ActivatePaneInDirection", "Down"]
    }
  }
]

キーバインディングの設定は、二つのセクションからなるオブジェクトで構成されています。

  • context: コンテクスト。Zedのどの場所にどの状態で差し掛かったらそのキーバーインディングを使用するか、でしょうか。contextは幅広く指定できるようで、Editorはコードを記述する場所、TerminalはZed内で起動されるターミナルにいる際、他は左に表示されるファイルエクスプローラー的な場所にいる場面、などほとんどすべてのZed上の要素を指定できそうです。このページで指定できるcontextの一覧を見ることができます→Key bindings - Zed
  • bindings: そのコンテクストで有効化するキーマッピングを定義します。

IntelliJVimバインディングだと正直コートエディタ内くらいまでしかまともに動かせない印象ですが、Zedの場合ほとんどすべてに対してキーマッピングを指定できます。非常に柔軟性が高く、手元のNeovimとほとんど同じ動作をさせるところまでたどり着くことができました。いわゆるリーダーキーを使った操作(私の場合は、<leader> = spaceでした)も定義することができて割と満足です。

Vimプラグインをたくさん入れている方の場合は、設定の自由度に物足りなさが多少あるかもしれませんが、私はそんなに詳しくないのもあり、あまりプラグインを多くは入れていない状態でNeovimを使っていたので一旦このキーマッピングで満足しています。

設定してみての感想

JetBrains系やVS Codeよりかは自由度高く設定できる印象です。おまけに今のところは非常に高速に動作するので満足しました。

欲しいのはFloating Windowなんですがこれは難しいんですかね。現状はターミナルを立ち上げ後、Shift + Escを押すと擬似的にそれっぽいことはできるんですが、Floating Window欲しいなという気持ちはあります。

git周りのサポートはまだこれから発展途上という感じではあるんですが、そもそも普段lazygitを使っているのであんまり問題ありませんでした。ターミナルを立ち上げ後、↑で示した方法でlazygitを操作すればほとんどNeovimのころと変わらない体験が得られました。

本当は仕事でも使いたいんですが、Kotlinはあんまりいい感じに動いてくれないのでまだIntelliJが手放せそうです。GoやTypeScriptの開発には積極的に利用したいんですが、TypeScriptでの開発時にはinlay hintsを切らないと型情報でコードエディタが爆発します。

それでは、Neovimの壊れてしまった部分を直す旅に出てきます。

参考資料

*1:日中の8割〜9割はIntelliJで過ごしていますが、時々Neovimで開いた方が早かったり一瞬使いたくなったりするケースがあります。ただ、そうした小時間のために直していると時間がもったいないので…といった理由です。

『Kotlin in Action Second Edition』を読んだ

しばらくは忙しく過ごしていてなかなか技術書を読む余裕はありませんでしたが、ようやく一冊読めたのでメモを残しておきたいと思います。『Kotlin in Action』という本の第2版です。未邦訳らしかったので、原著を読みました。

なお、気になったところだけつまんで読んだので、すべての章のメモが記されているわけではありません。ほとんどの章はScalaの頃の経験で概念としては知っている状態だったので、あまり印象に残りませんでした。後半戦は比較的Kotlin固有の概念が多くそちらは印象に残りました。

大前提ですが、Android側のKotlinの話はわかりません。したがって、いわゆるサーバーサイドKotlinというかよく想起されるJVMでの使用を前提としています。

書籍の内容

Part1は正直大丈夫かなと思ったので、Part2からちゃんと読み始めています。

1章

Kotlinというプログラミング言語についての概略です。Kotlinの設計思想を知ることができます。「おそらくこのような思想で設計された言語なのだろうな」という考えを持って読んではいたのですが、その答え合わせができる章だったように思います。

Kotlinは「実用的」「明示的」「安全」の3つを特徴とするプログラミング言語であると解説されています。

Kotlinの「実用的」は、実際にプログラミングをしている際に直面するユースケースから必要な機能を絞って提供していることを意味します。また、何か特定のプログラミングパラダイムに必ずユーザーを従わせるような言語ではなく、ユーザーが自身の慣れたパラダイムでプログラムを記述できるよう機能を提供しています。あるいはIDEをはじめとするツール周りの充実を徹底しており、ユーザーがすぐに開発者体験のよい開発に着手できることにこだわっているようです。

「明示的」というのは要するに読みやすいことです。Kotlinは意図的に記号を排しているように感じていたのですが、やはり設計思想的にもそうだったことが確認できました。たとえば遅延初期化をさせる際にはlateinitというキーワードを使用するわけですが、このようにキーワードを使った機能のオン・オフはKotlinを書いている上では散見されます。Swiftを書いたときもキーワードが多いと感じましたが、似たような設計になっていると思います。記号が少ないのはググラビリティが担保されるのでよいことだと個人的には思っています。

また、標準ライブラリの充実度合いも「明示的」に一役買っていると言えます。標準ライブラリが少ないと、何をするにもたくさんコードを書いて目的を達成する必要が出てきます。しかしその目的をメソッド一つで達成できるようなものが標準で提供されていれば、それだけコード中の情報量を圧縮でき読みやすいコードにつながるというわけです。

「安全性」は、たとえばJVMが担保するようなメモリ安全性やオーバーフローをはじめとする未定義動作を防ぐ意味での安全性がまずひとつ挙げられます。Kotlin固有のものとして特徴的だと思うのは、null安全とキャストの安全(つまり、型安全)です。このあたりは先日解説したスマートキャストと呼ばれる機能で静的かつ安全に処理できるのがKotlinの大きな特徴です。

型安全に関連して読んだんですが、KotlinはScalaなどとは違いコード中に現れる明示的な型付けというよりは、コンパイラが裏で暗黙的に処理する型付け(Kotlinではnon-denotable typesとかいう)が大きな役割を果たしている印象です。non-denotable typesはユーザー側から自由にいじれない点は確かにデメリットではあるのですが、一方でユーザーが使う分には軽めの型付け(?)をはじめとする言語の使い心地のライトさに貢献できていると思っています。

その他の推しポイントとしてツール周りがあがっています。個人的にはKotlinのツール周りはもう少し充実してほしいかなと思っていて、たとえばLanguage Serverを提供してほしいなと思っています。非公式のものはあるのですが、頻繁に不具合でクラッシュしたり、大きなKotlinプロジェクトを読み込むと処理がハングしたりするなど、あまり実用レベルにないなと感じています。JetBrains製のエディタを使ってほしい気持ちはわからなくはないんですが、最近だとたとえばGitHub上でVS Codeを起動したいなというときに、きちんと動作するLanguage Serverがなくてよく困っています。IntelliJプラグインで全部対応できてしまいそうですが。

9章

演算子オーバーロードの話です。演算子オーバーロード自体は他のプログラミング言語と大差はなく、他の同様の機構を持つプログラミング言語でできることは大半できると思っておいてよさそうです。

一方で少々驚きかつ注意が必要だと思ったのが、destructuringです。destructuringはdata classで利用可能で、component1component2といった関数を経由して提供される機能です。ScalaやRustに慣れている場合、当然の権利として享受したい機構でもあります。

注意点として、Kotlinのdata classに対するdestructuringはpositional destructuringと呼ばれるものだ、ということです。positionalなので、destructureする先の変数の順序が大事です。つまり、たとえば次のようなミスが起こりえます。日々の運用で普通にミスしそうですね。

data class A(a: String, b: String, c: String)

// a, b, cはフィールド名に対応するのではなく、この順番にAのフィールドの値が埋め込まれる。
// たとえば、b, cの順序が入れ替わったとしてもおそらく気づかない。
val (a, b, c) = A(...)

そういうわけで、とくにフィールドの数が多いクラスに対してdestructuringを使用することを、本書は推奨していないようです。個人的にもこの仕様を見た感じ、フィールドの数は2、3個が限度かなと思いました。

ScalaやRustではいわゆるフィールド名ないしはエイリアス先の一時変数名を使ったname-based destructuringが採用されており、混乱が少ないように設計されているように思います。Kotlinにも一応その予定はあるようで、value classに実装されることになるようです。

github.com

正直なところ少々中途半端な仕様に感じていて、できればdata class側もname-based destructuringを採用しておいて欲しかったなというのがあります。一方でKotlinの場合、applyをはじめとするスコープ関数が用意されている関係で、実はほとんどdestructuringが欲しくなる機会はないかもしれない、とも思います。これまでの経験上destructuringが無性に欲しくなる場面に出会ったことはほぼなかったです。だいたいスコープ関数で済む印象を持っています。

11章

reifiedがなぜ必要なのかあまり理解できていませんでしたが、この章を読んでよくわかったという感想を持ちました。

知らなかったのですが、JVMでは基本的にジェネリクスの型はコンパイル時に消費され、実行時には消去されるんですね。これはつまり、実行時にはジェネリック型の情報をまったく持っていないことになります。が、この仕様で困るのはたとえばジェネリック型に対してリフレクションを行いたい場合でしょうか。実際実務でも困ったんですが、その際エラーメッセージか何かでreifiedを使うよう指示された記憶があります。

inlinereifiedはセットで組み合わせて使用する必要があります。これはinline関数がいわゆるインラインコード展開を行うからで、コードの展開が行われるタイミングでそのジェネリクスに埋められるべき具体的な型情報も同時に埋め込まれ、バイトコードが生成されるためです。裏の仕組みを理解したので、次からはセットで使うのを忘れなそうです。

13章

DSLに関する解説です。KotlinはDSLを書きやすくしている印象がありますが、実際ライブラリを利用していても多々DSLが登場します。DSLは使っているうちは「使いやすい!」で進んでいけるのですが、いざ実装に足を踏み込むと急にわけのわからなさがやってきます。

この章でとくに学んだのは Receiver Type ならびに、Receiver Type Object です。どういうことかは次のコードを見るとわかりやすいのではないかと思います。

package org.yuk1ty.sandbox.chapter13

fun buildString(builderAction: StringBuilder.() -> Unit): String {
    val sb = StringBuilder()
    sb.builderAction()
    return sb.toString()
}

fun main() {
    val s = buildString {
        this.append("Hello, ")
        append("world!")
    }
    print(s)
}

buildString関数内でStringBuilderを操作する処理を受け取り、中で実行させるという関数になっています。ここでStringBuilder.()という型が出てきますが、これがReceiver Typeと呼ばれるものです。この型で定義されたブロック内は、StringBuilder型をレシーバとして使用することができます。これにより、main関数内に記述されているように、this.appendのようにStringBuilderオブジェクトに紐づくメソッドを呼び出せます。

また、いわゆる関数オブジェクトもKotlinでは実装可能です。これを利用するとGradleの設定ファイルを実装できます。

package org.yuk1ty.sandbox.chapter13

class DependencyHandler {
    fun implementation(coordinate: String) {
        println("Added dependency on $coordinate")
    }

    operator fun invoke(body: DependencyHandler.() -> Unit) {
        body()
    }
}

fun main() {
    val dependencies = DependencyHandler()
    dependencies.implementation("org.jetbrains.kotlin:kotlin-test")

    dependencies {
        implementation("org.jetbrains.kotlin:kotlin-test")
    }

    // invokeは下記と同じことをしている。Scalaのapplyと同じで、関数オブジェクトを呼び出すためのメソッドだと思う。
    // dependencies.invoke { implementation("org.jetbrains.kotlin:kotlin-test") }
}

DependencyHandlerインスタンスを作っておき、あとはそれを関数として利用できるようにinvokeという特別なオペレータを実装します。9章でも見たようにオペレータはKotlinで登場する特別視される関数のようなもので、たとえば足し算や引き算などの演算から、インデックスアクセスを可能にするget演算子、デストラクタまで幅広くあるようです。

dependencies変数で保持したDependencyHandlerインスタンスを実質的に関数呼び出しとさせることで、dependencies {}という呼び出し方ができるようになりました。DependenciesHandlerinvoke関数が引数にとるのはDependencyHandler.()です。つまり、レシーバを中で呼び出し可能ということです。これにより、dependencies {}ブロック内でimplementation関数を呼ぶことができます。

14章以降

Coroutinesに関する話だったのですが、こちらは別で記事にして概念を整理しようと思っています。

個人的なKotlinに対する印象

まだ使い始めて2ヶ月程度ですが、現状持っている印象を記しておきます。

モダンな言語機能がまずまず揃っている

私はScalaやRustのユーザーではありますが、そうしたユーザーから見ても一通り必要な機能は(そのままの形で導入されているわけではないにせよ)揃っていると感じています。というか、総じて機能は多めですね。もちろんいくつかはKotlinのコンパイラの思想に合うようにカスタマイズされて導入されているように見受けられます。

たとえばパターンマッチと他のプログラミング言語では呼ばれるwhenがあるでしょう。これはスマートキャストという概念と組み合わせて利用できるように、Kotlin向けにカスタマイズされて輸入され使用されているように思われます。あるいはScalaにあるようなコンパニオンオブジェクト等の概念も一通り揃っており、Kotlinの言語設計に合うようにカスタマイズされています。

もちろんこの「カスタマイズ」には好き嫌いがあるような感想を聞くことはありますが、私は達成したい目的が達成されていれば手段はなんでもよいタイプなので、そんなに気になっていません。それよりそうした非常に便利で保守性を高められるような優れた機能がそもそも「ない」ことの方が気になります。

思ったより静的に色々解決できる

思ったよりもきちんと静的解析に倒されている処理が多いです。型情報にすべてを落とす文化でコードを書いていた側からすると、思ったよりコンパイラによって裏で暗黙的に処理されている型付けが多く(non-denoted typesといいます)違和感があるかもしれません。

たとえばスマートキャストは一見するとキャストの類なので動的な型付けが起こり、静的に解決されていないように見えるかもしれません。しかしスマートキャストそれ自体はフローから型情報を取得し、対象の型付けを解決する静的解析な処理です。

本書の最初の章でも「安全性」に対してフォーカスが当たっていましたが、安全性(たとえば不意にNPEしないとか、キャストの結果ランタイムエラーが投げられないとか)についてはよく担保されているように思います。

coroutineはすばらしいが

Kotlinにはスタックレスコルーチンが導入されています。これにより並行処理を非常に楽に実装することができます。関数にsuspendと記述するだけで、自動的にその関数の処理はコルーチンのキューに入れられ、処理されるようです。suspend funsuspend fun内でしか呼び出せませんが、呼び出し箇所に都度awaitのような記述をする必要はありません[*1]。これは開発者体験が少し優れていると思います。

一方で運用していてしんどく感じるのはコルーチン内で送出されたエラーのスタックトレースです。呼び出し元となった関数とは別のスレッドで実行されるからか、呼び出し元の関数の情報は切り離され、例外が送出されたスレッドを起点にスタックトレースが吐き出されてしまうように見受けられます。ツールを使った緩和策などあるようですが、そもそもこの辺りは言語側でちゃんと追跡して欲しいなという気持ちは正直なところあります。なお、私が深く知らないだけでたとえばコンパイラオプションなどがあるのかもしれません。

サーバーサイドKotlinへの所感

いわゆるサーバーサイドKotlinについては、思ったよりPure Kotlinなエコシステムが充実していない印象を持っています。現状だとJavaの資産を使える関係で、たとえばSpring類を使えば幅広く要件に対応できるという可用性を持っているといえば持っています。しかし、Pure Kotlin側のエコシステムが現状はイマイチです。たとえばKtorのgRPC対応がまだ、などです。

ではJavaの資産に乗っかればよいのでは、という意見もあると思います。しかし各ライブラリを使用した際にJavaの資産に引っ張られるのも一長一短あると考えています。先ほども言及したようにJavaの大きなエコシステムにすぐに乗れるのはメリットです。一方でデメリットは、たとえばJavaの標準ライブラリないしはサードパーティライブラリから提供されるレガシーで微妙なデザインのAPIを使用する必要があるケースです。レガシーなデザインは、モダンなプログラミング言語ではそもそも踏む必要のないステップを踏んでいたり、保守性や運用性に難があることが多いと思います。そうしたbad habitに引っ張られる傾向にあるのは明確なデメリットでしょう。

エラーハンドリングのチグハグさ

Javaの資産に引っ張られる関連で他に問題なのは検査例外の扱いだと思います。現状Kotlinでは(Scalaと同じように)検査例外のキャッチを強制しない方向に倒しているようです。その際問題になってくるのが、Javaの資産を呼び出した時の対応です。こうした資産側からの例外は、検査例外をキャッチしない手前すべてスローされ握りつぶされることはないといえばないのですが、これだと実アプリケーションの保守や運用上困るケースがあります。たとえば関数のシグネチャを見ただけでは例外の送出の可能性をすぐに認知できない、コードを奥深くまで辿る必要があるなどです。これはただ、Javaとのインテグレーションを前提としている以上、多少仕方のない仕様なのかなと思います。

Kotlinはエラーハンドリングにしばらく悩んでいるように見受けられます。Result型をKotlinの標準ライブラリで提供したものの、デザインがイマイチな関係からかあまり利用されている印象は持っておらず、多くのサーバーサイドKotlinの実プロジェクトでは、kotlin-resultやarrow-ktといったライブラリの使用が検討されているようです。一方で、Result型ないしはEither型はたしかに優れた解決策ではあるのですが、こうしたライブラリは単なる複雑な型パズルを生んでいるだけという見方もできてしまいます。加えて初期化ブロックinit内でのエラーハンドリングも困りものです。

解決策としてKotlin 2.0以降での導入が行われているユニオン型を利用したエラーハンドリングが検討されているようです。つまり、T | Errorという型を返すということです。ここにさらに言語側からのエラーハンドリングの支援が加わるような提案もなされており、これが承認されると、Kotlinのエラーハンドリング周りのデザインはかなり前進することになるのではないかと思われます。ユニオン型によるエラーハンドリングは、個人的にはKotlinのような手続的書き方を多く使用するプログラミング言語とは相性がよく、そもそも複雑な型付けをユーザーに押し付けないKotlinの設計思想と良くあっていると思っていて今後に期待できそうです。一方で、Kotlinにおけるエルゴノミックなエラーハンドリングには初期化ブロックやrequireのような標準ライブラリにいるアサーション関連の関数に対しても改善が必要になると思っています。なので、統一的な議論がなされるとよいなと思っています。

関数型プログラミング

Kotlinで関数型プログラミングをする動きは、実際記事などでよく見かけます。たとえば次のような書籍を見かけており、個人的にも試してみています。

Arrow-ktというライブラリもあり、こちらを利用するとEither型などを利用できて嬉しい!といった感じのようです。

arrow-kt.io

個人的な意見としては、Kotlinでも十分関数型プログラミングを使ったアプリケーションの構築は楽しめると思っています。他のプログラミング言語でいわゆるモナドをはじめとする概念を利用する際、専用記法がないために逆に型パズルを生んで大変になっているケースが散見されますが、Kotlinの場合DSLがあります。これが意外によく効いてくるように思っており、少なくとも型パズルはDSLである程度防げる感じがしています。

ただ、それをするのであれば、そちら方面のエコシステムがより充実した、歴史のあるScalaも選択肢となってくるわけです。ScalaにはAkkaなどの実績あるフレームワークやライブラリも多くあります。「関数型プログラミング」という新しい概念を学ぶ必要がある場合、であれば最初からScalaを選択してもよいのでは?そちらの方が安全択では?と私はなってしまうのです。Kotlinでわざわざ関数型に取り組みたい理由が見つからないのです。[*2]

そういうわけで、私個人はKotlinでいい感じに手続型、オブジェクト指向、関数型あたりを融合したマルチパラダイムなプログラミングをやっていきたいなと思っています。一昔前、Scalaで言われていた「Better Java」という言葉がこれに近いでしょうか。そういうわけでKotlinのエラーハンドリングが、現状のKotlinが持つResultEitherのようなコンビネータ方式のものではなく、ユニオン型と言語側から支援を受けた脱出機構を組み合わせたエラーハンドリングだと嬉しいと思っているというわけです[*3]。スマートキャストとの相性もいいですしね。

*1:main関数に至るまでには、一度以上Rustでいうtokio::spawn_blockingに相当するrunBlockingブロック内に入れ込む必要がありますが。

*2:Scalaエンジニア」で募集をかけてしまうと採用市場で大変だから、という理由はあると思います。会社の技術戦略的な都合でKotlinを採用しているものの、関数型プログラミングにチャレンジしたい場合にこうなるのかなとは推察しています。ちなみにですが、Kotlinも情報発信されている量も求人も少なく、Scalaを採用する場合と実は大差ないのではと個人的には思っています。この辺りは定量的に分析してみると興味深い結果が得られるかもしれません。

*3:RustのResult型は同様に?という脱出機構を言語側が用意しており、これにより手続的なプログラミングスタイルを損なわずにコードを書けるようになっていると思っています。もちろんmapやand_thenのようにコンビネータを利用する記法も使えます。

『ルールズ・オブ・プログラミング』を読んだ #iknowtherulesjp

Ghost of Tsushimaなどを作った会社の人が書いた本です。ゲーム開発におけるコードを書く際の教訓を整理し、改めて示し直したいい一冊だったと思います。大事なことですが、著者は決して「このルールを絶対使え」と言っているのではなくて、そもそもまず会社の製品の特性上、このようなルールを敷いておくと品質や生産性を高く保てたという前提があり、その前提を元に「ルールを選び取って自分たちのコーディング哲学を構築しよう」と推奨しています。

この手の本では『リーダブルコード』がよく薦められる傾向にあると思います。私にとってもリーダブルコードは確かに駆け出しの頃すごく役に立った記憶はあるのですが(もう10年くらい前に読んだので正直忘れた)、そこから知識がアップデートされておらず、私の中ではもう古い書籍になっていると言えるかもしれません。そして、なぜこの本ばかりが現場エンジニアからやたら薦められるかというと、薦める上級エンジニア側がビギナーやジュニアのエンジニアにこの手の話を手解きする際、上級エンジニア側のコーディング一般の書籍の知識がアップデートされておらず、結果『リーダブルコード』が薦められるという傾向にあるのではないかと思っています。なぜかというとコーディング一般の話はあまりに常識すぎ、駆け出しの頃に一度読んでキャッチアップできたあとは、再び自分で知識をアップデートする必要に駆られない傾向にあるためだからではないか、と思っています。

が、著者も本書の中でいうように、そもそもコーディング一般のルール、すなわちコーディング哲学は、チームを組むたび、あるいはチームに新しい参画者が増えてきたタイミングで都度議論し合われるべきです。コーディング一般に関する新しいインプットをしていないということは、すなわちこの辺りの話を日常の業務でさほどしていないことの証左になりえてしまうかもしれません。この辺りは私も反省点があり、なるほどと思わされました。正直なところ、この手のコーディング哲学の本はあまりに当たり前すぎて読んでいて退屈ですからね。でも、よくよく議論しあうとチームメンバー同士で微妙に前提が違ったりすることはよくあります。たとえば、Java出身の私がC++出身者のチームに入ると、Java出身者の私はフルネームで全部書きたがる傾向にあるが、C++出身者は短い変数名や場合によっては省略された命名を好む傾向にある、などです。[*1]

この本には21個ルールがあるので、中でとくに個人的によかったものを書き留めておきたいと思います。なお、書籍内には数多くのC++コードが掲載されており、文章以外にはそれを通じて実際の議論の進行を眺めることができます。

1. 単純さ

「単純さ」というか「シンプル」は、コードを書いているうちに議題に上がることがよくあるものの、実態として何を示しているのかいまいちよくわからない単語ではないかと思います。著者はこのことについていい言語化をしているように思っており、紹介しておきます。

単純さの計測はたとえば、次の手段を用いているかもしれません。

  • チームにいる他のチームメイトにとってどれだけ理解しやすいコードになっているか?で測る。
  • コードをどれだけ簡単に作成でき、かつ作成できたコードに不具合が存在しない状態まで持っていくかにかかる時間で測る。

単純の逆は複雑ですが、複雑さの計測にはよく使われるものとして次のような手法があるかもしれません。

  • 書かれているコードの量が少ないこと。
  • 持ち込んでいる概念の数やそのコードを読むのに必要になる新しい用語の数が少ないこと。
  • 説明するのにかかる時間量が少ないこと。

忘れがちなんですが、持ち込む概念の数が複雑さにつながることはままあるように思いました。個人的な癖としてついつい新しい概念を創造し、それを表現するためのデータ構造を追加してしまいがちなんですが、これは確かに複雑性を高めることになるんですね。できるだけ少ない概念で説明できるか、もしくは概念同士に関連があり、一つがわかると芋づる式に他の概念も理解できる関係性を持つような概念を作り出す必要があるんだなと思いました。

なお忘れてはいけないこととして著者も強調していますが、目の前のお題の考察をおざなりにしてコードの単純化ばかりに取り組んではいけません。そもそも取り組んでいる問題を単純できないかとか、問題に対する解法を単純化できないかなどを先に考えてみるべきとのことです。その通りです。

4. 一般化には3つの例が必要

どのタイミングで目の前のコードを一般化するのは悩みの種ですね。将来を見越して一般的な関数にしてしまおうか…と逡巡することは多いです。下手したら1日1回はやってるんじゃないかな。

著者は明確に、一般化には3つの例を思いつかなければならないと述べています。わかりやすい基準ですね。逆にいうと、2つ例を思いつく程度では一般化をする必要はなく、2つの例に対してそれぞれ関数なりを提供すればそれで済むのだ、ということです。

一般化はたしかに数が増えてこればメリットは多少あるのですが、正直デメリットに直面することの方が多いでしょう。一般化の仕方がきれいでなかったばかりに、別のユースケースでの利用を行うと追加でさらに修正が必要になるようなパターンです。これにより本来一般化で享受したかったはずの変更のしやすさより、変更のしにくさの方が目立つようになってしまいます。すべては、一般化するユースケースの数が足りず、考察が足りないがために起きていることと言えると思います。

5. 最適化するな

  1. 最適化するな。
  2. 単純なコードを速くするのは簡単だ。
  3. だいたいの最適化に関する懸念は存在しない。みんな気にしすぎ。

基本的には1、2を守りつつ、たとえばHFTのトレーディングシステムをPythonで作っていて遅いと思うのであれば、ゴッソリC++に置き換えてしまえばいい、という主張です。個人的にはシンプルでおもしろいと思いましたし、そもそもシンプルなコードを書いていれば最適化が容易というのもなんとなく、経験則に合うような気がします。

6. コードレビュー

コードレビューの目的は、バグを見つけること以外には知識の共有があると言います。コードレビューの適切な実施はチーム全体に知識を広める優れた方法です。

ところで、より有意義なコードレビューをするために、筆者はさらにコードレビューを4つに分類し、それぞれ意義があるかどうかをまとめています。

シニアレビュアー ジュニアレビュアー
シニアレビュイー 有用 有用
ジュニアレビュイー 有用 絶対禁止

私もやはり同様にジュニア同士のコードレビューにはあんまり意味がないなと思っていて、この表の整理はしっくりきたなと思いました。シニアとのレビューは、不具合の発見だけでなく、たとえばコードのチーム内での規則違反を発見できたりとメリットが大きいとのことです。コードレビューは教育プロセスやオンボーディングプロセスのひとつと捉え、真剣に場を設計する必要がある行為と言えるでしょう。

コードレビューの副次的な効果として、成果物を他の人に見せる必要があるとわかっていると、誰しも良いコードを書くようにがんばるというものがあります。「仲間に喜んで見せたり誇れたりする仕事をしなきゃいけない」、つまり仲間の同調圧力(ピアプレッシャー)の健全版である、と言っています。

コードレビューは極めて有用な社会的交流の場であり、そういう側面を持っているからこそ、レビュアーとレビュイーの意見交換が積極的に行われるよう努める必要があります。レビューの場が論争になっているのは何かがおかしい証拠で、それではお互いに何も学べなくなってしまいます。あとは、コードの規則や命名について議論する場ではなく、そうした水掛け論に近い主観で決まる議論はチームで解決すべきです。

11. 2倍よくなるか?

既存のアーキテクチャをまるっと置き換える時の話です。この手の話は費用対効果を論じるのが難しく、定量的な議論をがんばろうとすると結果結論を得られず物事が進まないが、一方で現場はいつまでも限界を迎えつつあるアーキテクチャの上で苦労しながら機能開発を進めなければならない、みたいな話はまあまああるのかなと思います。

著者は、この手の話をする際には、経験則から「2倍」よくなるかで決めているそうです。2倍にはいろいろあり、単純な人月計算での工数を減らせたりとか、そもそもかかるメモリ量を減らせたりとか、そういった基準です。それを満たせなそうな場合は、一旦小さい部分の改善をするに留めておきます。シンプルでわかりやすい基準だと思います。使っていきたいですね。

なお、「作り直しは並列で行う」というルール19が後半で登場します。2倍よくなることがわかって、作り直しが決定したら、ルール19を眺めてみるのもよいかもしれません。

14. 4種類のコード

この章は次の表が印象に残っています。

「やさしい」問題 「難しい」問題
「単純」な解法 期待通り 野心的
「複雑」な解法 実に、実にひどい 容認されている

やさしい問題をわざわざ複雑な解法で解くのはやめましょう、ということでしょうか。FizzBuzzEnterpriseなんかはいい例かもしれません。

平凡なプログラマはやさしくても難しくても複雑な解法を書きます。優秀なプログラマは、やさしい問題ならば単純な解法を書きますが、複雑な問題は難しい解法で解きます。偉大なプログラマは、どちらも単純な解法でコードを書く…とのことです。

まとめ

この本はプログラマ個人がどのようにプログラミングをするべきかという議論も多少はありますが、どちらかというとプログラミングそれ自体を集団的・社会的な行為として捉え、それに対する著者の洞察をアドバイスするという書籍と捉えるのがよいのかなと思います。

大規模開発でよく起こるさまざまな事象に対して興味深いアドバイスをいくつかしてくれており、私も最近規模が大きめのチームを見ることになったので、自分としても活かせる点が多いなと思いながら読んでいました。

一方で、たとえばスタートアップやそもそも一人で開発しているといったシチュエーションでは、必ずしもこのルールが役立つとも限らないと思います。著者も前段や最後で指摘するように、これらのルールはそもそもゲームの大規模開発から得られる知見であり、自身のチームに合わせてこれらのルールをカスタマイズする必要があると思います。

網羅的なコーディング指南書というよりかは、どちらかというと私のようにスタッフエンジニアくらいの人が、自分のチームやプロダクトの開発の指針をどうしていこう?と考えたときに、一定程度の指針を与えてくれる一冊なのかなと思っています。シニア〜スタッフエンジニアくらいの目線を知りたいまだシニアでないエンジニアにも、そういう意味ではおすすめできるのかもしれません。

*1:これは昔の職場での実際の経験談です。もしかすると、C++のように言語によらない話なのかもしれません。

『Tidy First?』を読んだ

最近アーキテクトなるお仕事になったようなので、コードやアーキテクチャ関連の本を読み漁っています。何冊か読んでいるんですが、まずは最近Kent Beckが出版した『Tidy First?』の話を書きたいと思います。

パート1: Tydings

「Tidy」というと、USでは一時期からコンマリが大流行りしているようで、「Kondo」がそもそも動詞化していたりするなど一大ブームとなっている(た)ようです。コンマリといえばそう、「お片付け」なんですが、なんとなくここから着想を得ているのかなと思います。Netflixでも「Tidying Up with Marie Kondo」という番組が作られていたくらいです。

Tidyingは「片付け」ないしは「整理整頓」あたりで訳せそうではあるんですが、日本語訳版(出るのかな?)で定訳がまだ出ていなさそうなので、一旦英語表記のまま話を進めます。

Tidyingは、リファクタリングの部分集合のようなもので、リファクタリング行為がときどきもたらす、長い期間機能開発を止めてしまうようなネガティブな側面を軽減する役割を持つ行為かと思われます。パート1の最初のページに、次のような文言が並んでいます。2文目の方では現代ではリファクタリングという言葉の指す意味合いさえ変わってしまったといい、「動作を変えないという文言さえ削除され…」と嘆いています。

Tidyings are a subset of refactorings. Tidyings are the cure, fuzzy little refactorings that nobody could possibly hate on.

"Refactoring" took fatal damage when folks started using it to refer to long pauses in feature development. They even eliminated the "that don't change behavior" clause, so "refactoring" could easily break the system.

Tidyingに該当する行為としてパート1では、ちょっとした変数名の調整や、対称性のない実装は対称性を保つこと、コメントの微調整や使っていないコードの削除などが挙げられていきます。このあたりは、Tidyingの具体例を示しているでしょう。

一見するとリファクタリングと同じように見えてしまうのですが、リファクタリングは確かにもう少し大規模で、それよりかは小規模な作業の「リファクタリング」なのが「Tidying」です。が、リファクタリングとは微妙に異なる概念ではあり、異なる概念に丁寧に名前を当て、その概念が具体的に何をする行為なのかを示すのがパート1の役割なのかなと思っています。

Tidyingはボーイスカウトの原則とも相性が良さそうです。

パート2: Managing

パート1で「Tidyings」でしたが、パート2では具体的にどう実行するかに焦点が当てられます。このパートでは、次の問いに答えられる形で話が進んでいきます。

  • いつTidyingを開始し、いつTidyingを終えるか?
  • Tidyingとシステムの振る舞いを変えるようなコードの構成の変更をどう組み合わせていくか?

まず後者からですが、Tidyingのコミットと機能変更を含むコミットとはそれぞれ分けてプルリクエストを作るようにしましょう、とのことでした。機能開発とTidyingsがまぜこぜになりそうな場合には、いい感じの単位で分けるようにするのを推奨しています。これは実際の開発現場でもそうで、リファクタリングと機能開発のプルリクエストは分けましょう、みたいな話をされることは多いでしょう。それと同じと思いました。

どのくらいの単位で取り組むべきかについては、どうやら時間を区切りとすることを考えているようです。最大でも1時間以内に終われないTidyingsはバッチサイズが大きいと言えるため、これは実質機能開発に相当してしまうかもしれません。本来の目的を見失っている可能性が高いと指摘します。

いつTidyingを開始するかですが、これは機能開発前である、という主張になっています。後回しにすればツケが回ってきますし、直後にやろうとすると今度はいつやれるときが来るかとか、Tidyingまで完遂しないとタスクを完了した感がなくなるなどの理由で、機能開発直前に行うことを推奨しています。これはなかなかなかった視点で、まず目の前のコードの状況を整理してから本題のタスクに取り組むというのは、料理をする前にまず必要な調理器具が取り出せる位置にあるか、なければ片付けて整理してから開始すると、料理中に焦らなくても済むみたいな話に近そうかな?(ちなみにですが、ちょっと違うか…)と思いました。

パート3: Theory

最後は理論編ですが、最初は経済的な分析、次にソフトウェアアーキテクチャの理論上の分析という構成になっています。

経済的な分析の方は、詳しい説明は端折りますがNPVとオプションの話が導入として入り、導入後これらの考えを使って少し思考実験をします。この経済分析を通じて強調されることは、Tidyingそれ自体やひいてはソフトウェア開発それ自体は経済的なインセンティブ(その作業にかかるコスト、その作業が結果的にもたらす将来の連続的なタスクの流れの中における効果)を度外視して推し進めることは難しく、常にこの手の話を頭に入れておく必要があるということです。

もう一つのアーキテクチャリングの理論的な話としては、Tidyingsは疎結合かつ高凝集を目指しながら行いましょう、というものです。これは目新しいものではなく、トレードオフを意識しながらやりましょうね、という話が書いてあったかなと思った程度でした。

感想

Tidyingないしは「片付け」という単語自体はぜひチームにはやらせていきたいと思いました。リファクタリングというほど大掛かりではないものの、やっておくと後々その投資効果が出てくる類のものだと思います。また、この手の話の説得というか理論武装としてファイナンスの考え方を持ち込むのはよい使い方だと思いました。こうした説明をすると、投資が好きな人なら納得してくれそうなので、積極的に使っていきたいところです。オプションの話はちょっとファイナンスのバックグラウンドがないと理解が難しそうですが…

Kotlinのスマートキャスト

最近はKotlinエンジニアをしています。Scalaを5年ほど書いていたので、Kotlinの文法それ自体はScalaの知識でだいたい追いつくことができました。一方で、使えたら便利だと思う機能としてスマートキャストという機能に出会いました。Kotlinには言語仕様書があり、それを読み解くといくつかおもしろいことがわかったので、仕様書を読んだ内容を簡単にまとめておきたいと思います。

スマートキャストとは何か

Kotlinを書いていると、ときどき次のようなコードに出会します。たとえばNullableな型に対してnullチェックを挟むと、その処理以降の型はnullを挟まない型として解釈されるというものです。下記のサンプルコードに示すように、nullであるチェックを走らせる前のコード中で推論される型と、nullであるチェック後に推論される型が変わる(実質的に型の「キャスト」が起こっている)ことがわかります。

fun nonnull() {
    var a: Any? = 42
    
    // a = Any?型

    if (a == null) {
        return
    }

    // a = Any型に変わる
    
    println("${a::class.simpleName}")
}

fun main() {
    nonnull()
}

スマートキャストは、「Flow-sensitive Typing」と呼ばれる型付けの手法を限定的に採用したものです。Flow-sensitive Typingというのは要するに、型付けの際に制御フローの情報を得て型付けの際のヒントにする手法のことを指します。たとえば先ほど説明したifをはじめとする制御構文の情報を追加で型付けの情報として利用するということだと思います。最初に導入されたのはSchemeで、似たような仕組みを取り入れているものとしては他にTypeScriptがそうかな、と思います。

Flow-sensitive Typingのメリットは、不要な実行時の型のキャストを減らせるという点にあるようです[*1]。Kotlinで思いつくケースだと、たとえばAny型で一度型付けをしておいたものの、その後の処理でそれがInt型であることがわかった場合、実行時に型をキャストする必要が出てきます。ところが、スマートキャストをはじめとするFlow-sensitive Typingが採り入れられた型システムにおいては、制御構文等でInt型であることを同定できる情報を付与しておきさえすれば、そのキャストをコンパイル時に行い、実行時に回さないようにすることができる、ということです。あるいはとくにダックタイピング等を導入する動的型付き言語では大きな威力を発揮する手法であるという予想が成り立つように思います。

スマートキャストはデータフロー解析の一種であると捉えられます。したがって、スマートキャストを定義するためには、そもそもデータフロー解析のフレームワークを定義する必要がある、と仕様書は述べています。データフローの領域は、いわゆる束(lattice)の構造になっており、SmartCastData = Expression -> SmartCastTypeという構造を持つようです。…が、この辺りは真面目に読んでないというかあんまりわからないので、詳しくは仕様書をご覧ください

(若干ここからの記述には解釈の誤りを含む可能性がありますが)コンパイラでは、スマートキャストそれ自体はそもそもきちんとひとつ型(というかExpression)が用意されています。そのデータの中では、元のExpressiondelegateする形でスマートキャストの結果を格納する型情報などが格納されています。加えてスマートキャスト時には、そのキャストの結果が「安定的か」どうかを知っておく必要があるのですが、それもプロパティをとして持っているようです。要するに普通のExpressionの解釈とは別に、それを(おそらく)上書きする形で特別視してコンパイラがスマートキャスト情報を保有しているということです。タイミングとしてはFIRを生成するところで保持するようです。

package org.jetbrains.kotlin.fir.expressions

// 筆者が省略

/**
 * Generated from: [org.jetbrains.kotlin.fir.tree.generator.FirTreeBuilder.smartCastExpression]
 */
abstract class FirSmartCastExpression : FirExpression() {
    abstract override val source: KtSourceElement?
    @UnresolvedExpressionTypeAccess
    abstract override val coneTypeOrNull: ConeKotlinType?
    abstract override val annotations: List<FirAnnotation>
    abstract val originalExpression: FirExpression
    abstract val typesFromSmartCast: Collection<ConeKotlinType>
    abstract val smartcastType: FirTypeRef
    abstract val smartcastTypeWithoutNullableNothing: FirTypeRef?
    abstract val isStable: Boolean
    abstract val smartcastStability: SmartcastStability

// 以下、関数定義が続く

スマートキャストが適用される条件

大事な点は、スマートキャストがいつ発動するかを知ることです。スマートキャストは少々暗黙的な機能です。コード上には直接は表現されない、コンパイラが付与する追加情報のようなものなので、コードの字面からそれが起きていることを追うのはなかなか困難なように思います。したがって、発生条件を知っていると幾分かコードリーディングに役立ちます。仕様書によると、スマートキャストは下記の条件で発生するようです。

  • if
  • when
  • エルビス演算子?:
  • セーフナビゲーション演算子?.
  • &&
  • ||
  • Non-nullアサーション!!
  • as
  • is
  • 単純な代入(simple assignments)
  • あとは、プラットフォーム依存のケース。何があるかは知らない。

その他

スマートキャストシンクの安定性

まず、このタイトルを理解するためには言葉の定義が必要になります。スマートキャストをさせる際に発生記述する条件のことを「ソース(source)」と呼び、スマートキャストされた結果導出された型情報を使って何かしらのメソッドの呼び出しなどをかけることをシンク(sink)と呼んでいるようです[*2]

var x: Int? = 42 // 定義
if (x != null) { // スマートキャストソース(smart cast source)
    x.inc() // スマートキャストシンク(smart cast sink)
} else {
    x = null // ネストされた再定義
}

そして、シンクが「安定的(stable)」であるというのは、制御フローグラフの外側からシンクの状況がいじられないことを指します。その結果、先のコード例のようにx.inc()xの型付けが、今回のケースでは「確実に」non-nullなものであると同定することができ、その結果が変わることはないというわけです。

逆にシンクの安定性を壊すことができるケースは、たとえば

  • ミュータブルな値のキャプチャ
  • 並行の書き込み
  • モジュールを跨ぐコンパイル
  • カスタマイズされたgetter
  • delegation

が仕様書には書かれています。実はコンパイラの実装を探してみたところ次のような実装があり、こちらを読むとさらに詳細な条件がわかると思います。SmartcastStabilityenumですが、まさにスマートキャストの安定性を示すためにコンパイラ内部で使用されているようです。enumのうちSmartcastStability.STABLE_VALUEのみが「安定的」である状況を示しており、それ以外のenumはすべて、「安定的でない」ケースを示しています。

package org.jetbrains.kotlin.types

/**
 * See https://kotlinlang.org/spec/type-inference.html#smart-cast-sink-stability for explanation on smartcast stability. The "mutable value
 * capturing" and "concurrent writes" categories in the spec are covered by [CAPTURED_VARIABLE] and [MUTABLE_PROPERTY] together.
 * Specifically, we only do capture analysis on local mutable properties. Non-local ones are considered to be always unstable (assuming
 * some concurrent writes are always present).
 */
enum class SmartcastStability(private val str: String, val description: String = str) {
    // Local value, or parameter, or private / internal member value without open / custom getter,
    // or protected / public member value from the same module without open / custom getter
    // Smart casts are completely safe
    STABLE_VALUE("stable val"),

    // Smart casts are not safe
    EXPECT_PROPERTY("expect property"),

    // Member value with open / custom getter
    // Smart casts are not safe
    PROPERTY_WITH_GETTER("custom getter", "property that has an open or custom getter"),

    // Protected / public member value from another module
    // Smart casts are not safe
    ALIEN_PUBLIC_PROPERTY("alien public", "public API property declared in different module"),

    // Local variable already captured by a changing closure
    // Smart casts are not safe
    CAPTURED_VARIABLE("captured var", "local variable that is mutated in a capturing closure"),

    // Member variable regardless of its visibility
    // Smart casts are not safe
    MUTABLE_PROPERTY("member", "mutable property that could be mutated concurrently"),

    // A delegated property.
    // Smart casts are not safe
    DELEGATED_PROPERTY("delegate", "delegated property"),
}

ところで、スマートキャストが安定的でなくなるケースはどのようなものがあり得るでしょうか?一例を示すと、次のコードはうまくいきませんでした。これはミュータブルな値xに対するキャプチャがrunブロック内で引き起こされるため、ブロック終了後に再登場するシンクは安定的でないという判定になるためです。スマートキャストそれ自体は制御フローの解析によるものであるため、スコープが確定しないとどこまでの制御フローを見ればよいかという情報がなくなってしまうからだと思われます。先ほどのenumでは、CAPTURED_VARIABLEに該当するでしょうか。

fun unstable() {
    var x: Int? = 42
    run {
        if (x != null) {
            // `Int`にスマートキャストされる
            x.inc()
        }
    }
    // 相変わらず`Int?`
    x.inc()
}

Kotlin Playground: Edit, Run, Share Kotlin Code Online

逆に安定的であるケースはどのようなものがあるでしょうか?仕様書によれば、シンクの安定性は次のような条件で「安定的である」とみなされるようです。

  1. delegationないしはカスタマイズされたgetterのない、イミュータブルなローカルプロパティないしはクラスのプロパティ。
  2. 「実質的にイミュータブルである (effectively immutable)」とコンパイラが判定できるかつ、delegationないしはカスタマイズされたgetterのない、ミュータブルなローカルプロパティ。
  3. delegationないしはカスタマイズされたgetterがなく、かつ、現在のモジュール内で宣言されている安定的でイミュータブルなローカルプロパティのイミュータブルなローカルプロパティ。

イミュータブルなものは基本的に、一度宣言された場所以降で値が書き換わることはまずないため、フロー解析の結果を安定化させやすく、ゆえにシンクの安定性を立証しやすいというところでしょうか。いわゆるミュータブルな操作はあとで書き換わる可能性があるという性質から、あるスコープで絞った範囲のフローの解析を「そのスコープ内で」完結させるのが難しくなります。逆にイミュータブルであれば、スコープ関係なくそもそも書き換えが起こることはありえないため、フローの解析の結果を完結させやすいと言えます。[*3]もちろん、ミュータブルだった場合であっても、「実質的にイミュータブル」であるという条件を満たせる限りであれば、それはやはり安定的であるとみなしやすいというのは、想像に難くないと思います。

「実質的にイミュータブルである(effectively immutable)」なスマートキャストシンクは条件がありますが、それは仕様書のこの部分に記述されています。(あとで気が向いたら追記するかもしれない)

ループ中の扱い

ループ中にデータフロー解析が発散してしまった場合には、killDataFlowという命令が走ることがあるようです。この命令は要するにデータフロー解析を止めるために発せられるものです。データフロー解析が、たとえばループなどに差し掛かって解釈に手間取ってコンパイル時間が極端に伸びることを防止するために設けられています。

データフロー解析が発散してしまうケースとしては、たとえば一度も評価されずに終わるボディを持つループが挙げられます。一度も評価されずに終わるボディを持つは、フローのグラフを構築するのが困難になります。一度も評価が行われないため、そもそもグラフを構築できないからです。こうしたケースではスマートキャストの適用は伝播させず、型付けを諦めることになります。

が、一部のループにおいてはきちんとスマートキャストが走るように実装されているようです。具体的には、下記2つはスマートキャストがきちんと走り、型付けが正しく行われます。必ず1回以上は評価が走るボディを持つと考えられるためです。

  • while (true) { ... }
  • do { ... } while (condition)

スマートキャストはフローに依存します。したがって、一度以上ループが走ってくれて制御フローを構築できさえすれば、スマートキャストを走らせられるということです。逆に一度も処理が実行されず、フローを構築できないものはそもそもスマートキャストを起こすのが無理、ということを言っているだけだと思います。

スマートキャストの結果の結びつけ

たとえばaという変数に値を代入後、abという別の変数に代入したとします。abはKotlinでは同じ値を指し示しています。そして、b側にnon-nullチェックを加えます。bに対するnon-nullチェックが入っただけですが、aIntとして判定されています。これを、「abのスマートキャストの結果が結び付けられて(bounded)いる」といいます。

fun bounded() {
    val a: Int? = 42
    val b = a
    if (b != null) {
        a.inc()
    }
}

Kotlin Playground: Edit, Run, Share Kotlin Code Online

まとめ

Kotlinにおけるスマートキャストの概要と型システム上のメリット、発生条件などをまとめました。

*1:Kotlinの仕様書ではそう説明されています→Kotlin language specification。ちなみにですが、Kotlinも「限定的な」flow-sensitive typingの採用になっているようです。

*2:仕様書では語の定義がなく、突然登場する印象を持ちましたが。

*3:余談ですがこのあたりの議論はRustのライフタイム解析でもやはり同様のことが当てはまり、RustはShared XOR Mutabilityという戦略をとることにより、この解析の正確性を担保したのでした。

IntelliJ IdeaにIdeaVimを入れてNeovimとほぼ同じ動作をさせる

JavaScalaで仕事をしていたころは毎日使っていたIntelliJですが、いつの間にかRustで仕事するようになってまったく開かなくなりました。最近はNeovimで仕事をしており、そちらの方がもはや慣れています。

ところが最近Kotlinを使う必要が出てきたので、Neovimでいつも通りセットアップしたところ、kotlin-language-serverがあまり安定的に動作してくれませんでした[*1]。たとえばリネームをかけるとエラーを吐いて死ぬので、パッチを投げるというような状況です(執筆時点ではまだマージされていません泣)。

github.com

他にも不具合を見つけていたり、そもそも定義ジャンプ(Go To Definition)が私の環境では動作していないように見えるなど、結構不具合がまだ多めです。可能な限りパッチは投げたいと思って調査していますが、喫緊必要なのでIntelliJを使わざるを得ない、というわけです。

プライベートでは今後もNeovimを使い続けるであろう手前、IntelliJのショートカットキーとの頭の切り替えはたぶん難しいです。そういうわけで、IdeaVimをなんとかハックして、Neovimの環境に近い状況にできないかと考えました。

IdeaVim

IdeaVimはVimの設定ないしはキーバインドを反映できるJetBrains製品向けのプラグインです。

github.com

初期設定で.vimrcを読み込んでくれますが、.ideavimrcというファイルがあればそちらを優先的に読み込んでくれます。.ideavimrcを用意してJetBrains向けの設定を書いておきつつ、.vimrcをsourceして手元の設定も読み込ませておく、という使い方が無難そうかなと思っています。

Action List

エディタ内での操作は基本的にはIdeaVimの初期設定でなんとか間に合います。が、たとえばIntelliJのターミナルを開きたいとか、左にくっついてるファイルファインダーを呼びたいとか、そういった操作は初期設定だけだとIntelliJのデフォルトになってしまいます。Neovimを使用していた際は、たとえば<leader> thとか、<leader> eとかで開いていたので、これと同じキーバインドを割り当てたいわけです。

調べてみると、IntelliJには「Action List」と呼ばれる設定が存在することを知りました。たとえばここに載っているリストを見ていくと、ターミナルのオープンはActivateTerminalToolWindow、ファイルファインダーのオープンはActivateProjectToolWindowといった具合にです。これに対して、Vimでのキーバインドを設定すると、オリジナルのキーバインドIntelliJ上のアクションを呼び出すことができます。

あるいは、IdeaVimに「Track Action Ids」という機能があるので、これを利用してもAction Listを知ることができます。

オンにすると、ショートカットキーで何かを動作させると右下にアクションIDが表示される。

設定してみた

いわゆるリーダーキーは「space」に割り当てています。下記のように設定してみました。

let mapleader = " "

" Key mapping
nmap gi <Action>(GotoImplementation)
nmap gr <Action>(FindUsages)
nmap <leader>fa <Action>(GotoAction)
nmap <leader>ff <Action>(SearchEverywhere)
nmap <leader>fw <Action>(FindInPath)
nmap <leader>c <Action>(CloseContent)
nmap <leader>bc <Action>(CloseAllEditorsButActive)
nmap <leader>bC <Action>(CloseAllEditors)
nmap <leader>e <Action>(ActivateProjectToolWindow)
nmap <leader>la <Action>(ShowIntentionActions)
nmap <leader>ls <Action>(ActivateStructureToolWindow)
nmap <leader>lr <Action>(RenameElement)
nmap <leader>o <Action>(EditorEscape)
nmap <leader>th <Action>(ActivateTerminalToolWindow)
nmap <leader>q <Action>(HideAllWindows)
nmap <leader>/ <Action>(CommentByLineComment)
nmap [b <Action>(PreviousTab)
nmap ]b <Action>(NextTab)
nmap u <Action>($Undo)
nmap <C-l> <Action>(NextSplitter)
nmap <C-h> <Action>(PrevSplitter)

github.com

困った点としては、ファイルファインダーを表示できる<leader> eやターミナルを表示できる<leader> thは、キーバインドをしてしまうとトグル形式にはならないようで、Neovimでは一度キーバインドを押してもう一度押すと開いて閉じるわけですが、IntelliJは2度目は認識してくれず、開いたまま閉じませんでした。これは結構困るので、<leader> qで開いているツールウィンドウを全部閉じるように設定してみました。

一旦普段開発で利用しているものはだいたい設定できたと思います。当然ですがtelescopeはないし、lazygitはターミナルをわざわざ開いて起動する必要ありです。

まとめ

とりあえずほとんど同じキーバインドで動かせるようになったので、エディタの切り替え時に混乱することも少なくなりそうです💛

ところで、kotlin-language-serverに代わる何かを実装したくなってきました。ScalaのMetalsくらいを目標にちまちまがんばりたいかもしれません。

*1:Kotlinは公式がLanguage Serverを用意していません。JetBrains製で、IntelliJなどの自社製品を使ってもらうのが一番いいはずなので、戦略上理解はできますが、できれば使用するエディタはあまり縛っては欲しくないですね。