For download of Scala 3, see Scala 3 (epfl.ch).

Scala 3’s compiler code name is Dotty. In general, the changes to Scala 3 can be summarized as follows:

  1. Code writing without parentheses is supported.
  2. The type system has been greatly enriched.
  3. Improvements in Scala 2implicitThe problem of flooding.

Running Scala 3 requires at least JDK 8 or later support. In addition, the actual IntelliJ IDEA of 2018 version did not recognize the SDK of Scala 3, so the author upgraded the IDE and its plug-ins to the 2020 version.

The major new features in Scala version 3 are described here with some reservations. Complete preview to see the official document: Scala 3 Syntax Summary | Scala 3 Language Reference. There is a lack of Chinese information about Scala 3. In order to avoid ambiguity, some titles are followed by English nouns from the official website.

Scala 3’s various syntax enhancements are based on Scala 2, so users need to have a basic understanding of Scala’s own syntax. In addition, in the following, methods defined by def are called methods, and expressions defined by val/var are called functions.

Grammar to improve

Syntax Optional Braces without parenthesis indentation

Scala 3 also introduces the no-parenthesis indentation syntax, theoretically omits all {} from code, and users can now write Scala code in both writing styles.

// Scala 2-style code:
object Obj {
  def f() : Unit = {
    println("Welcome to scala 3.")}}// Scala 3 style code:
object Obj :
  def f() : Unit =
    println("Welcome to scala 3.")
  end f
end Obj
Copy the code

Once you choose to write without parentheses, pay special attention to the indentation of each line of code. End XXX is not required, so Scala 3 code can also be formatted in the following format:

object Obj01 :
  def f01() : Unit = 
    println("Welcome to scala 3.")
  def f02() : Unit =
    println("Welcome to scala 3.")

object Obj02 :
  def g01() : Unit =
    println("Welcome to scala 3.")
  def g02() : Unit =
    println("Welcome to scala 3.")  
Copy the code

Control statements can also be rewritten without parentheses, as in Scala 3, if blocks and for blocks can be written as follows:

// if block in scala 2
if() {} else {} 

// if block in scala 3
if. then ...else. endif

// for block in scala 2
for() {}// for block in scala 3
for. do ... endfor
Copy the code

In addition, the Scala 3 official website also gives a debate about “Spaces vs Tabs”, see: Optional Braces | Scala 3 Language Reference | Scala Documentation (scala-lang.org). It’s a good programming practice not to mix Spaces and indentation in the same source file.

Variable parameter splicing

In Scala 2, if you want to “unpack” an array or sequence and pass it into a method’s mutable argument list, you need to express it as:

val xs: Array[Int] = Array[Int] (1.2.3.4.5)

def f(xs : Int*) :Unit =  xs.foreach(println(_))

f(xs : _*)
Copy the code

Scala 3 allows for a more concise expression (similar to *xs in Groovy) :

f(xs*)
Copy the code

On the other hand, the binding symbol @_ * for pattern matching:

val xs : Array[Int] = Array[Int] (1.2.3.4.5)
xs match {
    case Array(_,_,others @ _*) => others.foreach(println(_))
}
Copy the code

It is now possible to use a succinct * instead:

val xs : Array[Int] = Array[Int] (1.2.3.4.5)
xs match {
    case Array(_,_,others*) => others.foreach(println(_))
}
Copy the code

Global apply method

The Apply method, known by Scala as the Constructor agent Proxies, can be initialized by classes that implement the Apply method without the new keyword. In Scala 2, the compiler automatically provides the Apply method for case classes marked as sample classes.

class Person(var age : Int,var name :String)

// The essence of the omission of the new keyword is that the compiler compiles and constructs a factory method with the same name
val p2 = Person(19."Jane")  
Copy the code

In Scala 3, the Dotty compiler provides apply methods for all classes, so the new keyword is less important. However, the new keyword may not be omitted in some cases, such as when creating an object with dynamically mixed attributes:

val o = new aClass with aTrait with bTrait
Copy the code

Infix specifies infix expressions

