ここでいう「コンフリクト」とは、
「GitHubでpull req自分が出した変更箇所に対して、他の人のpull reqが先にmergeされてコンフリクトしてそのままmerge出来なくなったぁ〜」
というようなものですね。*1 GitHubでもgitでなくてもなんでもよいというか、現代の開発において大体同じだと思いますが。
また、思っているほど大規模でなくても、それなりに開発が活発である程度の人数が同時に開発していれば、多かれ少なかれ発生すると思います。
大抵のコンフリクトは、ほぼ無駄な追加の作業が発生するだけで、避けれるに越したことはないですよね?
(よほど大規模か開発活発でなければ)大抵のOSSではこのようなことを意識する機会はあまりないですが*2、割と普通の規模のプロダクト開発でも、この視点は地味に重要だと思います。
Scalaに限らない当たり前すぎる話もしますが、まぁまぁScalaに特有の話もします。
先に一覧で書いておくと
- デフォルトの開発用とは別のfeature branch気軽に作り過ぎない
- 同じファイルに書き過ぎない、ファイルは分割する
- importのコンフリクト避けるコツ
- ファイルの置き場所や命名の規約
- scalafmtの導入、設定
- scalafix使ったテクニック
あたりでしょうか。順に解説します
デフォルトの開発用とは別のfeature branch気軽に作り過ぎない
完全にScala特有ではない話なので詳細省略。これはそれぞれの開発方針によるというか、どうしても必要な場合はありますが、個人的には出来るだけ避けるようにしてますね
同じファイルに書き過ぎない、ファイルは分割する
これもScalaに限らないのですが、Scalaの言語仕様上、(Javaと比較すると)悪い意味でこれがいくらでも出来てしまう、という問題があります。 importあたりでも関連しますが、例えばJavaは
「1つのファイルには、トップレベルのpublicなclass(やinterfaceなど)は、1つしか定義不可能」
という仕様があります。ファイル名合わせる、という仕様もありますね。
もちろんJavaも、トップレベルでないものなら定義可能、publicでなければ可能、などの抜け道はありますが、これは(最初Javaやる人は面倒だと感じるかもしれませんが)大規模開発でコンフリクト回避という視点で言うと、言語仕様レベルで制限されているのは、考えれば考えるほど最高な可能性があります。
他の言語詳しくないので知らないのですが、他の言語ってこのあたりどうなってるんですかね?
必然性なく1つのファイルに複数のトップレベルclassを追加すると、コンフリクト観点でもそうですし、他にも以下のようなデメリットがどんどん生まれます
- import部分が無意味にコンフリクトしやすくなる
- ファイル名とclass名などが一致しないので、どこにどの定義が存在するか分かりにくい
- ファイル名とclass名などが一致しないので、ファイル名の命名に余計な多様性が生まれる
- 1つのファイル内部において、どのclass定義をどういう順番で定義、追加するのか?で、余計な多様性というか、考えることが増える
などなど。 もちろんScalaには
sealed
という「同じファイル内部からしか継承不可能」という言語仕様上の機能- コンパニオンオブジェクトは、必ず同じファイルで定義する必要がある
などはあるので、そういう場合は使えばいいというか、使うしかないのですが。
そういうのが全く関係ないのに、1ファイルに誰かが複数定義し始めて、 あとから参加した人も、なんとなく雰囲気でそこに追加していって、巨大なファイルが出来上がる、というパターンがありえます。
また、関連してpackage object内部になぜか色々を大量に定義してるパターンも稀に見かけますが、あれもやめましょう。 極論すると、package objectは謎のゴミ置き場になったりしがちなので、よくわからなかったら基本的にpackage object使用禁止くらいの強い態度で臨んだ方がいいかもしれません。
置き場所に困ったからといって、package objectに置くのではなく、適切な名前をつけたobjectを新規に作成してそこに置きましょう。objectに「適切な名前をつける」という行為をサボるのをやめましょう。
また、それ関連のscalafixを作ったことがあるので、ご活用ください
methodやclassの実装が、一定の行数以上長過ぎたら警告するscalafix rulehttps://t.co/C4NfQGoEPT
— Kenji Yoshida (@xuwei_k) July 29, 2022
methodやclassというか、任意のscalametaで識別可能なTreeで可能だから、こういうのはconfigにするよりも、そのままコピペして改変して使った方が使いやすそうだな
importのコンフリクト避けるコツ
scalafmtかscalafixで出来るだけ規定してしまって、余計な多様性が生まれないようにしましょう。
本来の変更と関係ないのにimportの順番やスタイルなどが変わって、それによってコンフリクト発生するのは、だいぶ不毛ですよね?
多少のリスクやトレードオフがありますが、scalafmtだけでやることも可能です。
例えば
rewrite.imports { sort = ascii groups = [[".*"]] }
をすると、強制的に全部ascii順でsortになると思います。
IntelliJ IDEAのデフォルト設定は、なぜか java
始まりのものを1行開けて分離したりしますが、そこは各自の好みで groups
部分やその他の設定を変えてください。個人的にはIDEAのデフォルトより全部asciiでsortしてしまうのが、ルールが単純で好きです。
ただし「多少のリスクやトレードオフ」とあるように、scalafmtは原理上、型を見ない見れないので、相対importがあるとscalafmt適用しただけで壊れる可能性があります。
これを導入して完全に強制sortする、ということは、相対importを諦める、禁止する、と同等のはずです。
また、細かい話ですが
import aaa.bbb.{ccc, ddd}
を
import aaa.bbb.ccc import aaa.bbb.ddd
に展開する設定に強制した方が、原理上はコンフリクトが減ると思います。その設定はscalafmtだと
rewrite.rules = [ExpandImportSelectors]
でしょうか?なぜ上記で減るか?というと
import x.{y1, y2, y3, y4, y5}
というものがあったときに
- import x.{y1, y2, y3, y4, y5} + import x.{y2, y3, y4, y5}
にする変更と
- import x.{y1, y2, y3, y4, y5} + import x.{y1, y2, y3, y4, y5, y6}
にする変更は、1行で書かれていたら普通コンフリクトしますよね?(gitやAIか何かなんでもいいけど、頭良くなって勝手にやってくれ!!!)
しかし
- import x.y1
import x.y2
import x.y3
import x.y4
import x.y5
と
import x.y1
import x.y2
import x.y3
import x.y4
import x.y5
+ import x.y6
なら、衝突しないはずです。
この、importを全部展開しておく記述は、まさにJavaの方式というか、Scalaで記述が短く書けるところが完全に裏目に出てる部分だと思います。 IDEが全く発達してない、使わないで手動で全部importを丁寧に書くような大昔や、絶対に一人でしか開発しないぞ!という場合ならいくらでも好きにすればいいですが、 コンフリクト避ける視点を最重視するなら、多少見た目が長くなっても、このくらい徹底的にした方が良いとなります。
scalafixに関しては、公式にOrganizeImportsというものがあり
https://scalacenter.github.io/scalafix/docs/rules/OrganizeImports.html
すごく大雑把にいってしまえば、上記で紹介したscalafmtと同じようなことができますが
- しっかり型を見るので、相対importをしっかり認識しつつ壊れないように展開してくれる機能がある
- しっかり型を見る必要があるので、semanticdb生成する必要があって、その分scalafmtと比較すると遅くなる
などのトレードオフがあります。どちらで頑張るか?はそこのトレードオフを認識しつつ、各自で決めましょう。 scalafmtとscalafixの両方でimport関連の設定をする場合は、それら両方の設定に整合性がないといけないので、設定の難易度が多少上がるとは思います。
scalafmtで出来ることを知らなかった(あるいは昔はできなかった?いつから出来るんだろう?)時に、あえてscalafixのSyntacticRuleでそれっぽいものを作ったことがあります。 (今となってはscalafmtでいい気がする?)
ファイルの置き場所や命名の規約
上記の「1ファイルに書きすぎるな!!!」的な話で実質触れましたし、これはコンフリクトしやすさも関連しますが、どちらかというと無駄な多様性を避けて、余計な命名や置き場所考えることに労力を使うのではなく、そこは規約で縛って思考停止して、他のもっと重要なことに頭を使いましょう。
的な観点の方が強いですね。
ある意味繰り返しになりますが、結局大規模開発でやっていくなら
- ファイル名とclass名は揃える
- ディレクトリの構造と、package名も揃える
というJavaと同じ慣習にした方が無難だと思います。大抵の開発ではそれになってるのに、ミスで微妙に間違ってることが割とどこでもあるので、それの検知のscalafixを作って、かなりよく使っています。
scalafmtの導入、設定
すでにscalafmtの話出してるので、順番が前後するというか今更感がありますが、無駄な多様性や無駄な変更、それによるコンフリクトを避けるためにscalafmtは導入してなかったらまず導入した方がいいですね。
とはいえ .scalafmt.conf
の設定を頻繁に変えるのも辛いので、最初に導入するとしたら、設定をある程度しっかり考えてからの方がいいとは思いますが。
2025年4月現在、Scala界隈のフォーマッターとしてはscalafmtは完全にデファクトスタンダードで、良くも悪くも他にほぼ選択肢ない、といっても過言ではありません。 *3
細かい設定によって、コンフリクトしやすさはそんなに変わらないことが多いというか、関連するものはあるにはありますが、細かい設定項目多過ぎるので、詳細は割愛します。
多少例を挙げるなら、例えば
val a = b val foo = bar
を
- val a = b + val a = b val foo = bar
と =
や、その他for式の <-
の位置や、build.sbt内部の %
など、設定によって実質任意のものを揃える機能があります。
これはだいぶ好みが分かれる問題で
- 揃ってると見やすいメリット
- 直接関係ない、ある意味で余計な場所も変更されるので、コンフリクトしやすくなるデメリット
などのトレードオフがあります。
あとはScala 2.12の途中から、末尾に ,
がつけられるようになりましたが、だいぶ細かい話ですが、これも一応全部強制付与の方がコンフリクトしにくいかも?といったポイントはあり得るかもしれません。
scalafix使ったテクニック
ここまで全部やったら、まぁ大体は十分だと思いますが、究極的には、コンフリクトしにくさのためというか無駄な多様性を避けるために、独自のscalafixを書く、という方法もありえます。
例えば、何かのエラーコードか、エラーメッセージ一覧なのか、id一覧なのか、あえて巨大な1ファイルにまとめて書く決まりだったとしましょう。
それの内部を強制的にsortしてることをチェックするscalafix、というのは実際に書いたことがあります。
それはそれでscalafixでなくても
- そもそもコードではなく別の設定ファイル的なものに書き、それのsortの保証はscalafixではなくテスト書く
- 無理やりリフレクションなどで取得してテストできるならそれでも良い?
などはあり得ますが。
ただし、上記の他の方法と比べて、scalafixの方が場合によっては即座にわかる、エラー箇所の表示がわかりやすい、といったメリットはあります。
あくまでそれは例ですが、scalafixを使うのはbug検出的な観点だけではなく、そういった視点でもscalafixを使えるようになると、だいぶ色々なことが出来て便利です。
他にも忘れてる細かい点などがあった気がしますが、現状思いつくものはこの程度でしょうか・・・?