Contextual Polymorphism With Operator Overloading in Kotlin and Scala
Ad-hoc polymorphism is very elegant. It morphs based on the context. This article will give you a new perspective on ad-hoc polymorphism.
Join the DZone community and get the full member experience.
Join For FreePolymorphism is one of the key concepts in programming languages. There are several kinds of polymorphisms such as ad-hoc polymorphism, parametric polymorphism, subtyping polymorphism, and a few others. In this article, we will mainly focus on ad-hoc polymorphism which relies on function overloading or operator overloading.
This article is about ad-hoc polymorphism that you’ve learned from your object-oriented Programming class, yet you will see it from a different angle, and you may be amazed at how elegant it is. The concept remains the same, only the presentation that is different. In the end, I’d like to take you to another concept that extends polymorphism further where the object can morph to a different kind depending on the context.
Ad-Hoc Polymorphism
Before getting started, let’s brush up on the definition.
Christopher Strachey chose the term ad hoc polymorphism to refer to polymorphic functions that can be applied to arguments of different types, but that behave differently depending on the type of the argument to which they are applied — Wikipedia
fun times(x: Int, y: Int): Int = x * y
fun times(x: String, y: Int): String = x.repeat(y)
This looks like a regular function overloading, and they actually are.
println(times(2, 3)) // 6
println(times("abc", 3)) // abcabcabc
Operator Overloading
What about we reimplement it using an operator overloading, and extension functions?
operator fun String.times(y: Int): String = this.repeat(y)
// We don't have to implement * for 2 Ints.
Even though the end results are the same, it shows you a new angle of regular function overloading that you may have never seen before.
println(2 * 3) // 6
println("abc" * 3) // abcabcabc
println("abc" + 3) // abc3
println("abc".substring(1) * 3) // bcbcbc
“abc” is a string object, and it has all the string properties. In the last line, you can call .substring(1) to get a partial string starting from the specified index. With operator overloading and extension functions, the string object can be used in a mathematical context. “abc” * 3 will be repeated 3 times which is correct in a mathematical sense.
We typically don’t say true or false in our daily conversation, but we say yes and no to represent true and false respectively. The string is used for representing a natural language. So, let’s apply that concept to boolean algebra, and Kotlin also supports unary operator overloading.
operator fun String.not(): String {
return if (this == "yes") "no" else "yes"
}
println(!"yes") // no
println(!"no") // yes
At this point, the examples I’ve given are kinda cool, but are they really useful? How can we use this idea in the real world, not just this toy program?
The World is Driven by Greed. Let Money Talks
data class Currency(val value: BigDecimal, val code: String) {
constructor(v: Int, code: String): this(BigDecimal(v), code)
fun convert(toCode: String): Currency {
return Currency(convert(this, toCode), toCode)
}
private fun convert(fromCurrency: Currency, toCode: String): BigDecimal {
//1 USD = 1.27082 CAD
return if (fromCurrency.code == "USD" && toCode == "CAD") {
fromCurrency.value * BigDecimal(1.27)
} else if (fromCurrency.code == "CAD" && toCode == "USD") {
fromCurrency.value / BigDecimal(1.27)
} else {
...
}
}
operator fun plus(that: Currency): Currency {
println(convert(that, this.code))
return Currency(convert(that, this.code) + this.value, this.code)
}
override fun toString(): String = "$value $code"
}
In this example, we define the class for holding the currency value. This is for demonstration purposes only. You probably don’t want to use BigDecimal since it suffers from IEEE 754! You cannot avoid it anyway since the computer talks 0 and 1. What I mean to say is that you should let the experts handle the estimation for you, and I believe JSR354 is a good one if you’re using the JVM language. It goes without saying that the conversion should use the data from the foreign exchange rates updated real-time, not the hardcoded if .. else. Otherwise, your company will go bankrupt instantly.
How Does Currency Work?
Assuming that you work in Canada and you get your salary in Canadian dollars. You also work for a company in the US for the moonlighting project and you get a salary in US dollars. We can represent this in the code as follows:
val earnInCanada = Currency(1000, "CAD")
val earnInUS = Currency(3000, "USD")
val totalEarning = earnInCanada + earnInUS
println(totalEarning) // 4810 CAD
println(totalEarning.convert("USD")) // 3787 USD
println("I have earned " + totalEarning) // I have earned 4810 CAD
Your currency object can be transported over HTTP REST and it can be encoded in XML or JSON. The object can be persisted in a database column. It can be operated in a mathematical context as demonstrated in this example. The object is handled differently based on the context it is being passed to.
Initially, I wanted to show you how awesome Kotlin is by demonstrating the language features like extension functions and operator overloading. It turns out that I find it hard to make the above code work in the real world, and that’s because I cannot provide the foreign exchange data to the function without making the object accessible through a static reference. I find that Scala is still a better choice in this case. Thanks to implicit parameters. I wish Kotlin supports implicit parameters, but it may never happen, given the many differences in philosophy (as of today Feb 2022).
Now let’s look at the Scala code.
class ForeignExchange {
def convert(fromCurrency: Currency, toCode: String): BigDecimal = {
//1 USD = 1.27082 CAD
if (fromCurrency.code == "USD" && toCode == "CAD") {
fromCurrency.value * BigDecimal(1.27)
} else if (fromCurrency.code == "CAD" && toCode == "USD") {
fromCurrency.value / BigDecimal(1.27)
} else {
...
}
}
}
case class Currency(value: BigDecimal, code: String) {
def convert(toCode: String)(implicit exchange: ForeignExchange): Currency = {
Currency(exchange.convert(this, toCode), toCode)
}
def +(that: Currency)(implicit exchange: ForeignExchange): Currency = {
Currency(exchange.convert(that, this.code) + this.value, this.code)
}
override def toString: String = s"$value $code"
}
The foreign exchange object can be provided implicitly. This allows us to inject the exchange instance into the operation without explicitly passing it. The foreign exchange can be implemented for different data sources such as BankOfAmericaExchange, XECurrencyAuthority, or any other exchanges.
implicit val exchange: ForeignExchange = new BankOfAmericaExchange()
val earnInCanada = Currency(1000, "CAD")
val earnInUS = Currency(3000, "USD")
val totalEarning = earnInCanada + earnInUS
println(totalEarning) // 4810 CAD
println(totalEarning.convert("USD")) // 3787 USD
As you can see, the exchange can be anything that implements the ForeignExchange interface, and it doesn’t have to be referenced through a static reference.
Taking Contextual Polymorphism to Another Level
At work, I have been tasked to implement the following feature in the scripting language that I developed. The requirement is simple. A user makes an HTTP request through some mechanisms that we provide, and the HTTP response returning from that request will be assigned to a variable. Assuming that the variable name is “response”, we want users to be able to treat the “response” object as if it’s an HTTP body. If the content type is JSON, the response will be JSON object. If the content type is TEXT, the response will be String. You may have a lot of questions at this point, and I will try to address some of them in the next few paragraphs.
What Happens in Case of an Error?
You can walk a JSON tree node using dot notations like response.person.name. In our scripting language, a dot notation is a null-safe navigation operator. It means no NullPointerException (NPE) even one of the paths or all of them are null. That sounds good, right? It’s intuitive and convenient, but that comes with a trade-off. You don’t know which node is null until you debug it. What about an error response? This is a design choice we have to make. We can provide a built-in function like response.isError() or we can provide a function for introspecting an object like isError(response).
The former looks more natural and we can provide a code completion easily to make the language more intuitive, but nothing is perfect. We support the JavaBeans style which means the properties of an object have companion accessor (getter) and mutator (setter) functions. isError() function can be an accessor for the error attribute in case there is a field named “error” in the JSON payload.
{
"error": false,
"others": {...}
}
The error is the field in the JSON tree node. Regardless of the HTTP status, which error we will shadow? The built-in function that we provide through the HTTP response object or the field name (isError) deserialized from JSON? Whatever path we choose to take, will lead to confusion. The best way to handle this is by providing the HTTP utility functions like http:isError(response)
Up to this point, you will have a lot of your questions answered already like How can we differentiate between an error and a successful response? or Can I access headers or other parts of the HTTP response?
Accessing other HTTP attributes can be done through the HTTP utility functions like http:getHeader(response, “Content-Type”)
Maybe it’s not very clear how this is related to contextual polymorphism. Let me elaborate a bit.
The response object can be treated as if it’s a string when the content-type is text/plain or a JSON tree node when the content-type is application/JSON. The object itself is still an HTTP response object that you can access any other attributes of the HTTP response.
Furthermore, when you walk a tree using dot notations, a terminal node can be used directly in a mathematic expression as if it’s a primitive type. For example, response.person.age > 21. The age attribute is a JSON number object but you can compare that with a primitive integer. A person is a JSON object, and you can print it out as a JSON string in a string context.
How Does That Work?
In the first half of this article, we know that the ad-hoc polymorphism works on the overloading mechanism. It’s part of the language syntax and type system, and that can be determined at compile time (if it is a statically typed language). In the second half, the morphing relies on the type at runtime. The language runtime needs some information in order to act based on the context, and the information can be stored in the class definition in the form of metadata.
I hope that this article gives you a new perspective on polymorphism and how elegant it is when we use it in different contexts.
Published at DZone with permission of Hussachai Puripunpinyo. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments