Slide 1

Slide 1 text

アルプでの Scala 3 移行 https://alp.connpass.com/event/239935/ Scala を使ったSaaS プロダクト開発の 裏側お見せします! 2022/3/4 1 / 66

Slide 2

Slide 2 text

twitter @xuwei_k github @xuwei-k blog https://xuwei-k.hatenablog.com 2 / 66

Slide 3

Slide 3 text

近況 相変わらず某社でScala 書く仕事してます もう3 年経った 家に引きこもっている 最近アルプで副業はじめた 2022 年2 月くらいから OSS 活動も地味に続けてるが、話すと時間なくな るので省略 例えばscalafix 頑張ったり blog1 blog2 3 / 66

Slide 4

Slide 4 text

副業? アルプ内の某知り合いに誘われる 「とりあえず最初は好きなことやってていいよ」 本当に好きなことやって過ごす 4 / 66

Slide 5

Slide 5 text

5 / 66

Slide 6

Slide 6 text

6 / 66

Slide 7

Slide 7 text

7 / 66

Slide 8

Slide 8 text

えっ、働きすぎ・・・!? 8 / 66

Slide 9

Slide 9 text

主にやっていること build の整理改善、依存アップデート リファクタ レビュー Scala 自体のアップデートやその準備 9 / 66

Slide 10

Slide 10 text

アルプのScala の現状 自分が入ったときには既にScala 2.12 から 2.13 移行がわりと進められていた それの残りを手伝って、そろそろ2.13 移行は終 わる? 10 / 66

Slide 11

Slide 11 text

アルプのScala の現状 AWS のEMR のApache Spark がまだ未対応なので いずれにせよ2.12 完全切り捨てが無理 2.13 終わらないうちから、勝手にScala 3 対応 やり始める(!?) 11 / 66

Slide 12

Slide 12 text

Scala version アップデート に向けてやること これは一般論。Scala 3 に限らない 1: 依存ライブラリ 調査と整理とアップデート 2: コンパイル通す 3: テスト通す 通常1 => 2 => 3 という順番だが 一部並列して実行可能 12 / 66

Slide 13

Slide 13 text

Scala version アップデート に向けてやること ある程度対応進んだら?あるいは最初から?ちゃ んとCI 用意しましょう そうしないと通らないコードが増えてしまうので 先ほどの手順実行時、古いversion でのbuild は引き続き維持 徐々に行えるようにするべき 新しいversion で本番で安定して動いたら、初 めて古い方のビルド消す? 13 / 66

Slide 14

Slide 14 text

Scala 3 の超基本 (2.13 と比較して) ソース互換はそれなりにある Scala 2 でオプション指定すれば3 で非推奨になるものが事 前にある程度わかる 関連scalafix 作ったりした バイナリ互換は微妙にある( 詳細は複雑) Scala 3 は2.13 のlibrary.jar に依存 macro, reflection は全く互換ない macro annotation やcompiler plugin も 14 / 66

Slide 15

Slide 15 text

依存ライブラリ調査 Scala 3 向けでリリースされてないものは何か? その中でmacro やreflection に依存しているも のは? メンテされてないものはやめることも検討? リリースされてなくても for3Use2_13 でコン パイルは通せるか? その方法で、とりあえず通せるものと、通せないものがある macro 使ってたり、その他特殊なケースはだめ 15 / 66

Slide 16

Slide 16 text

段階的にコンパイル通す テクニック CrossVersion.for3Use2_13 -Xignore-scala2-macros conflictWarning --no-warnings -Wconf 16 / 66

Slide 17

Slide 17 text

段階的にコンパイル通す テクニック libraryDependencySchemes -source:3.0-migration -Ykind-projector -Xsource:3 scalaBinaryVersion や CrossVersion.partialVersion による分岐 Scala version で分岐して一部のtest だけ skip 他にもいっぱいあった気がするが・・・ 17 / 66

Slide 18

Slide 18 text

CrossVersion.for3Use2_13 Scala 3 のバイナリがなくても2.13 のバイナリ を無理やり使う設定 最終的に本番で動かす時までには全て消したほう が望ましいが、とりあえず先にコンパイル試して いきたいなら有効 18 / 66

Slide 19

Slide 19 text

-Xignore-scala2-macros Scala 2 のmacro 呼び出しがあっても、とりあえず そこを??? 的なものにScala 3 compiler に置き換 えてもらって、無理やりコンパイルだけ通す 19 / 66

Slide 20

Slide 20 text

sbt のconflictWarning // `_3` と `_2.13` のものが混ざると依存解決時点でエラーになるが // Scala 3 の場合は一旦は警告のみにし、エラーにはしないようにする。 // TODO `_3` と `_2.13` のものが混ざった場合 // バイナリ互換が保証されず // 最悪実行時にエラーになる可能性があるので、 // 本格的にScala 3 移行するときまでには、 // この抑制の設定がなくてもビルド可能になるように修正する conflictWarning := { if (scalaBinaryVersion.value == "3") { // ウザかったらwarn より下げてもいいぞ! ConflictWarning("warn", Level.Warn, false) } else { conflictWarning.value } } 20 / 66

Slide 21

Slide 21 text

--no-warnings 文字通り、Scala 3 で警告を一切出さないオプ ション コンパイルエラーが大量過ぎる場合、警告修正は 後回しにしたく、邪魔でしかないので 21 / 66

Slide 22

Slide 22 text

-Wconf 警告内容によって柔軟に抑制その他色々できる Scala 2 でも3 でも使える --no-warning 付与しつつ、エラーがほぼ修正 終わったら、優先して直したい警告、一旦放置し たい警告、などに分けて順次修正するのに 22 / 66

Slide 23

Slide 23 text

libraryDependencySchemes Scala 2 でも発生するので3 に限った話ではない scala-xml やscala-parser-combinators あ たりがよく引っかかる? バイナリ互換が無いものが混ざってるかもしれな いが、とりあえず雑に最新を採用してしまう設定 にしたいなら以下のように設定 libraryDependencySchemes += "org.scala-lang.modules" %% "scala-xml" % "always" 最終的には本当にバイナリ互換あるのか?しっか り確認した方がいいぞ! 23 / 66

Slide 24

Slide 24 text

-source:3.0-migration デフォルトでエラーになってしまうものが、一旦警 告で済むようになる。 例: Function の括弧が必要な部分など 24 / 66

Slide 25

Slide 25 text

scala> val f = { x: Int => x } -- Error: ------------------------------------------------ 1 |val f = { x: Int => x } | ^ |parentheses are required around the parameter of a lamb |This construct can be rewritten automatically under -re 25 / 66

Slide 26

Slide 26 text

-Ykind-projector 完全に同一ではないが、Scala 2 のcompiler plugin であるkind-projector の機能を提供して くれるオプション 26 / 66

Slide 27

Slide 27 text

-Xsource:3 Scala 2.12 や2.13 の最新で設定可能 compiler がScala 3 の挙動に少し近くなる Scala 3 の一部のsyntax がScala 2 でも使用可 能になる 恒久的に設定しなくても、少し試してみると移行 準備にはなるぞ 27 / 66

Slide 28

Slide 28 text

ビルドファイルでversion による分岐 scalacOptions ++= { if (scalaBinaryVersion.value == "3") { Seq("-Ykind-projector") } else { Nil } } 28 / 66

Slide 29

Slide 29 text

ビルドファイルでversion による分岐 変数束縛する場合はさらにこれを Def.setting で包 むなど val deps = Def.setting( scalaBinaryVersion.value などのKey 使った定義 ) // 他のところ // `.value` つけて呼び出し libraryDependencies ++= deps.value 29 / 66

Slide 30

Slide 30 text

libraryDependencies ++= { if (scalaBinaryVersion.value == "3") { Nil } else { Seq(compilerPlugin( "org.typelevel" % "kind-projector" % "0.13.2" cross )) } } 30 / 66

Slide 31

Slide 31 text

