不安定度と求心性結合(依存性入力)と遠心性結合(依存性出力)について
最近 ソフトウェアアーキテクチャの基礎を読んでいるんですが、「不安定度」のところでやたらと引っかかったのでメモ。
まず求心性結合(依存性入力)、遠心性結合(依存性出力)について。
おそらく依存性入力、依存性出力と言い変えた方がわかりやすくはある。
求心性結合(依存性入力)
あるモジュール(クラス、関数、コンポーネントなど)に対して他のモジュールがどのくらい依存しているか。
考え方としては
- 対象となるモジュールを変えたときに、変更されるになるモジュールがどのくらいあるか
- クラス図で矢印を引いた時にモジュールがが矢印の先になる場合の矢印
- class A extends B
だったら、対処のモジュールがBなら、AはBに依存しているので、BはAにからの依存性入力がある
- モジュールCをモジュールDが使っているなら、モジュールDはCからの依存性入力が存在する事になる
遠心性結合(依存性出力)
あるモジュール(クラス、関数、コンポーネントなど)がどのくらい他のモジュールに依存しているか
考え方としては
- 対象以外のモジュールが変更されたときに、対象となるモジュールにどのくらい変更が必要か
- クラス図で矢印を引いた時にモジュールがが矢印の元になる場合の矢印
- class A extends B
だったら、対処モジュールがAなら、AはBに依存しているので、Bに対して依存性出力がある事になる
- モジュールCをモジュールDが使っているなら、モジュールCはDに対して依存性出力が存在する事になる
不安定度
さて本題の不安定度。
これは - 求心性結合(依存性入力) = Afferent Coupling = Ca - 遠心性結合(依存性出力) = Efferent Coupling = Ce - 不安定度 = Instability = I
とすると
I = Ce / (Ce + Ca)
となる。
簡単に読み解くと、Caが多く、Ceが少ないほど不安定度Iは小さくなり、安定したモジュールといえる。
説明が特にないと何故って気がするんですが - Caが多い = 入力が多い = 対象モジュールを使っているモジュールが多い = 対象モジュールを変更する事が難しい - Ceが少ない = 出力が少ない = 対象モジュールが使っているモジュールが少ない = 対象モジュールは変更しやすい
と考えるっぽい。なるほど。
scala: jmhでreifnedのパフォーマンスを(適当に)調べてみる
環境 Scala 2.13.8
ちょっと気になってrefinedのPositiveなどに比べて、requireで契約的に正数を縛る場合、どのくらいのパフォーマンス差があるか調べてみた。
コードはだいぶ適当だが以下
package bench import cats.implicits._ import eu.timepit.refined._ import eu.timepit.refined.api._ import eu.timepit.refined.auto._ import eu.timepit.refined.numeric._ import eu.timepit.refined.types.numeric._ import scala.util._ import org.openjdk.jmh.annotations._ class JMHSample { @throws def convertPositiveInt(i: Int): Int = { require(i >= 0) i } @Benchmark def measureAgreementPosInt(): Either[String, Int] = { try { Right[String, Int](convertPositiveInt(1)) } catch { case t: Throwable => Left[String, Int](t.getMessage) } } @Benchmark def measureAgreementPosIntFPLike(): Either[String, Int] = { Try(convertPositiveInt(1)).toEither.leftMap(t => t.getMessage) } @Benchmark def measurePosInt(): Either[String, PosInt] = { RefType.applyRef[PosInt](1) } @Benchmark def measureIntRefinedPositive(): Either[String, Int Refined Positive] = { refineV[Positive](1) } }
結果
sbt:jmh-example> Jmh / run -i 3 -wi 3 -f1 -t 1 ... [info] Benchmark Mode Cnt Score Error Units [info] JMHSample.measureAgreementPosInt thrpt 3 365097651.422 ± 19080986.848 ops/s [info] JMHSample.measureAgreementPosIntFPLike thrpt 3 344466648.190 ± 20924629.667 ops/s [info] JMHSample.measureIntRefinedPositive thrpt 3 73142259.692 ± 16294716.321 ops/s [info] JMHSample.measurePosInt thrpt 3 78952274.799 ± 58359188.751 ops/s [success] Total time: 241 s (04:01), completed 2022/10/16 14:03:36
おおむね1/3~1/5くらいrefinedの方がパフォーマンスは劣化する。 とはいえ普通にアプリケーション実装するときとかは、I/Oとかと比べれば微細なレベルだと思うので、有用な時は使えばよいと思う。
github discussionのコメントをslackに投稿するaction
2022/07/24 現在、まだslack integrationが本家で対応していないのでざっくり書いてみた。
ただしインジェクション対応など適当なので、privateで信頼できるところ以外では使わない方がよい。
詳しくはこの辺参照。
https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions
で以下がYAML
# .github/workflows/discussions_comment_to_slack_api.yml on: discussion_comment: types: [created] jobs: on_comment: runs-on: ubuntu-latest if: github.event.comment # outputs: # formatted_body: ${{ steps.format.outputs.body }} steps: - name: 'format comment body & title' id: format run: | title='${{ github.event.discussion.title}}' body='${{ github.event.comment.body }}' # all newline convert to \n body=${body//$'\r'/'%0A'} body=${body//$'\n'/'%0A'} # escape double quote title=${title//\"/'%5C%22'} body=${body//\"/'%5C%22'} echo "title: $title" echo "body: $body" echo "::set-output name=title::${title}" echo "::set-output name=body::${body}" - name: 'request to slack' id: request run: | curl -X POST 'https://slack.com/api/chat.postMessage' \ -d 'token=${{ secrets.SLACK_API_TOKEN_FOR_CHAT_WRITE}}' \ -d 'channel=general' \ -d 'blocks=[ { "type": "section", "text": { "type": "mrkdwn", "text": "<${{ github.event.comment.user.html_url}}|${{ github.event.comment.user.login }}> commented <${{ github.event.comment.html_url }}|${{ github.event.discussion.number }}/discussioncomment-${{ github.event.comment.id }}>" } }, { "type": "divider" }, { "type": "section", "text": { "type": "mrkdwn", "text": "*POST* ${{ github.event.discussion.category.emoji}}" } }, { "type": "section", "text": { "type": "mrkdwn", "text": "${{ steps.format.outputs.title }} #${{ github.event.discussion.number}}" } }, { "type": "section", "text": { "type": "mrkdwn", "text": "*COMMENT :speech_balloon:*" } }, { "type": "section", "text": { "type": "mrkdwn", "text": "${{ steps.format.outputs.body }}" } } ]'
後は、Slack側のBotに chat:write
の権限つけて、GitHubのsecretにtokenを入れてやる。
出力はこんな感じ
デザインを変更したい場合は、https://app.slack.com/block-kit-builder/ とかで blocks
のところのデザイン作って書き換える。
そのうち本家で対応してくれるまでの間に合わせという事で。
scala3: Matchableとパターンマッチ
環境 Scala 3.1.2
scala3では Any
と AnyVal
& AnyRef
の間に Matchable
というタイプが追加された。
A First Look at Types | Scala 3 — Book | Scala Documentation
理由はこちら
何が変わったのか?
この変更は ` immutable に定義したタイプがパターンマッチで不変性が破壊されてしまう事への対処として導入された。
簡単に言うとパターンマッチのセレクター、 C(_)
や _ :C
のような、には Matchable
が実装される必要がある。
一番影響がありそうなのは、型パラメータが Matchable
を実装しないとパターンマッチにでWarningが出る事だろう。
例として以下のようなメソッドがある。
def matchableTest[T](t: T) = t match { case _: Int => println("Int") case _ => println("Other") }
scala2であれば当然問題ない。しかし型パラーメーター T
は Matchable
かどうかの保証がないため、scala3ではWarningになる。
sbt:scala3> compile [info] compiling 1 Scala source to /path/to/target/scala-3.1.2/classes ... [warn] -- [E165] Type Warning: /path/to/src/main/scala/Main.scala:7:11 [warn] 7 | case _: Int => println("Int") [warn] | ^^^^^^ [warn] | pattern selector should be an instance of Matchable,, [warn] | but it has unmatchable type T instead [warn] one warning found
つまり T
を T <: Matchable
としてやる必要がある。
// OK def matchableTest[T <: Matchable](t: T) = t match { case _: Int => println("Int") case _ => println("Other") }
またこの問題は 型 C[T]
をパターンマッチした時も発生する
case class Foo[T](val t: T) def matchableTest2[T](foo: Foo[T]) = foo match { case Foo(i: Int) => println(i) case Foo(_) => println("other")
sbt:scala3> compile [info] compiling 1 Scala source to /path/to/target/scala-3.1.2/classes ... [warn] -- [E165] Type Warning: /path/to/src/main/scala/Main.scala:16:18 [warn] 16 | case Foo(i: Int) => println(i) [warn] | ^^^ [warn] | pattern selector should be an instance of Matchable,, [warn] | but it has unmatchable type T instead [warn] one warning found
また余談だが、 scala2 だと List[Any]
となる以下のような場合 scala3 だと List[Matchable]
になる
scala> List(1, "a") val res1: List[Matchable] = List(1, a)
Matchable
のメンバ
現状 Matchable
はメンバを持たない
abstract class Any: def isInstanceOf def getClass def asInstanceOf // Cast to a new type: myAny.asInstanceOf[String] def == def != def ## // Alias for hashCode def equals def hashCode def toString trait Matchable extends Any class AnyVal extends Any, Matchable class AnyRef extends Any, Matchable
が将来的には getClass
や getInstanceOf
は Matchable
に移動する可能性もあるらしい。
Matchable is currently a marker trait without any methods. Over time we might migrate methods getClass and isInstanceOf to it, since these are closely related to pattern-matching.
-source:future
とscala2互換性
と説明を書いたが、この機能はデフォルトではscala3でも互換性を考慮しWarningを出さない。
scalacOptions
に -source:future
を追加する事で有効になる。
Scala3で弱適合性(= Week Conformance)の削除で問題が発生するコードについて
scala3で削除される機能の一つに「弱適合性(= Week Conformance)」というのがある。 docs.scala-lang.org
しかし説明を読み、コードを書いてみても実際に何が問題になるのかが最初よく分からなかった。 ここではscala2からscala3への移行、もしくはそれを前提としてscala2のコードを書くとき、で問題が発生しそうなパターンを書き残しておく。
そもそも弱適合性とは?
弱適合性についてをそもそも理解したい場合はscala2のREFLECTION シンボル、構文木、型#弱適合性関係 を参照。
削除されるものなので細かくは説明は書かない。
問題と解決方法
結論
先に結論を書くと、アプリケーションのレベルであれば、変数や関数に返り型を記述してあれば問題は発生しない
実際のコード
scala2とscala3で同じように書いて、scala3で問題が出るのは以下のようなコードになる。
// scala2 val d = if (true) 1f else 1d val d2: Double = d
// scala3 val d = if (true) 1f else 1d val d2: Double = d // Error
[error] -- [E007] Type Mismatch Error: /path/to/Main.scala:4:21 [error] 4 | val d2: Double = d // Error [error] | ^ [error] | Found: (d : AnyVal) [error] | Required: Double
上記のような場合は Float
はコンパイル時に自動変換されず AnyVal
として扱われるためエラーとなる。
しかしscala3のドキュメントに
Therefore, Scala 3 drops the general notion of weak conformance, and instead keeps one rule: Int literals are adapted to other numeric types if necessary.
とある通り Int
の場合は Double
へ自動変換されるためcompile可能である。
// scala3 val d = if (true) 1 else 1d // from 1f to 1:Int val d2: Double = d // OK
また、次のように関数に返り型が明示されている場合は自動変換され、コンパイルを通過する。
これは Float
から Double
のように精度が下がらない型変換は暗黙に定義されるので許容される
https://dotty.epfl.ch/api/scala/Float$.html#float2double-72d
// scala3 val d: Double = if (true) 1f else 1d // <- Add return type `Double` val d2: Double = d // OK
つまり複数の数値型が一つの式の中に存在した時、明示的に型指定をしている場合は、暗黙で変換される。
(ただし高精度から低い精度への変換は定義されていないためエラーとなる)
しかし明示的に型を指定しない場合は、 Int
以外は AnyVal
として扱われてしまい、その後の特定の数値型として呼び出すと AnyVal
から数値型への変換は暗黙変換が定義されていないためエラーになる。
結論
つまるところ、返り型を常に書いておけば基本的にこの機能削除による影響は受けないと思われる。
またそれ以外の場合も AnyVal
からの変換が不可能であるというエラーがcompile時に発生するため、修正せずに妙な動きをするという可能性は低い。
・・・のではないかと考えられる。
余談
scala3ドキュメントに書いてる List
の例も、返り型をきちんと書いておけば問題なくcompileできる。
NG
// scala3 def main(args: Array[String]): Unit = val ls: List[Double] = foo() def foo() = // NG: No return type val n: Int = 3 val c: Char = 'X' val d: Double = math.sqrt(3.0) List(n, c, d)
[error] -- [E007] Type Mismatch Error: /path/to/Main.scala:3:30 [error] 3 | val ls: List[Double] = foo() [error] | ^^^^^ [error] | Found: List[AnyVal] [error] | Required: List[Double]
OK
// scala3 def main(args: Array[String]): Unit = val ls: List[Double] = foo() def foo(): List[Double] = // OK val n: Int = 3 val c: Char = 'X' val d: Double = math.sqrt(3.0) List(n, c, d)