The concept of infix, prefix, and postfix expressions was introduced experimentally in Scala 2. See: Scala: Implicit conversions and custom operators – nuggets (juejin. Cn). In Scala 3, the infix keyword is recommended for methods named by combinations of letters and numbers to be treated as infix expressions.

case class Box(var v : Int){
  // Symbolic Operators do not need to be displayed with the infix keyword.
  def +(that : Box) : Box = Box(this.v + that.v)
  
  // Alphanumeric operator recommends adding the infix keyword.
  infix def sub (that : Box) : Box = Box(this.v - that.v)
}
Copy the code

Improper use of infix characters may now be compiled warning of depredated. Functions marked as infix symbols can have only one argument.

In addition, Infix can be used for type definitions. Such as:

infix type to[X.Y] = (X.Y)
val e : Int to String = 1 -> "1"
Copy the code

@ the main annotations

Scala 3 introduces a new @main annotation that allows users to mark the methods of any singleton object as program entry.

object Obj:
  @main
  def test() :Unit=???Copy the code

On the other hand, the program entry parameter list can be precisely set based on the actual content, rather than binding all inputs to args: Array[String].

// When the input is not normal, the program will prompt:
// Illegal command line after XXX arguments: java.lang.NumberFormatException: For input string: xxx
@main
def test(a : Int, b : Int, c : Int) :Unit = println(a + b + c)
Copy the code

Assign a method to a function

Scala 3 allows methods defined by def to be assigned directly to a function expression, officially called Eta Expandition. The term comes from the Lambda calculus, see: What is Eta-expansion? – zhihu.

def aMethod(a :Int , b : Int) : Int = a + b

// scala 3
val aFunction: (Int.Int) = >Int = aMethod
Copy the code

In Scala 2, you need to carry an extra _ symbol to do this:

// scala 2
val aFunction: (Int.Int) = >Int = aMethod _
Copy the code

Improved top-level declaration

Scala 3 now supports top-level definition of variables and functions, which means they are “exportable.” As a result, Scala 3 no longer recommends using Scala 2’s package variables to define global content. The official website revealed that the concept of package variables will be outdated and Dropped in the near future: Package Objects | Scala 3 Language Reference | Scala Documentation

package myTestForScala3

// This type of declaration belongs to the top-level declaration.
var a = 1
var b = 2
def f() : Unit = ()

object Test :
  @main
  def test() =

    g(1) (2) ("3")
    g(1) (2) (3)

end Test
Copy the code

Improved import writing

Scala 3 now provides additional support for the * symbol to indicate “wildcard” when importing packages, consistent with Java writing.

// Scala 2:
import java.lang._

// Scala 3:
import java.lang.*
Copy the code

Introduce all but one component:

// Scala 2:
import java.lang.{Integer=> _, _}// Scala 3:
import java.lang.{Integer as _ , *}
Copy the code

Also, Scala recommends using as instead of => when renaming is introduced.

// Scala 2:
import java => J

// Scala 3:
import java as J
Copy the code

Opaque Type: Opaque

With the additional opaque modifier, a new abstract type is constructed externally.

opaque type Logrithm : Double
Copy the code

The opaque type cannot be observed. The following is an example:

object MathUtil :
  Dsmt4 is an abstraction of Double, it's opaque.
  Dsmt4 = log(x) (base E).
  opaque type Logarithm = Double
  object Logarithm :
    def apply(d : Double) : Logarithm = math.log(d)

  Embed EQUATION. Dsmt4. Dsmt4. Dsmt4.
  Dsmt4 is computed by Double.
  extension (ths : Logarithm)
    // logM + logN = log(MN)
    Dsmt4. Dsmt4. Dsmt4. Dsmt4. Dsmt4. Dsmt4.
    def + (that: Logarithm) :Logarithm = ths + that


object B :
  import MathUtil. *Dsmt4 is not compatible with Double.
  Dsmt4. // All operations of type Double are not dSMT4.
  Dsmt4: dSMT4. Dsmt4. Dsmt4
  
  val v1 : Logarithm = Logarithm(math.E)
  val v2 : Logarithm = Logarithm(math.pow(math.E.3))

  @main
  def test11() : Unit =
    
    // log(e) + log(e^3) = log(e^4) = 4
    println(v1 + v2)
