?? Scala3 詳解 Context Abstractions

官方文檔詳見: https://docs.scala-lang.org/scala3/reference/contextual/index.html

[TOC]

Context Abstactions,抽象上下文,Scala3的核心。它可以顯著的節(jié)省代碼,并且提升 Scala3 的編程簡潔性和優(yōu)越性。也是Scala3 難學(xué)的核心點。它主要包括:

  • Given Instances
  • Using Clauses
  • Context Bounds
  • Importing Givens
  • Extension Methods
  • ...
    我們將一一進(jìn)行學(xué)習(xí)。

1.Given Instances

定義 Ord[T] trait

trait Ord[T]:

  def compare(x: T, y: T): Int

  extension (x: T) def less(y: T) = compare(x, y)  < 0
  extension (x: T) def great(y: T) = compare(x, y)  > 0

使用 given 實現(xiàn)

given intOrd: Ord[Int] with
  override def compare(x: Int, y: Int): Int =
    if x < y then -1 else if x > y then +1 else 0

given listOrd[T] (using ord: Ord[T]): Ord[List[T]] with
  def compare(xs: List[T], ys: List[T]): Int = (xs, ys) match
    case (Nil, Nil) => 0
    case (Nil, _) => -1
    case (_, Nil) => +1
    case (x :: xs1, y :: ys1) =>
      val fst = ord.compare(x, y)
      if fst != 0 then fst else compare(xs1, ys1)

As it mentioned above, 我們實現(xiàn)了 int 類型 和 list 類型的 given 實例。簡單測試一下:

  @main def test() =
    val a = 13
    val b = 18
    println(s"a>b ? ${a great b}")

    val as = List(1, 3, 5)
    val bs = List(2, 3, 5)
    println(s"as>bs ? ${as great bs}")

可以運(yùn)行成功。

given instances 還有另外一種寫法,即不帶 with。

case class IntOrd() extends Ord[Int] {
  override def compare(x: Int, y: Int): Int =  if x < y then -1 else if x > y then +1 else 0
}

case class ListOrd[T]()(using ord: Ord[T]) extends Ord[List[T]] {
  override def compare(xs: List[T], ys: List[T]): Int = (xs, ys) match
    case (Nil, Nil) => 0
    case (Nil, _) => -1
    case (_, Nil) => +1
    case (x :: xs1, y :: ys1) =>
      val fst = ord.compare(x, y)
      if fst != 0 then fst else compare(xs1, ys1)
}

given intOrd: Ord[Int]  = IntOrd()
given listOrd[T] (using ord: Ord[T]): Ord[List[T]]  = ListOrd()

比較 given...with 和 given= 語法,我們發(fā)現(xiàn),前者是給出實例的同時,實現(xiàn)該實例子。而后者是直接給出已經(jīng)實現(xiàn)的實例。

推薦寫法

1.將 extension 等信息定義在 trait 中。
2.在其他文件中提供 extension 中的方法實現(xiàn),使用 given 形式給出。
3.trait 定義和 given 實現(xiàn)實例在一個 package 下。

2.Using Clauses

函數(shù)式(Functional programming) 編程傾向于將大多數(shù)依賴關(guān)系表示為簡單的函數(shù)參數(shù)化。這樣簡單又高效,但有時會在定義函數(shù)時,會導(dǎo)致函數(shù)定義許多參數(shù),其中相同的值在長調(diào)用鏈中一遍又一遍地傳遞給下一個函數(shù)。
如何避免這種場景?我們可以使用 上下文參數(shù)(Context parameters)。
編譯器可以合成上下文參數(shù),而不需要我們在編寫代碼的時候顯示的去進(jìn)行傳遞。

比如下面這個例子:

def max[T](x: T, y: T)(using ord: Ord[T]): T =
  if ord.compare(x, y) < 0 then y else x

ord 是一個上下文參數(shù),它通過 using 語法描述, max 方法可以通過以下代碼進(jìn)行調(diào)用:

