Scala - Code Blocks



Scala code block enclosed in curly braces '{ ... } as a special kind of text. It is like putting a note inside double quotes, but here, it contains actual instructions for a computer.

To include this special code inside other code, you can use $expr or ${ expr }. These are like containers for the special code, where expr is a specific type called Expr[T]. When you put code inside the single curly braces ('{ ... }), it does not run right away. But when you use the double curly braces (${ ... }), it runs and its results become part of the surrounding code.

For example,

val msg = Expr("Hello")
val printHello = '{ print($msg) }
println(printHello.show) // print("Hello")

In this code, the single curly braces delay the special code from running immediately. The double curly braces make the special code execute before the rest of the code. This concept allows you to use ${ ... } without the single curly braces, where the special code is evaluated during compilation, and its result is added to the generated code. However, there are some rules, and you can only use double curly braces at the top level inside something we call a macro. People do not usually nest these special code blocks inside each other when creating macros.

Level consistency

When writing code using quotes and splices, you can not just mix any code together. Some parts of your program exist during compilation (making the code). Whereas others exist during runtime (when the code runs).

For example, in this bad code:

def myBadCounter1(using Quotes): Expr[Int] = {
   var x = 0
   '{ x += 1; x }
}

Here, "x" exists during compilation. But you try to use it after the compiler is done. It is like trying to use something in a different place and even on a different computer which does not work.

Now, consider the opposite situation:

def myBadCounter2(using Quotes): Expr[Int] = '{
   var x = 0
   ${ x += 1; 'x }
}

Here, we try to use "x" before it even exists which is clearly impossible.

To prevent these problems, we introduce "levels" to count the number of quotes minus the number of splices around an expression and definition. So the system decides what you can and cannot reference.

For example,

// level 0
'{ // level 1
   var x = 0
   ${ // level 0
      x += 1
      'x // level 1
   }
}

You can reference global things like "println" at any level. But it limits you with local definitions. You can only access a local definition if it is defined at the same level where you are using it. So, it prevents errors in code like "myBadCounter1" and "myBadCounter2."

Even if you can not directly refer to a variable inside a quote. You can still pass its current value through a quote by converting it into an expression using Expr.apply.

Generics

When you are working with type parameters and abstract types in quoted code. You sometimes need to be explicit about these types. Scala follows a principle where it removes type information during compilation and the runtime does not keep track of all types.

For example,

def evalAndUse[T](x: Expr[T])(using Quotes) = '{
   val x2: T = $x // error
   ... // use x2
}

You will get an error because it is missing the context for Type[T]. To fix this, you can explicitly state it:

def evalAndUse[T](x: Expr[T])(using Type[T])(using Quotes) = '{
   val x2: T = $x
   ... // use x2
}

This code is equivalent to a more detailed version:

def evalAndUse[T](x: Expr[T])(using t: Type[T])(using Quotes) = '{
   val x2: t.Underlying = $x
   ... // use x2
}

Note that "Type" has a type member called "Underlying" that refers to the actual type held within "Type". In this case, "t.Underlying" is "T". While you can use "Type" implicitly, it is generally better to keep it in the context parameters because changes within the quote might require it. The less detailed version is usually easier to read but in some cases. You might need to use "t.Underlying" when the type within "Type" is not statically known.

When do you need this extra "Type" parameter?

When a type is abstract and it is used at a higher level than the current one.

When you add a "Type" contextual parameter to a method. You can either get it from another context parameter and obtain it implicitly using a call to "Type.of."

For example, these two lines of code are equivalent:

evalAndUse(Expr(3))
// is equivalent to
evalAndUse[Int](Expr(3))(using Type.of[Int])

However, not all types can be used as a parameter to "Type.of[...]" straightforwardly. For example, you can not recover abstract types that have already been erased:

def evalAndUse[T](x: Expr[T])(using Quotes) =
   given Type[T] = Type.of[T] // error
   '{
      val x2: T = $x
      ... // use x2
   }

But you can work with more complex types that depend on these abstract types. For example, if you are looking for and explicitly creating "Type[List[T]],". Then system will require "Type[T]" in the current context to compile.

In good code, you should add "Type" to the context parameters and avoid using them explicitly. However explicit usage can be helpful for debugging even though it may have less concise and clear code.

Conditional Statements, Loops, and Collections

Conditional statements let you make decisions in your code using if-else blocks.

For example,

// Check if math is still good...
val x =
   if (2 > 1)
      "math is fine"
   else
      "math is broken"
println(x) // --> math is fine

Loops like while and do-while run a piece of code while a certain condition is met.

Here is a while loop that prints multiples of 3:

var i = 1
while (i < 4) {
   println(i * 3)
   i += 1
}
// --> 3
// --> 6
// --> 9

And here is a do-while loop that finds the first power of 2 greater than 100:

var powerOfTwo = 1
do {
   powerOfTwo *= 2
} while (powerOfTwo < 100)
println(powerOfTwo) // --> 128

Most of the time, you can use collection methods like map, flatMap, and foreach instead of loops, which can make your code cleaner and more concise.

Note

  • You can define methods and variables within curly braces { }.
  • Code blocks can be nested within each other.
  • The result of a code block is the last line that is executed within it.
Advertisements