Copy the code

Dsmt4 is viewed as the type alias of Double only inside MyUtil. Dsmt4. Dsmt4. Dsmt4. Dsmt4. Dsmt4. Dsmt4. Dsmt4. Dsmt4. Dsmt4. Dsmt4. Dsmt4. Dsmt4.

Currified function overloading

Changes in Overload Resolution | Scala 3 Language Reference | Scala Documentation (scala-lang.org)

Scala 3 now supports overloading of Corrified functions. Here’s an example:

// Syntax errors are reported in Scala 2:
def g(a : Int)(b : Int)(c : String) : Int = 0
def g(a : Int)(b : Int)(c : Int) : Int = 0

g(1) (2) ("3")
g(1) (2) (3)
Copy the code

Note, in particular, that Scala 3 requires top-level declarations for overloaded functions.

Quality parameters

Attributes can now carry a list of parameters, and parameters modified by var are treated as attributes of the trait.

// IP is treated as a TCP property.
// trait TCP(var ip : String){}

// IP is treated only as a normal parameter.
trait TCP(ip : String){
  def ipconfig = s"ip : ${ip}"
}

// Arguments can be passed through the class's main constructor.
class Device(ip : String) extends TCP(ip)

// Can be assigned directly.
class VM(ip : String) extends TCP("192.168.3.140")
Copy the code

Thousands separator

In foreign countries, the unit is thousand, for example, thousand (1K for users) or 1,000,000 (1 MIO for short). For some long-value literals, Scala 3 introduces _ as the thousands separator, to improve readability.

// 1000 => can be expressed as 1_000
print (1_000 = =1000)
Copy the code

Literal type

Scala 3 supports declaring numeric values and strings directly as a type. Such as:

// Declare the literal type
val NOT_FOUND : "404" = "404"
val OK : "200" = "200"

/ / "404" | "200" said code to receive the "404" or "200" literal.
// See intersection type below.
def state(code : "404" | "200") : Unit = ()

state(NOT_FOUND)        	 // ok
state(OK)			// ok
state("200")			// ok
state("500")			// error
Copy the code

type

Intersection Type

The intersection type here refers to the intersection between attributes (∩\ Cap ∩) because Scala, like Java, is single-inherited.

class Person
trait Jumper {def jump() : Unit = ()}
trait Runner {def run() : Unit = ()}

val person1 = new Person with Jumper with Runner
val person2 = new Person with Jumper
val person3 = new Person with Runner

// p is the intersection type of the two qualities.
def check(p : Jumper & Runner) : Unit = ()

capabilityCheck(person1)	// ok
capabilityCheck(person2)	// error
capabilityCheck(person3)	// error
Copy the code

You can cross any number of trait types. An intersection type can be viewed as an unordered set of trait types, so it has nothing to do with the mixing order of traits, such as:

val person1 = new Person with Jumper with Runner
val person2 = new Person with Runner with Jumper

def check(p : Jumper & Runner) : Unit = ()
check(person1)  // ok
check(person2)  // ok
Copy the code

Union Type Union Type

Union is expressed in Scala as Union \cup∪, not Union. There are two types of expression: “A or B”.

// This variable takes Account or Email for authentication.
var idntfy : Account | Email = Account("ljh2077")

// The function inf allows you to accept Account or Email.
def verify(inf : Account | Email) :Unit ={
    inf match {
        case Account(username) => println(username)
        case Email(address) => println(address)
    }
}
Copy the code

You can combine any number of types. Has nothing to do and type and type of the order, such as equivalent to B | A | B.

Dependent Function Types

Scala 2 allows def to define Dependent Methods. The following is the description of the Dependent method and Dependent function in the official example.

trait Entry { type Key; val key: Key }

def extractKey(e: Entry): e.Key = e.key          // a dependent method

val extractor: (e: Entry) => e.Key = extractKey  // a dependent function value
/ / ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^
// a dependent function type
Copy the code