Scala 3 の場合だけskip するのをtest class 単 位で設定 Test / testOptions ++= { if (scalaBinaryVersion.value == "3") { // 変更頻度が高い場合、 // これもconfig から読むようにすると良い? val excludeTestNames = loadScala3Config("exclude_tests Seq( Tests.Exclude(excludeTestNames), ) } else { Nil } } https://github.com/sbt/sbt/blob/v1.6.2/mai actions/src/main/scala/sbt/Tests.scala#L89 L98 31 / 66

Slide 32

Slide 32 text

sbt のproject 毎に徐々に 対応するためのテクニック 例: sbt のsub project が複数あって a1, a2: test まで全部通る a3: Test/compile は通るがtest 通らない a4: main 側のcompile だけ通る a5: main 側のcompile すら通らない この時sbt に渡すべき引数とは? 32 / 66

Slide 33

Slide 33 text

sbt \ "all a1/Test/compile a2/Test/compile a3/Test/compile a4/compile" \ "all a1/test a2/test" 33 / 66

Slide 34

Slide 34 text

https://github.com/sbt/sbt/blob/v1.6.2/main/ L42 sbt のshell > help all all + Executes all of the specified tasks concurrently. 34 / 66

Slide 35

Slide 35 text

アルプにおけるsbt のsub project は60 以上あ る 先ほどのsbt のall に渡す引数生成がだるい 次どこをコンパイル通せばいいのか?が分かりに くい 35 / 66

Slide 36

Slide 36 text

やりたいことや方針 どこがScala 3 対応済、未対応なのか?を可視 化したい できるだけ細かくやっていきたい sbt のall に渡す引数を半自動で生成したい 次にどのsub projct の対応をやればいいのか? がわかるようにしたい 36 / 66

Slide 37

Slide 37 text

とあるsbt plugin を使いつつ拡張した仕組み定 義 https://github.com/dwijnand/sbt- project-graph https://gist.github.com/xuwei- k/4469101194f6a192eb3a1c71444741ea 37 / 66

Slide 38

Slide 38 text

Scala 3 対応の現状 Spark 除いてtest コード含めてcompile は通り そうな状態 これからtest 通していく予定 実際に動かすには、結局ライブラリ待ちも多少あ る 出したpull req 60 個以上 38 / 66

Slide 39

Slide 39 text

Scala 3 関連で具体的にやっ たことをひたすら話していくよ + と- は変更行数です # のものはpull req 番号だが、資料作る都合で 付けただけ 39 / 66

Slide 40

Slide 40 text

依存ライブラリ系 全く使っていない依存ライブラリや設定削除 #8541 +0 -8 circe の依存整理 #8544 +1 -12 kind-projector の依存をCrossVersion.full にして最新に #8545 +7 -8 semanticdb の依存の書き方を変更 #8560 +9 -1 明示的な scala-java8-compat の依存の記述削除 #8599 +3 -4 意味がないcats の明示的依存削除 #8597 +0 -1 scala-parallel-collections を最新に更新 #8613 +1 -1 某library が色々厳しいので改変しつつ必要な部分だけ組み込み #8614 +290 -10 scala-parallel-collections の依存の書き方修正(Scala 3 準備) #8885 +2 -2 circe-generic-extras の依存定義を必要なところのみに移動 #9018 +9 -5 Scala 3 準備のために org.jetbrains annotations の依存追加 #8916 +10 -0 https://github.com/lampepfl/dotty/issues/13523 40 / 66

Slide 41

Slide 41 text

ビルド設定 wartremover 追加 #8641 +12 -0 wartremover 自体はむしろScala 3 未対応だが、Scala 3 で非推奨な機能の警 告出すのに使う build ファイル内でScala 3 の準備のための設定追加 #8790 +14 -1 implicit に型が書いてなかったら警告するscalafix rule 追加 #8948 +1 -0 ExplicitImplicitTypes というscalafix rule 41 / 66

Slide 42

Slide 42 text

コード修正 非推奨なscala.App をやめて明示的なmain 定義 #8665 +401, −234 公式ドキュメント 中途半端な?scala.App 書き換えscalafix procedure syntax 修正 #8668 +6 −5 wartremover で非推奨なscala.App を使用禁止に #8670 +4 -1 Scala 3 で動かないのでcirce のJsonCodec 全て削除 #8700 +1041 −395 CirceCodec 書き換えscalafix circe のJsonCodec マクロアノテーションの実装 呼び出し側と定義側で括弧の有無揃える #8768 +26 −27 Scala 3 で消えるdo-while を書き換え #8779 +3 -2 42 / 66

Slide 43

Slide 43 text

コード修正 Scala 3 に備えてkind-projector でのinfix やめる #8783 +6 -6 それ用のscalafix Scala 3 本体の議論 コンパイルエラーになる型パラメーターと同じtype member 削除 #8784 +0 -2 Scala 3 の準備のためにshepeless 使った3 で動かない部分を分割 #8791 +135 −109 shapeless 2 に依存した未使用のclass 削除 #8792 +0 -62 Scala 2.13 での警告修正( 呼び出しと定義の括弧の付与揃える) #8796 +5 -5 override してsub type を返している場合に型を明示(Scala 3 準備) #8857 +5 -5 export がScala 3 の予約語なのでバッククオートで囲う #8866 +6 -6 重複している必要ないimplicit 削除 #8871 +7 -7 scalatest のMatcher のimplicit def が名前指定でimport されてるのを修正 #8874 +2 -2 shapeless のHList の書き方修正(Scala 3 準備) #8880 +75 −72 パターンマッチ部分に型を書くと逆にエラーになる場合があったので型を消す #8886 +4 -5 43 / 66

Slide 44

Slide 44 text

コード修正 Function1 の引数に必ず括弧を付与 #8887 +49 −48 https://github.com/xuwei-k/scalafix- rules/commit/384782c38bc688eaa1acec57d30b2c2db3881391 Scala 3 で自動で導出されないのでUnit に対するテスト用のインスタンス明示的に追 加 #8890 +4 -0 cats.implicits をcats.syntax.all に置き換え #8908 +185 −213 https://github.com/typelevel/cats/issues/4138 Scala 3 bug 回避のためcompanion object のcase を消す #8910 https://github.com/lampepfl/dotty/issues/12919 case class ではないclass のEncoder でmacro 使うのやめる(Scala 3 準備) #8911 +13 −3 case class でないものに対するderving ができないので導出された型クラスインス タンスの使用やめる #8914 +4 -4 重複しているimplicit 削除 #8918 +1 -1 shapeless 2 依存でそのままでは動かないものを削除、書き換え #8953 +7 -51 使ってないのにcirce-generic-extras のConfig を生成している箇所を削除 #8961 +1 -5 44 / 66

Slide 45

Slide 45 text

scalikejdbc アップデート scalikejdbc のbatchByName でscala.Symbol 使っている箇所修正 #8723 +29 -29 scalikejdbc をupdate する準備として括弧削除 #8907 +113 −109 scalikejdbc のapply に括弧を付与(scalafix で) #8952 +247 −226 scalikejdbc を3.5.0 から4 にアップデート #8999 +25 −13 45 / 66

Slide 46

Slide 46 text

mockito-scala やめる #9006 +4 -13 #9007 +399 −395 #9004 +255 -32 46 / 66

Slide 47

Slide 47 text

ひたすらimplicit に型を付与 implicit に型書きつつvalue class に #8760 +12 −8 #8777 +167 −107 #8653 +37 -35 #8865 +20 -21 #8876 +43 -29 #8895 +44 -32 #8923 +41 -20 #8931 +49 -46 47 / 66

Slide 48

Slide 48 text

すぐに必須では無い? が警告修正系 object に対するfinal 削除 #8780 +2 -2 Unreachable Warning 修正 #8888 +0 -3 テスト内部の重複している type R 削除 #8915 +0 -1 #8964 +0 -1 null 以外でmatch する可能性がないパターンマッチのcase を消す #8955 +13 −67 48 / 66

Slide 49

Slide 49 text

sub type が返らない件 Welcome to Scala 3.1.1 (1.8.0_322, Java OpenJDK 64-Bit Ser Type in expressions for evaluation. Or try :help. scala> trait A | | class B extends A | | trait X { | def foo: A | } | | class Y extends X { | override def foo = new B // ここの型を省略するかどうか | } | | val y = new Y | | y.foo // Scala 3 ではB ではなくA 型でかえる。Scala 2 ではB 49 / 66

Slide 50

Slide 50 text

implicit を2 回書くの禁止 Welcome to Scala 3.1.1 (1.8.0_322, Java OpenJDK 64-Bit Ser Type in expressions for evaluation. Or try :help. scala> class A(a: Int)( | implicit b: String, | implicit val c: Boolean) -- [E015] Syntax Error: ---------------------------------- 3 | implicit val c: Boolean) | ^^^ | Repeated modifier implicit 50 / 66

Slide 51

Slide 51 text

HList の件でpull req に書いた説明 HList のapply は 「implicit parameter でScala 2 macro で生成されたもの(Generic) 」 を受け取っ ているので、このままではScala 3 でコンパイルが通らないため。 https://github.com/milessabin/shapeless/blob/v2.3.8/core/src/main/scala/shapeless/hlists.scal https://github.com/milessabin/shapeless/blob/v2.3.8/core/src/main/scala/shapeless/generic.sca このapply というのは、任意のcase class やTuple を受け取って、HList に変換するためのメソッドである。 可変長引数のように見えるが、これはTuple を渡している。 implicit parameter でScala 2 macro で生成 されたものに関しては、無理やり頑張ってScala 3 のmacro で生成するのは不可能ではないが、Scala 2 の implicit が勝手にスコープに入るため、もしapply 使い続けるならば、「Scala 2 と3 でソースコード分けつ つ、implicit を明示的に渡す」といった変なことをやらないといけないので、HList をそのまま書く方式に変 えた。 これは結局HList のapply で返ってくる結果そのものなので、compile 時の処理的にも実行時の処理的 にも、この方が処理することが減るはずである。 ある程度は以下のscalafix で自動修正したが、format が崩 れたりコメントが消えてしまったところやimport を手動で修正した。 51 / 66

Slide 52

Slide 52 text

shapeless 2 のHList をScala 3 で使う Before (Scala 2 のmacro 依存) HList(a, b, c) After ( とりあえずScala 3 で動く) a :: b :: c :: HNil 52 / 66

Slide 53

Slide 53 text

import scalafix.Patch import scalafix.v1.SyntacticDocument import scalafix.v1.SyntacticRule import scala.meta.Term class ShapelessHListApply extends SyntacticRule("Shapeless override def fix(implicit doc: SyntacticDocument): Patch doc.tree.collect { case t @ Term.Apply(Term.Name("HList"), args) => Patch.replaceTree( t, args.mkString("(", " :: ", " :: HNil)") ) }.asPatch } } 53 / 66

Slide 54

Slide 54 text

54 / 66

Slide 55

Slide 55 text

えっ、mockito-scala 辛すぎ・・・!? 55 / 66

Slide 56

Slide 56 text

mockito-scala core がscalatest に依存してしまってるので、 デフォルトではfor3Use2_13 で誤魔化してコン パイル通すだけでも厳しい! 待っていても対応される可能性は低い I've been pretty busy at work and had 0 time. Maybe someone in the community wants to step up? 56 / 66

Slide 57

Slide 57 text

デフォルト引数mock 問題 class X { def a(b: Boolean = true): Int = ??? } val x = new X x.a() 57 / 66

Slide 58

Slide 58 text

生成されるコード class X { def a(b: Boolean): Int = ??? def a$default$1: Boolean = true } val x = new X x.a(x.a$default$1) 58 / 66

Slide 59

Slide 59 text

mockito 使ったテストコード例 val x = mock[X] when(x.a()).thenReturn(3) // 途中省略 verify(x, times(1)).a() 59 / 66

Slide 60

Slide 60 text

生成されるコード val x = mock[X] when(x.a(x.a$default$1)).thenReturn(3) // 途中省略 verify(x, times(1)).a(x.a$default$1) // x の `a$default$1` を2 回呼んでるよ m9(^Д^) って怒られる 60 / 66

Slide 61

Slide 61 text

内部実装依存な力技。 これらのデフォルト引数扱 うだけなら、macro もreflection も必要なかった if ( realMethod.isInvokable && ( methodName.contains("$default$") || ExecuteIfSpecialised(methodName) ) ) i.callRealMethod() https://github.com/mockito/mockito- scala/blob/3f7dbfaac58/common/src/main/scala L27 61 / 66

Slide 62

Slide 62 text

shapeless はcase class でなくても勝手にいい感 じにやるがScala 3 だと普通には無理 https://github.com/milessabin/shapeless/blob def isCaseClassLike(sym: ClassSymbol): Boolean def isCaseAccessorLike(sym: TermSymbol): Boolean 62 / 66

Slide 63

Slide 63 text

scalikejdbc のapply 括弧の自動付与scalafix import scalafix.Patch import scalafix.v1.SyntacticDocument import scalafix.v1.SyntacticRule import scala.meta.Term class ScalikejdbcApplyParentheses extends SyntacticRule("S override def fix(implicit doc: SyntacticDocument): Patch doc.tree.collect { case a @ Term.Select( Term.Select(_, Term.Name("list" | "single" | "upda apply @ Term.Name("apply") ) if a.parent.forall(!_.is[Term.Apply]) => Patch.addRight(apply, "()") }.asPatch } } 63 / 66

Slide 64

Slide 64 text

type param とtype member 同じだと怒られる Welcome to Scala 3.1.1 (1.8.0_302, Java OpenJDK 64-Bit Ser Type in expressions for evaluation. Or try :help. scala> trait A[B] { type B } -- [E161] Naming Error: ---------------------------------- 1 |trait A[B] { type B } | ^^^^^^ | B is already defined as type B 1 error found 64 / 66

Slide 65

Slide 65 text

自作Scala 3 関連scalafix AddExplicitImplicitTypes AddLambdaParamParentheses CirceCodec ExplicitImplicitTypes KindProjector LambdaParamParentheses ObjectSelfType ReplaceSymbolLiterals Scala3ImportRewrite Scala3ImportWarn Scala3Placeholder ScalaApp 65 / 66

Slide 66

Slide 66 text

OSS での対応含め、細かいこと話そうとすればいく らでもある気がするけど、ひとまず終わり。 質問タイム? 66 / 66