Don't Repeat Yourself

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

『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くらいです。アカデミックな内容を読みこなすにはまだちょっと遠い、みたいなレベル感ですかね。

年初でアップデートした開発環境の話

いくつか記事を読んで、もう少しGitならびにGitHubの操作周りを便利にしたいと思ったので、いくつかアップデートをしてみました。追加したのは次のとおりです。

  • ghqの導入と、リポジトリfzfで探せるようにした。
  • ghコマンドをちょっと覚えた。
  • Orbstackをbrew installで追加できるようにした。
  • tmuxで新しいウィンドウを開いた際に直前で使用していたディレクトリパスで開くようにした。
  • miseを入れた。

ghqの導入と、リポジトリfzfで探せるようにした

これまで私の開発環境ではリポジトリ間の移動が結構大変だなと思っていました。ghqそれ自体にはいろいろ用途があるようなのですが、今のところはghq listを便利に使っています。

公式にはpecoと組み合わせてリポジトリ一覧を取得しつつ、カーソルを動かした先でEnterすると該当するリポジトリに移動できるというものが載っています。私はfzfの方を使っているので、fzf用にカスタマイズしました。このgistを参考にしました

github.com

exaを使っているので、ついでにexaが呼び出されてカーソルを動かした先のプレビューがツリーで表示されるようにしています。Ctrl+gを押すと出てきます。

実のところ仕事の方のPCではtmuxのウィンドウを仕事用のリポジトリに割り当てて行き来してるだけなので、仕事の方は困りません。時々ウィンドウに用意しているリポジトリ以外に移動する必要が出てきた時に活用しています。

ghコマンドをちょっと覚えた

全然使っていませんでしたがghqをBrewfileに登録する際に近くにghというのがいたので、せっかくだから使ってみようかと思った次第です。普段はlazygitでほとんど管理しているのでlazygitと組み合わせて使おうかなと思っています。lazygitのショートカットキーを割り当てて使うといいよというアドバイスを見かけたので、どこかで整理したいと思っています。

とりあえずPull Requestをターミナルを出ずに作成できるのが便利です。gh create prですね。ただPull Requestを作成する際、descriptionを書く時にNanoが開いてしまって困ったので、そこだけ微調整しました。Vimが開いて欲しかったので、

gh config set editor vim

で調整できました。

Orbstackをbrew installで追加できるようにした。

少し前まではたしか手元にダウンロードしかダメだったと思うんですが、Docker Desktopが重すぎて無理と思いOrbstackをインストールしようとしたところ、brew installできるようになっているのを見つけました。やったね!

tmuxで新しいウィンドウを開いた際に直前で使用していたディレクトリパスで開くようにした。

地味に不便だったため。この記事を参考にしながら修正しました

github.com

あと、水平分割は-で、垂直分割は|でできるようにしておきました。直感的だったので。

miseを入れた。

miseを導入してみました。少し前にrtxからリネームされたツールです。さまざまなenvをこれひとつで管理できるようになります。細かい機能はまだまだ開発途上な気はしますが、すでにある程度使える状態にはあると思います。

github.com

miseのおかげで結構な数のbrew installを飛ばせていることがわかるdiffです↓

github.com

そのほか

あとはこの記事を見ながら、xhを追加したりしました。exaのフォーク版ezaが出てるんですね。どこかで乗り換えなければと思いつつ、さまざまに設定したエイリアスを切り替えるのが面倒で重い腰が上がっていません…

zenn.dev

dieselでbatch upsertをするには

Rustのdieselでbatch upsertをやる方法について、検索してもなかなか苦労したのですごく簡単にメモしておきます。バックエンドはPostgresを想定しています。MySQLでもこれができるかはわかりません。

結論

diesel::insert_into(...).values(...).on_conflict(...).do_update().set(...)でいける。

excluded句を入れたい場合

たとえばcolumn_aというカラムをexcludedしたいとします。最後のsetの呼び出しのところで、.set(column_a.eq(excluded(column_a)))とすれば実装できます。複数カラムの場合、setにタプルを投げ込むと指定できます。たとえばcolumn_acolumn_bcolumn_cに対してexcludedしたい場合、

diesel::insert_into(...)
                .values(...)
                .on_conflict(...)
                .do_update()
                .set((column_a.eq(excluded(column_a)), column_b.eq(excluded(column_b), column_c.eq(excluded(column_c))))

のようにです。

複数カラム指定を楽にする

ただカラムの数が増えてくるとだんだん間違えます。そこで、このタプルの生成はマクロに任せてしまうといいと思います。次のようなマクロを書きます。

macro_rules! excluded {
    ( $( $column:expr ),* ) => {{
        use diesel::{upsert::excluded, ExpressionMethods};
        ($( ($column.eq(excluded($column))) ),*)
    }}
}

これを先ほどのsetに投げ込みます。

diesel::insert_into(...)
               .values(...)
                 .on_conflict(...)
                .do_update()
                .set(excluded! {
                    column_a,
                    column_b,
                    column_c
                })

これで、あとはコードが展開されて勝手にexcludedを含むタプルが生成されます。結構便利だと思うので、困ったなと思ったらぜひ使ってみてください。