Let’s create a practical example of implicit conversions using string interpolators. Scala has some nice built-in string interpolators—see String Interpolation—but you can also create your own custom interpolators quite easily by using the concepts we’ve seen in this chapter. We’ll create a custom interpolator that will mask out select values from the processed string. To show that the interpolation can return something other than String—just about any object—we’ll return a StringBuilder as the result of processing the string. But first let’s revisit string interpolation to explore some concepts we need for the implementation.
When Scala sees the form
| interpolatorName"text1 $expr1 text2 $expr2" |
the compiler turns that into a function call on a special class named StringContext after separating the texts and the expressions. In effect, it translates the example form to
| new StringContext("text1", "text2", "").interpolatorName(expr1, expr2) |
The separated-out texts are sent as arguments to the constructor of StringContext and are available through its parts property. The expressions, on the other hand, are passed as arguments to the StringContext’s method with the name of the interpolator. In the arguments sent to the constructor, each argument is a piece of text before an expression, with the last argument representing text that follows the last expression. In this example, it’s empty ("") since there’s no text after the last expression.
As we saw earlier, the three functions that StringContext already provides are s, f, and raw. We’ll create a new interpolator named mask that will only partially display select expressions in the processed string. Let’s first use the interpolator as if it were a built-in function. Once we get a grasp of the intent from an example use, we’ll then create the interpolator.
MakingUseOfTypes/mask.scala | |
| object UseInterpolator extends App { |
| import MyInterpolator._ |
| |
| val ssn = "123-45-6789" |
| val account = "0246781263" |
| val balance = 20145.23 |
| |
| println(mask"""Account: $account |
| |Social Security Number: $ssn |
| |Balance: $$^${balance} |
| |Thanks for your business.""".stripMargin) |
| } |
The call to println receives a string with the yet-to-be-written mask interpolation. The string attached to mask is a heredoc (see Strings and Multiline Raw Strings) with texts and expressions. Our mask interpolator will return a StringBuilder with each expression’s string representation converted, where all but the last four characters are replaced with “…,” unless the expression is preceded with a caret (^). In the example, we’re asking the mask function to keep the value of balance intact by placing a caret in front of it, but the display of the other two expressions, account and ssn, will be altered.
To process this string Scala will transform the code with mask to
| new StringContext("Account:", "Social...", ...).mask(account, ssn, balance) |
However, there is no mask method in StringContext. That should not deter us; there’s no days function in integer but we made the code 2 days ago work. We’ll apply the same trick of implicit conversion here.
Since the compiler is looking to call a method on the StringContext, our implicit value class should take an instance of StringContext as its constructor parameter and implement the mask method—it’s that simple. Let’s get to the code.
MakingUseOfTypes/MyInterpolator.scala | |
| object MyInterpolator { |
| implicit class Interpolator(val context: StringContext) extends AnyVal { |
| def mask(args: Any*) = { |
| val processed = context.parts.zip(args).map { item => |
| val (text, expression) = item |
| if(text.endsWith("^")) |
| s"${text.split('^')(0)}$expression" |
| else |
| s"$text...${expression.toString takeRight 4}" |
| }.mkString |
| |
| new StringBuilder(processed).append(context.parts.last) |
| } |
| } |
| } |
The implicit value class Interpolator is housed in the singleton MyInterpolator. It takes an instance of StringContext as the constructor parameter, and extends AnyVal.
As its core behavior, the mask method combines the expressions given as parameters and the texts available in the StringContext’s parts. We can easily combine these two collections into one, much like the way we can combine the two sides of a winter jacket—using the zip function. This function works with two arrays—the texts and the expressions in this example—and produces one array of tuples. Each element in the tuple has one text and the expression that follows the text. One excess text that follows the last expression is discarded by the zip function—we’ll take care of this in the last step. We use the map method that we saw in Chapter 1, Exploring Scala to transform from this array of tuples to an array of combined strings. As we iterate through each text-expression pair, if a text ends with a caret, we leave the expression intact. Otherwise, we replace it with the ellipsis and last four characters using the takeRight method. Finally we combine the array of strings with the mkString method and append the final text that follows the last expression to the result StringBuilder.
Let’s compile and run the code using the following commands:
| scalac MyInterpolator.scala mask.scala |
| scala UseInterpolator |
The output of our interpolator, in full glory, is shown next:
| Account: ...1263 |
| Social Security Number: ...6789 |
| Balance: $20145.23 |
| Thanks for your business. |
This is a little example, but it brought together a number of nice little concepts. We used singletons, implicit value classes, the map function with functional style to iterate and process the elements, a custom string interpolator, and more.
Take some time to play with the code, evolve it, introduce some more formatting, and implement it, while keeping the implementation close to the functional style.
3.149.231.128