max(2,3)(using intOrd)

(using intOrd) 部分將 intOrd 作為參數(shù)傳遞給 max 方法。但是這個語法的重點是,上下文參數(shù) ord 可以被省略,我們也通常這樣去使用。所以一般來講我們會這樣調(diào)用:

@main def main() = {
  max(2, 3)
  max(List(1, 2, 3), Nil)
}

匿名上下文參數(shù) Anonymous Context Parameters

即上下文參數(shù)的name 可以被省略,例如以下代碼中,Ord[T] 沒有定義它的name,區(qū)別于上一節(jié)代碼如 using ord:Ord[T]

def maximum[T](xs: List[T])(using Ord[T]): T =
  xs.reduceLeft(max)

maximun 擁有一個上下文參數(shù)類型 Ord[T],它僅被當(dāng)作推斷參數(shù)傳遞給了 maximun 方法,而其參數(shù) name 被省略了。
通常來說,上下文參數(shù)要么以 (p_1:T_1) 方式出現(xiàn),要么以 (T_1) 方式出現(xiàn)。

類的上下文參數(shù) Class Context Parameters

如果通過添加 val 或 var 修飾符使上下文參數(shù)稱為類的成員,那么這個成員可作為 given instance 使用。

比較下面兩段代碼,

class GivenIntBox(using val givenInt: Int):
  def n = summon[Int]

class GivenIntBox2(using givenInt: Int):
  given Int = givenInt
  //def n = summon[Int] // ambiguous

類里的 given member 成員是可導(dǎo)入的,看以下例子:

val b = GivenIntBox(using 23)
import b.given
summon[Int]  // 23

import b.*
//givenInt // Not found

方法多個 using 語法

def f(u: Universe)(using ctx: u.Context)(using s: ctx.Symbol, k: ctx.Kind) = ...

應(yīng)用層熟會從左到右進(jìn)行匹配。

object global extends Universe { type Context = ... }
given ctx : global.Context with { type Symbol = ...; type Kind = ... }
given sym : ctx.Symbol
given kind: ctx.Kind

以下調(diào)用方式是標(biāo)準(zhǔn)的:

f(global)
f(global)(using ctx)
f(global)(using ctx)(using sym, kind)

以下調(diào)用方式會報錯,因為其缺少了 ctx

f(global)(using sym, kind)

Summoning Instances

通過 summon 方法召喚 given instance。

Predef 中的方法調(diào)用返回特定類型的 given 值。例如,Ord[List[Int]] 的給定實例由以下調(diào)用得到:

summon[Ord[List[Int]]]  // reduces to listOrd(using intOrd)

summon 方法被簡單地定義為一個擁有上下文參數(shù)上的(非擴(kuò)展)恒等函數(shù)。

疑問與總結(jié)

1.怎么使用方法上的 using XXX ?

  def fetch[T](using Ord[T]): Ord[T] = summon[Ord[T]]
  def fetch2[T](using ord: Ord[T]): Ord[T] = ord

using 的時候可以選擇傳遞參數(shù)變量和不傳遞參數(shù)變量兩種情形。如代碼中的 fetchfetch2

  • fetch 方法中因為沒有 Ord[T] 變量,通過 summon[Ord[T]] 召喚 Ord[T] 的 given 實例并返回。
  • fetch2 方法擁有 using 的 Ord[T] 的變量,直接返回變量 ord。

3.Context Bounds 上下文綁定

指一個上下文參數(shù),依賴于類型參數(shù),Context Bounds 就是表示將這個上下文參數(shù)綁定到類型參數(shù)上的一種簡寫。

def maximum[T: Ord](xs: List[T]): T = xs.reduceLeft(max)

:Ord 就是一個上下文綁定。在 maximum 方法上,依賴于類型參數(shù) T,則 Ord 即等價于表示 using Ord[T],而上述就是這種表示的一種簡寫。
從上下文綁定中生成的上下文參數(shù),在定義時,排在最后一個,舉個例子:

