Partially-Applied TypeとScala 3.6からのSIP-47 Clause Interleaving

Scalaにおいて、一部の型引数だけを明示して部分適用し、残りの型引数は推論させたい、というパターンが稀に存在します。

そういう場面に遭遇しない人は別に今あまり読む必要はないです。

typelevel/catsのドキュメントでは Partially-Applied Type と呼ばれています。

(余談 https://x.com/xuwei_k/status/1547054067880697867 )

https://typelevel.org/cats/guidelines.html#partially-applied-type

そして、2022年の時点でScala 3のPolymorphic Function Typeを使って短く定義出来る方法が以下で紹介されています。

qiita.com

さて、Scala 3.6から Clause Interleavingという、型引数の定義を分割して途中に記述することが可能になりました。

(正確にはもう少し前から実験的機能としては入っているが、正式な機能になったのが3.6から)

https://docs.scala-lang.org/sips/clause-interleaving.html

上記のPolymorphic Function Typeを使う方法では、実用上問題にならないかもしれませんが、原理上は、無駄にPolymorphic Functionが生成されて気持ち悪い問題がありました。

それが一応このClause Interleavingを使うと解決するはずです。

つまり

Scala 2

final class PurePartiallyApplied[F[_]](val dummy: Boolean = true ) extends AnyVal {
  def apply[A](value: A)(implicit F: Applicative[F]): OptionT[F, A] =
    OptionT(F.pure(Some(value)))
}
def pure[F[_]]: PurePartiallyApplied[F] = new PurePartiallyApplied[F]

Scala 3のPolymorphic Function

def pure[F[_]]: Applicative[F] ?=> [A] => A => OptionT[F, A] =
  [A] => (a: A) => OptionT(a.some.pure)

Scala 3のClause Interleaving

def pure[F[_]](using Applicative[F])[A](a: A): OptionT[F, A] =
  OptionT(a.some.pure)

javapすると以下のように普通の?メソッドになる

  public static <F, A> cats.data.OptionT<F, A> pure(cats.Applicative<F>, A);

実際の例

build.sbt

libraryDependencies += "org.typelevel" %% "cats-core" % "2.12.0"

scalaVersion := "3.6.1"

REPL

Welcome to Scala 3.6.1 (21.0.5, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
                                                                                                                                             
scala> import cats._ ; import cats.syntax.all._ ; import cats.data.OptionT
                                                                                                                                             
scala> def pure[F[_]]: Applicative[F] ?=> [A] => A => OptionT[F, A] =
     |   [A] => (a: A) => OptionT(a.some.pure)
     | 
def pure
  [F[_$1]]: (cats.Applicative[F]) ?=> [A] => (x$1: A) => cats.data.OptionT[F, A]
                                                                                                                                             
scala> pure[List](2)
val res0: cats.data.OptionT[List, Int] = OptionT(List(Some(2)))
                                                                                                                                             
scala> pure[Option]("x")
val res1: cats.data.OptionT[Option, String] = OptionT(Some(Some(x)))

また、これは偶然 (using Applicative[F]) が存在したのでいい感じになっていますが、以下のような定義は怒られます

Welcome to Scala 3.6.1 (21.0.5, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
                                                                                                                                             
scala> def foo[A][B](b: B) = ??? // Aだけ明示し、Bは推論させたい
-- Error: ----------------------------------------------------------------------
1 |def foo[A][B](b: B) = ??? // Aだけ明示し、Bは推論させたい
  |          ^
  |  Type parameter lists must be separated by a term or using parameter list

これは、だいぶダサい?ですが、DummyImplicitを挟むことによって回避可能です(もっと良い案ないの・・・???)

def foo[A](using DummyImplicit)[B](b: B) = ???

いかがでしたか?

ところで、このblog書いてる2024年11月初め時点で確かに3.6.1はリリースされていますが、3.6はリリースをミスして、まだすぐ使うには微妙なので、少なくとも3.6.2が出るまで本格的なものに使うのは控えた方がいいです

https://www.scala-lang.org/news/post-mortem-3.6.0.html