Scala Tutorial - The Magnet Pattern
Overview
As a follow on to the preceding Type Class section, we present yet another great strategy that is commonly referred to as the Magnet Pattern. The latter was no doubt made popular by the brilliant engineers who designed the robust HTTP middle-tier named Spray. It can be especially handy in the context of method polymorphism, and for the purpose of illustration, we take another look at the applyDiscount() method from the Function Polymorphism section in chapter two.
In particular, we will use the Magnet Pattern as a means to provide different overloaded methods that are type safe. It goes without saying, that type safety is perhaps a mandatory requirement whenever you are tasked to design a Domain Specific Language (DSL), such as, the one that was characteristic of Spray. Arguably, you could just as well use a number of overloaded parameters in overloaded methods to meet your respective requirements. However, these tend to lead to unpleasant errors, such as, method signature collisions, among others. As a matter of fact, these issues tend to originate from Type Erasure - that is, certain types that exist at the higher level syntax are eventually discarded at runtime. To this end, the Magnet Pattern can be a nice substitute to using overloaded methods.
As a reminder, and before proceeding further, feel free to review the previous materials on traits as they are especially important:
- Learn How To Create And Extend Trait
- Learn How to Create Trait With Type Parameters
- Learn How To Extend Multiple Traits
- Learn How to Use Traits for Dependency Injection Part 1
- Learn How To Use Trait For Dependency Injection Part 2 Avoid Cake Pattern
- Traits, Companion Objects, Factory Pattern
- Type Class For Ad-hoc Polymorphism
- Trait Mixin And Linearization
Steps
1. Review of applyDiscount() method
Let us remind ourselves of the applyDiscount() method which had two versions - one with a couponCode parameter of type String, and the other having a percentageDiscount of type Double.
println("Step 1: Review of applyDiscount() method")
def applyDiscount(couponCode: String): Unit =
println(s"Lookup percentage discount in database for $couponCode")
def applyDiscount(percentageDiscount: Double): Unit =
println(s"$percentageDiscount discount will be applied")
applyDiscount("COUPON_1234")
applyDiscount(10)
You should see the following output when you run your Scala application in IntelliJ:
Lookup percentage discount in database for COUPON_1234
10.0 discount will be applied
2. Overloaded methods
It would be perfectly sensible to have other variations to the applyDiscount() method that provide additional discounts to our customers. Consider one that provides daily, weekly and monthly coupon codes, and another that applies some further discount on given holidays.
println("\nStep 2: Overloaded methods and type erasure")
def applyDiscount(dailyCouponCode: String, weeklyCouponCode: String, monthlyCouponCode: String): Unit = ???
def applyDiscount(percentageDiscount: Double, holidayDiscount: Double): Double = ???
3. The Magnet Pattern
All things considered, we could certainly define the above-stated overloaded methods, as our code would compile just fine. However, in the upcoming Step 4, we will in fact demonstrate some of the nuances of type erasure, and therefore let us get familiar with defining, and using, the Magnet Pattern. You typically start by defining a general trait, such as, the trait DiscountMagnet, to express the parameters and return type of our applyDiscount() method. By doing so, and instead of using an explicit type parameter - that is, trait DiscountMagnet[Out] - we make use of an abstract type member - that is, type Out.
Additionally, we will make use of implicit conversions, and as of Scala 2.13, this feature requires an import scala.language.implicitConversions. On another note, during the setting up of our real- world Scala project, we will provide details on how to enable compiler options in the build.sbt file, such that you do not require the above-mentioned import statement.
println("\nStep 3: The Magnet Pattern")
import scala.language.implicitConversions
sealed trait DiscountMagnet {
type Out
def apply(): Out
}
Next, we define a discount() method that takes in the DiscountMagnet type as its input parameter - that is, def discount(magnet: DiscountMagnet). The body, or implementation, of the discount() method is simply an instance of the DiscountMagnet trait, and thanks to its apply() method, you can basically use the magnet() notation for that purpose. Most importantly though, the return type of the discount() method remains versatile as a result of defining the abstract type member - that is, type Out - in the trait DiscountMagnet.
def discount(magnet: DiscountMagnet): magnet.Out = magnet()
What is left is to provide the different variations to our discount() method, such as, def discountStringCouponCode String), and def discountDoubleCouponCode(percentageDiscount: Double), inside the Companion Object of the trait DiscountMagnet. The glue, however, is defining these methods as implicit, and let the compiler fill in an appropriate implicit method whenever the discount() method is invoked.
object DiscountMagnet {
implicit def discountStringCouponCode(couponCode: String) =
new DiscountMagnet {
override type Out = Unit
override def apply(): Out = {
println(s"DiscountMagnet -> discountStringCouponCode = Lookup percentage discount in database for $couponCode")
}
}
implicit def discountDoubleCouponCode(percentageDiscount: Double) =
new DiscountMagnet {
override type Out = Double
override def apply(): Out = {
println(s"DiscountMagnet -> discountDoubleCouponCode $percentageDiscount discount will be applied")
10.0
}
}
}
To sum up, invoking the discount() method looks-and-feels comparable to an overloaded method. For instance, we can call the discount() method by passing through a String value, or a value of type Double. With the above implicit layout in scope, the compiler will apply the correct implicit def discountStringCouponCode, or implicit def discountDoubleCouponCode, method respectively.
discount("COUPON_1234")
discount(10.0)
You should see the following output when you run your Scala application in IntelliJ:
Step 3: Magnet Pattern
DiscountMagnet -> discountStringCouponCode = Lookup percentage discount in database for COUPON_1234
DiscountMagnet -> discountDoubleCouponCode = 10.0 discount will be applied
4. Type erasure and Magnet Pattern
In all likelihood, we are adding too much verbosity to our code base by using the Magnet Pattern, as opposed to equivalent overloaded methods. Let us, however, consider a reasonable situation whereby you need to provide two additional overloaded methods to the applyDiscount() method. The first method will accept a List of type String as its input parameter - that is, def applyDiscount(coupons: List[String]) - as our customers may wish to use more than coupon code. As for the second method, its parameter is also a List data structure, but of type Int - def applyDiscount(coupons: List[Int]) -and these could pertain to, say, third-party coupon format of the type Int.
def applyDiscount(coupons: List[String]): Unit = ???
def applyDiscount(coupons: List[Int]): Unit = ???
As it turns out, the above two overloaded methods will result in a compile time error! And, they are a classic example of type erasure on overloaded methods.
Error:(112,7) double definition:
def applyDiscount(coupons: List[String]): Unit at line 111 and
def applyDiscount(coupons: List[Int]): Unit at line 112
have same type after erasure:(coupons: List)Unit
def applyDiscount(coupons: List[Int]): Unit = ???
With the Magnet Pattern, however, we can match the above overloaded methods requirement by merely adding two implicit methods to our object DiscountMagnet closure.
object DiscountMagnet {
implicit def discountListOfString(coupons: List[String]) =
new DiscountMagnet {
override type Out = Unit
override def apply(): Out = {
println(s"DiscountMagnet -> discountListOfString = $coupons")
}
}
implicit def discountListOfInt(coupons: List[Int]) =
new DiscountMagnet {
override type Out = Unit
override def apply(): Out = {
println(s"DiscountMagnet -> discountListOfInt = $coupons")
}
}
}
You can thereafter invoke the discount() method by passing through either a List of type String, or a List of type Int.
discount(List("COUPON_1234", "COUPON_4321"))
discount(List(111, 222, 333))
You should see the following output when you run your Scala application in IntelliJ:
Step 4: Type erasure and Magnet Pattern
DiscountMagnet ⇒ discountListOfString = List(COUPON_1234, COUPON_4321)
DiscountMagnet => discountListOfInt = List(111, 222, 333)
Summary
In this tutorial, we went over the following:
- Review of applyDiscount() method
- Overloaded Methods
- The Magnet Pattern
- Type erasure and Magnet Pattern
Tip
- If you have not used Spray in the past, that is OK. As it happens, the project and code base have now been superceeded by Akka HTTP. And, don't forget to check out our Akka HTTP tutorials.
Source Code
The source code is available on the allaboutscala GitHub repository.
What's Next
In the next tutorial, I will do a comprehensive review of the various concepts and techniques that we have learned so far. In particular, we will look at the Functor trait from the popular cats library.