takafumi blog

日々の勉強メモ

不安定度と求心性結合(依存性入力)と遠心性結合(依存性出力)について

最近 ソフトウェアアーキテクチャの基礎を読んでいるんですが、「不安定度」のところでやたらと引っかかったのでメモ。

まず求心性結合(依存性入力)、遠心性結合(依存性出力)について。
おそらく依存性入力、依存性出力と言い変えた方がわかりやすくはある。

求心性結合(依存性入力)

あるモジュール(クラス、関数、コンポーネントなど)に対して他のモジュールがどのくらい依存しているか。

考え方としては - 対象となるモジュールを変えたときに、変更されるになるモジュールがどのくらいあるか - クラス図で矢印を引いた時にモジュールがが矢印の先になる場合の矢印 - 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が本家で対応していないのでざっくり書いてみた。

Get notifications of Discussions in slack (or equivalent) · Discussion #2844 · github-community/community · GitHub

ただしインジェクション対応など適当なので、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側のBotchat:write の権限つけて、GitHubのsecretにtokenを入れてやる。

出力はこんな感じ

デザインを変更したい場合は、https://app.slack.com/block-kit-builder/ とかで blocks のところのデザイン作って書き換える。

そのうち本家で対応してくれるまでの間に合わせという事で。

github.com

scala3: Matchableとパターンマッチ

環境   Scala 3.1.2

scala3では AnyAnyVal & AnyRef の間に Matchable というタイプが追加された。

A First Look at Types | Scala 3 — Book | Scala Documentation

scala3_class_hierarchy

理由はこちら

The Matchable Trait

何が変わったのか?

この変更は ` immutable に定義したタイプがパターンマッチで不変性が破壊されてしまう事への対処として導入された。

簡単に言うとパターンマッチのセレクター、 C(_)_ :C のような、には Matchable が実装される必要がある。

一番影響がありそうなのは、型パラメータが Matchable を実装しないとパターンマッチにでWarningが出る事だろう。

例として以下のようなメソッドがある。

def matchableTest[T](t: T) =
  t match {
    case _: Int => println("Int")
    case _      => println("Other")
}

scala2であれば当然問題ない。しかし型パラーメーター TMatchable かどうかの保証がないため、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

つまり TT <: 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

が将来的には getClassgetInstanceOfMatchable に移動する可能性もあるらしい。

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)の削除で問題が発生するコードについて

環境   Scala 2.13.8  Scala 3.1.1

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)