The reason for calling dependent methods is that e. key above is a path-dependent type associated with E. But until Scala 3, its dependent methods could not be converted to dependent functions because e.key had no concrete type to describe it (note that there is a semantic difference between the path type and the projection type Entry#Key). Scala 3 is a syntactic sugar equivalent to:

Function1[Entry.Entry#Key] :def apply(e: Entry): e.Key
Copy the code

Match Type Match Type

Match Type “ConstituentPartOf[T]” is used to translate ConstituentPartOf[T] into different types, depending on Type parameter T. For example, “BigConstituentPartOf[BigInt]” is converted to Int and “BigConstituentPartOf[String]” is converted to Char.

type ConstituentPartOf[T] = T match
    case String= >Char 	// ConstituentPartOf[String] =:= Char
    case BigInt= >Int 		// ConstituentPartOf[String] =:= Int
    case List[t] => t 		// ConstituentPartOf[t] =:= t

val v1 : ConstituentPartOf[List[Int]] = 1 	// ok
val v2 : ConstituentPartOf[String] = 'a' 	// ok
val v3 : ConstituentPartOf[BigInt] = 20 	// ok
Copy the code

Match Types in Scala 3_bilibili

Type λ Type Lambda *

Reference: Type Lambda in Scala

Type lambdas and kind projector – Underscore

The type Lambda is related to Scala’s higher-order types, so first we need to review some concepts in Scala 2. Here’s an example:

class Functor01[M]
class Functor02[M[T]]
Copy the code

Functor01 takes a type parameter M, the “generic” encountered in Java programming. Here, M itself can be represented as any Class, that is, Apple, Fruit, etc., without Type parameters, or Type, that is, List[X], Map[K,V], Option[T], etc., with Type parameters.

type f1 = Functor01[Apple]        // ok, M : Apple
type f2 = Functor01[List[_]]      // ok, M : List[_]
type f3 = Functor01[Map[_, _]]// ok, M : Map[_,_]

type f4[T] = Functor01[List[T]]   // OK, M : List[T]
Copy the code

Functor02 accepts a higher-kind Type parameter M[T], which is equivalent to binding a Type parameter T to the constraint M itself. For example, M receives the List class, then M[T] represents the List[T] type.

class Apple
class Functor02[M[T]]
type g1 = Functor02[Apple]        // error, Apple has no type parameters
type g2 = Functor02[List]         // ok, M[T] => List[T]
type g3 = Functor02[Map]          // error, Map has two types of parameters
Copy the code

On the one hand, higher-order typing gives Scala the ability to abstract complex types, such as T[U[V],M[O,I]]. On the other hand, higher-order types also delay type inference. The following example demonstrates how M and T are determined sequentially:

class Functor02[M[T]] :
  def fff[T] () :Unit = ()

// set M to List
type g2 = Functor02[List]

// set T to Int
new g2().fff[Int] ()Copy the code

If the user does not care about the actual type of T, it can also be expressed as M[_]. The following example only determines that M is of type List, but ignores the type parameters carried by List itself.

class Functor02[M[_]] :
  def fff() : Unit = ()

type g0 = Functor02[List]
Copy the code

Functor[M[_]] cannot accept Map as a type parameter. The reason is that the Map itself carries two type parameters, which do not meet the M[T] form.

// Type argument Map[Int, X] does not have the same kind as its bound [K]
// error
type g0[X] = Functor02[Map[Int.X]]
Copy the code

This requires one step projection: Map a Map with two type parameters to an intermediate type IntMap[X] with only one parameter.

class Functor02[M[T]] : 
  def fff[T] : Unit = ()

type IntMap[X] = Map[Int.X]
type g1 = Functor02[IntMap]

new g1().fff[Int]
Copy the code

This type projection is called type Lambda. Further, in order to do the projection with a single line of code, Scala 2 uses very obscure expressions like this:

type g1 = Functor02[({type IntMap[X] = Map[Int.X#]})IntMap]
Copy the code

Scala 3 introduces an optimization of type lambda with =>>. Here’s a simplification:

type g1 = Functor02[[X] = > >Map[Int.X]]

// Delay determines that X is of type String
new g1().fff[String]
Copy the code

In this case, the upper and lower bounds of type X depend on T. Summary: the upper bound of X cannot be lower than T, and the lower bound cannot be higher than T. The boundary of X contains the boundary of T.

// A >: B >: C >: D >: E
class A
class B extends A
class C extends B
class D extends C
class E extends D

// A >: T >: B
class Functor02[M[T> :D< :B]]

// A >: X >: B
type g2 = Functor02[[X> :E< :A] = > >List[X]]
Copy the code

Functional parameter type

Scala 3: Type Lambdas, Polymorphic Function Types, and Dependent Function Types | by Dean Wampler | Scala 3 | Medium

Scala 3 supports defining function expression types like this, for example: [K,V] => (K,V) => Map[K,V]. (K,V)=> Map[K,V]; (K,V) => Map[K,V] type “.

This feature makes it possible to define function expressions with type parameters directly in Scala 3.

// Scala 2
// Generic methods can only be defined through def.
def toMapS2[K.V](k: K,v: V) : Map[K.V] = Map[K.V](k -> v)

// Scala 3
// Currified version
val toMapF2g_0: [K.V] = > (K.V) = >Map[K.V] = 
  [K.V] => (key: K,value: V) = >Map(key -> value) // good

// Non-Currified version
val toMapF2g_1: [K.V] = >K= >V= >Map[K.V] =
  [K.V] => (key: K) => (value: V) = >Map(key -> value) // good
Copy the code

Enumeration and algebraic data types

Enumeration declarations in Scala 2 are cumbersome. See my notes on an earlier edition of Scala: Other Classes – Nuggets (juejin. Cn). Scala 3 introduces the enum keyword to represent enumerated classes:

// Enumeration classes can take construction parameters.
enum Gender(genderID : Int) {
    // All cases here are an enumeration, and they inherit Gender by default.
    // Therefore extends Gender can be omitted when there is no enumerated class that does not require an argument.
    case Male extends Gender(0)
    case female extends Gender(1)}Copy the code

Based on this concise declaration, enumerated classes can be used to construct the Algebraic Data Type (ADT, also short for Abstract Data Type, not to be confused), an important concept in functional programming:

enum Tree[T] {
  case Leaf(v : T)
  case Node(l : Tree[T], r : Tree[T])}Copy the code

Have a brief overview of ADT, it is recommended that the reference: function model and domain modeling | younglab said (zhangyi. Xyz) | algebraic data type is what? – zhihu

Algebraic data types, hence the name, refer to data types that can be subjected to algebraic Algebra operation. The algebraic operation here refers specifically to Sum operation and Product operation. Users can combine these two methods to create new data types that can be exhausted. Enumeration class for Direction and Velocity:

// Add var to treat tag as a Direction attribute.
enum Direction(var tag : String) :
  // By default, each internal case inherits from Direction.
  case East   extends Direction("east")
  case West   extends Direction("west")
  case North  extends Direction("north")
  case South  extends Direction("south")

enum Velocity(var kmh : Int) :
  case Fast extends Velocity(100)
  case Slow extends Velocity(30)

East.tag // "east"
Copy the code

Direction is the sum type of East, West, North and South because there are only four basic directions: East, West, South and North. It’s a bit like mentioned above and type: Direction quite so East | West | North | South. The “and” here mean “or”. Similarly, Velocity is also the sum type of Fast and Slow (in this example, only “Fast” and “Slow” are divided).

If we multiply Direction and Velocity, we can obtain the product type of Movement. Moving contains a total of 2 × 4 = 8 states that describe all possibilities of azimuth movement: “Moving fast east” and “Moving slowly west”,…… . Considering the non-moving state Stay, Movement describes a total of 8 + 1 = 9 states through combination.

enum Movement:
  // Moving is the product type of Direction and Velocity.
  case Moving(direction: Direction,velocity: Velocity) extends Movement
  case Stay extends Movement
Copy the code

Using Scala’s pattern-matching mechanism, the compiler can examine the various potential branches of ADT and leave it up to the user to decide how to handle them.

val maybeMoving: Movement = Moving(East.Fast) // Stay, Moving(East,Fast)

maybeMoving match {
  case Moving(East.Fast) => println("=>>> fast.")
  case Moving(East.Slow) => println("=> slowly.")
  case Moving(West.Fast) => println("<<<= fast.")
  case Moving(West.Slow) => println("<= slowly.")
  case Moving(_,_) => println("moving...")
  case Stay => println("staying...")}Copy the code

Improvements to Implicit

Scala 3 addresses a major pain point in Scala 2: ubiquitous and transparent implicit greatly reduces the readability of code while increasing the risk of potential conflicts. In Scala 2, Implicit has three uses:

  1. Define implicit values, such asimplicit val a = 100.
  2. Method accepts implicit arguments such asdef f(x : Int)(implicit y : Int) = ....
  3. Define implicit class implementation method extensions, such asimplicit class A(x : B) ....

Scala 3 specifically breaks it down into three keywords: given, using, and extension. However, the Implicit keyword can still be used in Scala 3. Implicit values are called Context variables in the official documentation of Scala 3.

Given keyword

The given keyword is used to define implicit values in a way that follows the uniform access principle, so the right-hand side of the equation can be a simple literal value or a function call that returns a value. As follows:

// defines an implicit value.
// Implicit values are initialized only once in a context.
given Int = Random.nextInt()

// This is equivalent to:
implicit val aInt : Int = Random.nextInt()
Copy the code

The implicit value (or context variable, as the official manual calls it simply a ‘given’) defined by Given may not be defined by a variable name, which is then assigned by the compiler according to its type and named in the given_XXX format. To avoid potential naming conflicts, it is recommended to give given names voluntarily:

given aRamdomInt : Int = Random.nextInt()
Copy the code

Note that the given type must be declared. It is also important to note that two Givens of the same type cannot occur in the same context.

Using closures clause

Using is the opposite of given: if given is the “define” context, then using is the “find” context.

def calculateByContext(using ram :Int) : Int = ram % 2
/ / ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^
// using clause

As in Scala 2, the using clause does not need to be specified when calling a function.
println(calculateByContext)
Copy the code

Using x1:T1… Some are called using closures. The compiler itself looks for a given of type Int in the context and assigns it to the parameter RAM. The using clause can be defined as many times as you want.

given Int = 3
def f(using a : Int)(using b : Int)(using c : Int) : Int = a + b + c
println(f) / / 9
Copy the code

When the user needs to override the context, they can also proactively provide a using closure when calling a function, as follows:

println(f(using 1)(using 2)(using 3)) / / 6
Copy the code

Note that the using keyword above cannot be omitted.

Import givens

To avoid potential context conflicts, Scala 3 is more careful about importing context. When a package or class is now imported using the import keyword, its context is not loaded together.

object B :
  given Int = 300
  given Double = 2.99
  def b_f() : Unit = println("")

object A :
  
  // B.* Does not contain 300 or 2.99.
  import B. *// f requires an Int given.
  def f(using i : Int) : Unit= println(s"imported $i")

  @main
  def testThis() : Unit =
    // Compile failed because given Int is missing.
    f
Copy the code

If you want to import givens of B, you must declare it in the following format:

// Just write given to import all context of B
import B.given

// Only 300 will be imported
import B.given Int

// Import 300 and 2.99
import B.{given Int,given Double}

// import all contents of B, including context
import B.{given,*}
Copy the code

Context functions and builder patterns *

See resources: Context Functions provides | Scala 3 Language Reference | Scala Documentation (scala-lang.org)

Context is King. Context functions in Scala 3 | by Adam Warski | SoftwareMill Tech Blog | SoftwareMill Tech Blog

In layman’s terms, context functions refer to functions that use only context variables. Scala 3 introduces a new symbolic representation for this? =>, written as follows:

given Int = 3000
val g: Int? = >String =
  ($int : Int)? = >s"Got: ${$int}"
Copy the code

Int? $int ($int: int); $int ($int: int); $int ($int: int);

The following three invocation methods are all legal. Where notation 1 can be replaced by the more simplified notation 2:

/ /? => The left side is equivalent to the using clause, so the keyword using is required.
println(g(using given_Int))

// Since the compiler automatically looks for given_Int from the context, this is equivalent.
println(g)

// Actively override the context, printing: Got: 1000.
println(g(using 1000))
Copy the code

There’s a simpler way to declare g, which is omitting, right? Summon [Int] fetch Int values in context directly with Summon [Int] in the “using clause” section on the left (this method is similar to Scala 2’s IMPLICITLY [Int] method). Similarly, if using clause is actively passed in when g function is called, summon[Int] takes precedence over the value passed in.

val g: Int? = >String = s"Got: ${summon[Int]}"

// summon[Int] = 20
// Got: 20
println(g(using 20))
Copy the code

Context functions can be currified like normal functions, taking type parameters, and so on. Note, however, that the context function is of type ContextFunctionX.

val gg: ContextFunction1[Int.String] = g
Copy the code

Using context functions, control abstractions enable a simplified builder pattern. Here’s an official example:

class Table:
  val rows = new ArrayBuffer[Row]

  def add(r: Row) :Unit = rows += r

  override def toString = rows.mkString("Table(".",".")")

class Row:
  val cells = new ArrayBuffer[Cell]

  def add(c: Cell) :Unit = cells += c

  override def toString = cells.mkString("Row(".",".")")

case class Cell(elem: String)
Copy the code

Construct three constructor functions with the same name that receive control abstractions (context functions) for initialization work.

def table(init: Table? = >Unit) =
  given t: Table = Table()
  init
  t

def row(init: Row? = >Unit)(using t: Table) =
  given r: Row = Row()
  init
  t.add(r)

def cell(str: String)(using r: Row) =
  r.add(new Cell(str))
Copy the code

The structure of the Table is written as follows:

table(
    // The context function $t is provided by given t in the table function.
    ($t : Table)? = > {// =>Unit controls the abstract part, and its row method defaults to an external $t
        row(
            // =>Unit controls the abstract part, and its cell method defaults to an external $r
            ($r : Row) ?=> {
                cell("r:1 c:1")(using $r)
            }
        )(using $t)
        row(
            ($r : Row) ?=> {
                cell("r:2 c:1")(using  $r)
            }
        )(using $t)
    }
)
Copy the code

Since both $t and $r are provided by context by default, you can do so without actively assigning a using closure. The short form is:

val t1: Table = table {
    row {cell("r:1 c:1")}
    row {cell("r:2 c:1")}}Copy the code

This design idea is similar to delegate closures in Groovy. In this example, the state preservation and transmission of Table, row and cell is realized through Givens.

The context parameter of the name

The using clause allows to receive named calls to cxInt for the following code block. If x is null, the complexInt is not actually evaluated.

given complexInt : Int = {
  println("init..")
  1000
}

// the using clause accepts the named call
def CodeC(x : Int | Null)(using cxInt : =>Int) :Int ={
  x match {
    case xx : Int => xx * cxInt
    case_ = >0}}/ / print init... , a complexInt is initialized.
CodeC(300)
// do not print init... ComplexInt is not initialized.
CodeC(null)
Copy the code

A named call and a value call are relative concepts. See the section: Scala: The Beginning of Functional Programming – Nuggets (juejin. Cn)

extension

Scala 2 uses Implicit Classes to extend classes without violating THE OCP principle, while Scala 3 uses Extension instead, which makes the class extension semantics more explicit. As with implicit classes, the type of extension must be indicated after the extension keyword. Another point to note: Extension cannot be defined inside a method.

  @main
  def test() :Unit =
    // '<>' in SQL statement with '! = ` equivalence.
    println(1 <> 4)
  end test

  extension (x : Int) 
    infix def <>(that : Int) : Boolean= x ! = thatCopy the code

Extension extensions themselves can carry using closures and generics. Such as:

extension [T](x: T)(using n: Numeric[T])
  def + (y: T) :T = n.plus(x, y)
Copy the code

The resources

Now that Scala3 is out, what’s the future of the language? – zhihu (zhihu.com)

Scala3 and Type Classes – Zhihu (Zhihu.com)

Algebraic Data Types | Scala 3 Language Reference | Scala Documentation (scala-lang.org)

Overview | Scala 3 Language Reference | Scala Documentation (scala-lang.org)

New features in Dotty – Zhihu.com

Transparent Trait | Scala 3 assigned (dotty-china.com)

Introducing Transparent Traits in Scala 3 – Knoldus Blogs