Don't Repeat Yourself

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

『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などの自社製品を使ってもらうのが一番いいはずなので、戦略上理解はできますが、できれば使用するエディタはあまり縛っては欲しくないですね。

git commit --fixupを使いましょう

発端

ポストの前提がちょっとわかりませんが、レビュー後にforce pushされると、どこに修正を入れたのかわからないケースだと仮定します。プルリクエストがまだドラフト状態でのforce pushやrebaseで困るケースはそんなにないと思うからです。

git commit --fixup

このケースではgit commit --fixupが便利です。レビューで指摘が入ったコミットに対して--fixupをかけておき、レビュワーはfixupコミットの内容を確認します。レビュワーが確認してOKが出た段階で、git rebase -i --autosquashなどを使ってfixupコミットを元コミットにrebaseします。こうすることで、最終的に見えるコミットは非常にきれいなものになります。

fixup周りのひととおりのフローは下記が参考になると思います。どのコミットに対する修正なのか紐付けされるので、レビュー指摘事項の修正に関するコミットを新たに積み上げるより、効率よくわかりやすいと思います。

qiita.com

個人的な意見ですが、「レビュー指摘事項の修正」のようなコミットはあまり積み上げても情報量が少ない関係であまり嬉しいことはなく、fixupで綺麗に整理しておくのが無難だと思います。squash & mergeしている場合は別ですが…。

fixupコミットをrebaseし忘れる問題

一点問題があるとすれば、fixup!とついたコミットをrebaseして綺麗にし忘れる問題です。これをしてしまっては、情報量のほとんどないコミットログを何個も積み上げてしまいます。

こうした問題にはCIでの対処が有効です。GitHub Actionsを利用しているようであれば、下記のアクションが利用できます。

github.com

あるいは、「そのプルリクエストの中に含まれるコミットの中に登場するfixup!メッセージを含む行をカウントし、1以上だった場合はエラーとする」といったスクリプトを用意して対処する手もあります。

追記: lazygitだとかなり楽にできます

私は普段lazygitでコミット関係を管理しているのですが、lazygitもしっかり対応しています。

コミットログを楽に整形できるlazygitの紹介 | ランサーズ(Lancers)エンジニアブログ

勉強方法について

最近よく聞かれるのですが、実際のところ答えに困ったので普段何をしているかをメモしておこうと思います。自分語りです。前提として、筆者はソフトウェアエンジニアであり、ソフトウェアエンジニアとしてどうしているかという話をしています。

学び方

学ぶチャネルは学ぶ対象に完全によります。大別するとふたつかもしれません。

  • 文字媒体(技術書やドキュメント、チュートリアル)を読んで学ぶ。
    • 「大規模言語モデル」「TypeScript」のような大きなテーマを学ぶ際は、基本的に技術書を読んでいます。
  • YouTubeなどの動画を見て学ぶ。
    • 技術書やドキュメントを読んだ上で、特定のテーマについて具体的に知りたくなったときに利用しているかもしれません。
    • 最近だと、Neovimのセットアップについてよく海外のストリーマーの動画を見ていました。
    • 書籍やドキュメントからだけではイメージをつかむのが難しいものを学ぶ際に利用できます。
    • 数学やアルゴリズム系は読んだだけではわからないことが多いので、動画をよく利用していそうです。

大きな興味関心のあるテーマがわかっているものの、具体的にどういう要素があるのかわかっていないときには技術書を利用しているようです。たとえば「大規模言語モデル」や「TypeScript」などの関心ワードはわかっているものの、具体的にそれが何で、何ができるかといったことはわかっていない、という状態のとき、技術書を最初から読んでいくのは大いに役立つと思います。

余談ですが、学び方はひとによると思います。私のように視覚優位で言語優位なひとは、おそらく書籍等で学んでいくのがもっとも効率が良いです。一方で、ひとから話を聞くのが得意な聴覚優位なひともいると思います。そうした方は、Udemyなどの講義形式のものがよいと思います。自身がどちらが得意かは、たとえば学生時代に授業を50分なり90分聞き続けられていたか、それが苦でなかったかを思い出すとよいかもしれません。参考程度にですが、私は当時はそれが苦痛で、今でも座ってひとの話を聞くのが難しいです。

