These days, software engineers with knowledge of robust frameworks/libraries are abundant, but those who fully command the core basics of a language platform remain scarce. When required to come up with coding solutions to perform, scale or resolve tricky bugs, a good understanding of the programming language’s core features is often the real deal.
Scala’s signature strengths
Having immersed in a couple of R&D projects using Scala (along with Akka actors) over the past 6 months, I’ve come to appreciate quite a few things it offers. Aside from an obvious signature strength of being a good hybrid of functional programming and object-oriented programming, others include implicit conversion, type parametrization and futures/promises. In addition, Akka actors coupled with Scala make a highly scalable concurrency solution applicable to many distributed systems including IoT systems.
In this blog post, I’m going to talk about Scala’s implicit conversion which I think is part of the language’s core basics. For illustration purpose, simple arithmetics of complex numbers will be implemented using the very feature.
A basic complex-number class would probably look something like the following:
class Complex(r: Double, i: Double) { val re: Double = r val im: Double = i override def toString = ... // Methods for arithmetic operations +|-|*|/ ... }
Since a complex number can have zero imaginary component leaving only the real component, it’s handy to have an auxiliary constructor for those real-only cases as follows:
// Auxiliary constructor def this(x: Double) = this(x, 0)
Just a side note, an auxiliary constructor must invoke another constructor of the class as its first action and cannot invoke a superclass constructor.
Next, let’s override method toString to cover various cases of how a x + yi complex number would look:
// Override method toString override def toString = if (re != 0) if (im > 0) re + " + " + im + "i" else if (im < 0) re + " - " + -im + "i" else // im == 0 re.toString else // re == 0 if (im != 0) im + "i" else re.toString
Let’s also fill out the section for the basic arithmetic operations:
// Overload methods for arithmetic operations def + (that: Complex): Complex = new Complex(re + that.re, im + that.im) def - (that: Complex): Complex = new Complex(re - that.re, im - that.im) def * (that: Complex): Complex = new Complex(re * that.re - im * that.im, re * that.im + im * that.re) def / (that: Complex): Complex = { require(that.re != 0 || that.im != 0) val den = that.re * that.re + that.im * that.im new Complex((re * that.re + im * that.im) / den, (- re * that.im + im * that.re) / den) }
Testing it out …
scala> val a = new Complex(1.0, 2.0) a: Complex = 1.0 + 2.0i scala> val b = new Complex(4.0, -3.0) b: Complex = 4.0 - 3.0i scala> val c = new Complex(2.0) c: Complex = 2.0 scala> a + b res0: Complex = 5.0 - 1.0i scala> a - b res1: Complex = -3.0 + 5.0i scala> a * b res2: Complex = 10.0 + 5.0i scala> a / b res3: Complex = -0.08 + 0.44i
So far so good. But what about this?
scala> a + 1.0 :3: error: type mismatch; found : Double(1.0) required: Complex a + 1.0 ^
The compiler complains because it does not know how to handle arithmetic operations between a Complex and a Double. With the auxiliary constructor, ‘a + new Complex(1.0)’ will compile fine, but it’s cumbersome to have to represent every real-only complex number that way. We could resolve the problem by adding methods like the following for the ‘+’ method:
def + (that: Complex): Complex = new Complex(re + that.re, im + that.im) def + (x: Double): Complex = new Complex(re + x, im)
But then what about this?
scala> 2.0 + b :4: error: overloaded method value + with alternatives: (x: Double)Double (x: Float)Double (x: Long)Double (x: Int)Double (x: Char)Double (x: Short)Double (x: Byte)Double (x: String)String cannot be applied to (Complex) 2.0 + b ^
The compiler interprets ‘a + 1.0’ as a.+(1.0). Since a is a Complex, the proposed new ‘+’ method in the Complex class can handle it. But ‘2.0 + b’ will fail because there isn’t a ‘+’ method in Double that can handle Complex. This is where implicit conversion shines.
// Implicit conversion implicit def realToComplex(x: Double) = new Complex(x, 0)
The implicit method realToComplex hints the compiler to fall back to using the method when it encounters a compilation problem associated with type Double. In many cases, the implicit methods would never be explicitly called thus their name can be pretty much arbitrary. For instance, renaming realToComplex to foobar in this case would get the same job done.
As a bonus, arithmetic operations between Complex and Integer (or Long, Float) would work too. That’s because Scala already got, for instance, integer-to-double covered internally using implicit conversion in object Int, and in version 2.9.x or older, object Predef:
// Scala implicit conversions in class Int final abstract class Int private extends AnyVal { ... import scala.language.implicitConversions implicit def int2long(x: Int): Long = x.toLong implicit def int2float(x: Int): Float = x.toFloat implicit def int2double(x: Int): Double = x.toDouble }
Testing again …
scala> val a = new Complex(1.0, 2.0) a: Complex = 1.0 + 2.0i scala> val b = new Complex(4.0, -3.0) b: Complex = 4.0 - 3.0i scala> a + 1.5 res11: Complex = 2.5 + 2.0i scala> 3 - b res12: Complex = -1.0 + 3.0i scala> a * 2 res13: Complex = 2.0 + 4.0i scala> val x: Long = 50 x: Long = 50 scala> a + x res14: Complex = 51.0 + 2.0i scala> val y: Float = 6.6f y: Float = 6.6 scala> val y: Float = 4.0f y: Float = 4.0 scala> y / b res15: Complex = 0.64 + 0.48i
Implicit conversion scope
To ensure the implicit conversion rule to be effective when you use the Complex class, we need to keep it in scope. By defining the implicit method or importing a snippet containing the method in the current scope, it’ll certainly serve us well. An alternative is to define it in a companion object as follows:
// Instantiation by constructor object Complex { implicit def realToComplex(x: Double) = new Complex(x, 0) } class Complex(r: Double, i: Double) { val re: Double = r val im: Double = i def this(x: Double) = this(x, 0) // Auxiliary constructor override def toString = if (re != 0) if (im > 0) re + " + " + im + "i" else if (im < 0) re + " - " + -im + "i" else // im == 0 re.toString else // re == 0 if (im != 0) im + "i" else re.toString // Methods for arithmetic operations def + (that: Complex): Complex = new Complex(re + that.re, im + that.im) def - (that: Complex): Complex = new Complex(re - that.re, im - that.im) def * (that: Complex): Complex = new Complex(re * that.re - im * that.im, re * that.im + im * that.re) def / (that: Complex): Complex = { require(that.re != 0 || that.im != 0) val den = that.re * that.re + that.im * that.im new Complex((re * that.re + im * that.im) / den, (- re * that.im + im * that.re) / den) } }
As a final note, in case factory method is preferred thus removing the need for the ‘new’ keyword in instantiation, we could slightly modify the companion object/class as follows:
// Instantiation by factory object Complex { implicit def realToComplex(x: Double) = new Complex(x, 0) def apply(r: Double, i: Double) = new Complex(r, i) def apply(r: Double) = new Complex(r, 0) } class Complex private (r: Double, i: Double) { val re: Double = r val im: Double = i override def toString = if (re != 0) if (im > 0) re + " + " + im + "i" else if (im < 0) re + " - " + -im + "i" else // im == 0 re.toString else // re == 0 if (im != 0) im + "i" else re.toString // Methods for arithmetic operations def + (that: Complex): Complex = new Complex(re + that.re, im + that.im) def - (that: Complex): Complex = new Complex(re - that.re, im - that.im) def * (that: Complex): Complex = new Complex(re * that.re - im * that.im, re * that.im + im * that.re) def / (that: Complex): Complex = { require(that.re != 0 || that.im != 0) val den = that.re * that.re + that.im * that.im new Complex((re * that.re + im * that.im) / den, (- re * that.im + im * that.re) / den) } }
Another quick test …
scala> val a = Complex(1, 2) a: Complex = 1.0 + 2.0i scala> val b = Complex(4.0, -3.0) b: Complex = 4.0 - 3.0i scala> a + 2 res16: Complex = 3.0 + 2.0i scala> 3 * b res17: Complex = 12.0 - 9.0i