上下文綁定可以與子類型的綁定相結(jié)合。如果兩者都存在,則首先出現(xiàn)子類型綁定,例如:

def g[T <: B : C](x:T): R = ...

方法或類的類型參數(shù) T 上的類似 : Ord 的邊界表示使用 Ord[T] 的上下文參數(shù)。從上下文邊界生成的上下文參數(shù)在包含方法或類的定義中排在最后。例如,

以下是測試代碼:

def f[T:C1:C2,U:C3](x:T)(using y:U,z:V):R

該方法會擴(kuò)展為 =>

def f[T,U](x:T)(using y:U,z:V)(using C1[T],C2[T],C3[U]) :R

以下是測試?yán)?
定義 Bound.scala

trait Bound:
  def invoke(msg: String): Unit
end Bound

//def f[T: C1 : C2, U: C3](x: T)(using y: U, z: V): R


object Bound:
  given C1[Int] = C1[Int]()

  given C2[Int] = C2[Int]()

  given String = "15"

  given C3[String] = C3[String]()

  given V = V()

end Bound

測試:

import com.maple.scala3.ca.cb.Bound.given

object Main:

  def f[T: C1 : C2, U: C3](x: T)(using y: U, z: V): String =
    val msg = "test"
    summon[C1[T]].invoke(msg)
    summon[C2[T]].invoke(msg)
    summon[C3[U]].invoke(msg)

    println(s"y=${y}")
    summon[V].invoke(msg)
    z.invoke(msg)
    "Done."

  @main def test1(): Unit =
    f[Int, String](13)

end Main

4.Importing Givens

從其他包中引入 given 方法和普通方法的形式略有不同,我們將展開詳述。

定義一個 object A, 它包含不同方法和 given 修飾的方法。

object A:
  class TC
  given tc: TC = ???
  def f(using TC) = ???

我們使用 import A.* 只能 import A 中的普通方法和變量,無法引入 given 方法。通過 import A.given 可將 A 中定義的全部方法引入。

object B:
  import A.*
  import A.given

上述代碼可以簡化為:

import A.{*,given}
object B:

除了通過 * 和 given 等全量引入之外,也可以通過具體的方法名稱進(jìn)行引入。

好處

  • given 的范圍更好界定,我們可以清晰的知道,當(dāng)前 using 的對象是從哪個類 given 來的。
  • 只導(dǎo)入某個類的所有 given 實例,不導(dǎo)入這個類的其他方法和變量。這一點特別重要,因為 given 可以是匿名的,所以使用命名導(dǎo)入的通常方法是不切實際的。

根據(jù) Type 導(dǎo)入(Import)

given 可以是匿名的,所以有時根據(jù) name 來導(dǎo)入不太可行。因此 scala3 提供了通過 By-type 來進(jìn)行導(dǎo)入。按類型導(dǎo)入為通配符導(dǎo)入提供了更具體的替代方案。例如:

import A.given TC

它代表導(dǎo)入A 中所有符合類型 TC 的 given 實例,不屬于此類型的將不會進(jìn)行導(dǎo)入。可支持批量的各種類型的導(dǎo)入,例子:

import A.{given T1,given T3}

導(dǎo)入?yún)?shù)化的given實例,例如 Instances 下的這些 given 實例:

object Instances:
  given intOrd: Ordering[Int] = ...
  given listOrd[T: Ordering]: Ordering[List[T]] = ...
  given ec: ExecutionContext = ...
  given im: Monoid[Int] = ...

可以通過如下方式導(dǎo)入所有 Ordering 類型的 given 實例:

import Instances.{given Ordering[?]}

上述 import 會將 intOrd,listOrd 導(dǎo)入,但是 ecim 將不會導(dǎo)入。

by-type 和 by-name 可以同時使用,如果兩者都指向某個 given 實例,則 by-name 的優(yōu)先級高于 by-type。

import Instances.{im, given Ordering[?]}

未完待續(xù) ...

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容