加えて視覚優位聴覚優位の話もそうですし、大人になってからの学び方は、そもそも大人になると脳の使い方が変わるので、学生時代のころとは変える必要があります。実は人間の脳は25歳ごろに完成をようやく迎えると言われており*1、30歳〜50歳くらいがもっとも脳力(?)のピークにあたるのだそうです。詳しくは下記の本などがおもしろいです*2

一方で私も完全に書籍のみで学んでいるかというとそういうわけでもなく、たとえば「他人の開発環境設定が気になる」とか、「このツールのこの機能ってどうやって使ったらいいのか」といった、特定のテーマについて具体的に知りたくなった際には動画を見る傾向にあるかもしれません。最近だと下記の動画がおもしろく、Amethystというツールをそこから知って入れたりなどしました。

www.youtube.com

加えて、数学系やアルゴリズム系は残念ながら一読しただけではイメージできないことが多く、人が図解しながら説明する様子を見ると理解が進むことが多いです。そのため、こうした分野を学ぶ際には動画を比較的早い段階で活用する傾向にあるかなと思います。たとえばグラフ理論のこのシリーズは、図がわかりやすくとても直感的でよかったです。

www.youtube.com

英語で学ぶのか日本語で学ぶのかについても記しておきます。たしかに英語で普段仕事をしている関係で、そんなに英語には抵抗はないのですが、書籍というか書き言葉になるとさすがに難しいと感じることが多いです*3。結局のところ第二外国語なので無理して英語で毎回学んではいません。邦訳のものがある限りは、概念の理解は日本語でやりきることを重視しています。動画やポッドキャストなどの会話については、そんなに文法が難しくないことから英語でそのまま受容してしまっていることが多そうです。最初から第二外国語で学び切るのはたしかにすごいことですが、時間は有限なので効率を考えて母国語でまずは概念習得をし、理解した段階で第二外国語でも、くらいのスタンスです。

学ぶ際に気をつけていること

濃淡をつける

完全ないしは細かく理解する必要があるものと、そうでないものとを濃淡つけて学んでいます。学ぶこと、学びたいことは多いのですが、人生はそれに比較するととても短いのです…。

  • それを「使いたい」ので、細かく理解する必要があるもの。
    • 仕事で直近使う技術や概念などは、理解の解像度を高めておく必要がある。
    • 書籍を読みながらNotionに読書ノートを取るなどして、できるだけ用語やその技術の概念構造が記憶に残るようにしている。
    • もしくは、その技術の学習に本当に時間を費やしたいと思っている場合。
  • そうでないもの。知識だけ欲しいもの。
    • 単に興味があるだけのもの。最近だとLLMとか。
    • 脳内に知識の地図(インデックス)を作ることを目的とする。

大事なことは濃淡をつけて学ぶことです。すべてを完全に理解していくのが理想的ではありますが、時間が無限にないとほとんど不可能でしょう。モチベーションの維持の問題もあります。社会人の場合、日々忙しく過ごしていると関心が次々別のことに移っていきます。それ自体はなんら悪いことではありません。そうした状況の中で、自身がとれる現実最適解を探すとよいと思います。

私の場合は仕事等で使用したいものは基本的にきちんと腰を据えて学ぶようにしています。仕事等で使う以上は高い理解の解像度でもって接する必要があると考えているためです。理解の解像度が高いとは、人に聞かれた際にその技術に関して一通り正しく漏れなく語れるようになることをいうかもしれません。たとえばその技術が取り組んでいる課題領域に対する理解そのものから始まり、複雑な内部構造をもつものはその内部構造への理解などといったところでしょうか。

逆に「そうではないもの」を学ぶ際は、あくまで知識の地図を拡充できればいいやくらいの気持ちで取り組んでいることが多いです。最近よい記事がありましたが、脳内にその領域のインデックスを作ることを目的としています。専門用語の名前とそれらの関係性くらいは頭に入っているが、世界史の問題集にあったような一問一答に答えられる程度であって、詳しく聞かれても答えられはしない、くらいのレベル感です。

levtech.jp

そもそも「そうでないもの」を学ぼうとする人は、世間的には体感そんなに多くなく、これはプラスアルファの話だと思います。他に趣味ややることがあるのであればそちらが優先されて然るべきです。私がたまたま好奇心が強い性格で、なんでも知りたいと思ってしまうために、このルートがあるだけなのです。できなくともなんら気にする必要はないのではないか、と私は個人的には思っています。

身体知を大事にする

知識の習得は思った以上に身体的な活動だと思います。チャンスがあれば身体を動かしながら学ぶようにします。ただここでは、ウォーキングやジムの電動サイクルに乗りながら書籍を読めといっているのではなく、たとえばコードの写経であるとか、手元に紙とペンを用意し、マインドマップなどの図に書き起こしながら読むということを「身体的」と言っています。

これについてはさまざまな研究があると思うので詳細は割愛しますが、人間は身体を通じて、周辺環境と関わりながら知識を習得していくようです。最近子育てをずっとしている関係で、子どもを見ているととくにそれを感じます。言語習得の瞬間などが際たる例でしょう。座って読んだだけですべてを理解し記憶できれば最も省エネで理想的ではありますが、一度見ただけですべてを瞬時に記憶できる人などでない限り、まず不可能でしょう。そこで役に立つのが身体的な経験を伴う学習であると私は考えています。ひとは、身体的な経験などを通じてちょっとずつ知識を内面化していくのだと思います。

そういうわけで、結構しっかり写経したり、複雑で理解が難しい箇所が出てきた際は図を起こします。Notionに読書ノートをとるのも、やはり身体的な知識の習得を意識してのものです。

時間がかかることを前提とする

成果や結論が出るのを急がない、ともいうかもしれません。

そもそも知識は1時間や2時間で身につくものではありませんし、積み重ねなければ増えていくことはありません。ある分野に関して、自分より理解があり知識があると感じられるひとたちは、そこにかけた時間が多いのです。時間をかけているからこそ、そこに辿り着いているのです。

また、1時間や2時間学んだとしてその場で学んだことを理解できていなくとも落ち込む必要はないと思います。たしか下記に示す本で昔読んだんですが、知識は(個人差はありますが)半年くらいかけてだんだん消化されていき、いつか「わかった!」が来ることがあります。そもそも知識や理解は環境や身体的経験と共に構築されるものですから、人生経験を積んでいるうちに理解が深まることが多々あるのです。

それくらいの時間軸で取り組むようにしています。

まとめ

何かを学ぶ際に何を考えているかをまとめてみました。なおいうまでもありませんが、常にやっているわけでもないです。「使いたい」側であっても、少し学んでみて「やっぱり深く学ばなくてもいいからとにかく使いたい」と思い、さらっと読んで済ませるだけのこともあります。まずまずそういう傾向にあるかな、という話を記しました。

*1:人間の脳を25歳くらいまで柔軟な状態に保っておくことで、環境の変化に長い期間対応できるようにし他の生物に対して競争優位に立つことで、生存してきたのではないかと言われています。ちなみにですが、精神疾患が15歳ごろ〜25歳ごろに多いのも、これが原因なのではないかと考えられているようです。詳しくは ヒトの発達の謎を解く (ちくま新書) | 明和 政子 |本 | 通販 | Amazon など。

*2:タイトルは死ぬほど怪しそうなんですが、最新の脳科学の知見などは知らない素人の意見ですが、そこまでひどい内容ではないと思います。むしろ、よく整理されており普通にいい本です。どうでもいいですけど、医師の書く本って、なんでプロフィールや実績をやたら強調するんでしょうかね。医学の知識の権威性はそこにあるわけではなく、普通に論文の引用数などで決まってくると思うんですが、それをベースとした本がなさすぎると思います。

*3:私の英語のレベルはCambridgeだとC1くらいです。アカデミックな内容を読みこなすにはまだちょっと遠い、みたいなレベル感ですかね。