DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • Mastering Advanced Aggregations in Spark SQL
  • Thermometer Continuation in Scala
  • Deploying a Scala Play Application to Heroku: A Step-by-Step Guide
  • Upgrading Spark Pipelines Code: A Comprehensive Guide

Trending

  • Detection and Mitigation of Lateral Movement in Cloud Networks
  • Docker Base Images Demystified: A Practical Guide
  • FIPS 140-3: The Security Standard That Protects Our Federal Data
  • Beyond Code Coverage: A Risk-Driven Revolution in Software Testing With Machine Learning
  1. DZone
  2. Coding
  3. Languages
  4. Scala Generics: Generalized Type Constraints (Part 3)

Scala Generics: Generalized Type Constraints (Part 3)

This entry in this series on Scala generics builds on type bounds and use site variance to tackle generalized type constraints.

By 
Rafael Ruiz Giner user avatar
Rafael Ruiz Giner
·
May. 14, 18 · Tutorial
Likes (3)
Comment
Save
Tweet
Share
13.2K Views

Join the DZone community and get the full member experience.

Join For Free

This is the third article in our series on Scala Generics (we have already looked at upper and lower Scala type bounds and covariance and contravariance) and today, we are going to talk about constraints — to be more specific, we're going to talk about generalized type constraints.

In Generics I, we talked about type bounds and use site variance. We also talked about control over abstract types, but there are methods that need to make sure that the abstract type of the class meets certain restrictions only in that method.

And today, we are going to work with this small set of classes:

trait Thing
trait Vehicle extends Thing
class Car extends Vehicle
class Jeep extends Car
class Coupe extends Car

class Motorcycle extends Vehicle

class Vegetable


We will work with Parking, as usual.

class Parking[A <: Vehicle](val v: A){
    def park: A = v
}


In this example, the parking method can return any type of vehicle, just as the upper type bound of Parking specifies, but what happens if we want to have specific logic to park cars and motorcycles, kind of like this?

class Parking[A <: Vehicle](val v: A) {
    def parkMoto(): A = {
        println("moto") // this could call some public method of Motorcycle
        v
    }
    def parkCar(): A = {
        println("car") // this could call some public method of Car
        v
    }
}

 

In these cases, we want to ensure that A is a Motorcycle type for parkMoto and Car is a type for parkCar, right?

If we remember Generics I, there were two ways to do similar things: type bounds on the method and with use site variance.

Let’s try it with the type bounds!

Let’s try to add bounds to A for the methods parkMoto and parkCar:

class Parking[A <: Vehicle](val v: A) {
    def parkMoto[A <: Motorcycle] = {
        println("moto") // this could call some public method of Motorcycle
        v
    }
    def parkCar[A <: Car] = {
        println("car") // this could call some public method of Car
        v
    }
}


If you put this in an IDE, this will give you clues … Suspicious shadowing … But it does not matter, it compiles and we will try it!

val p1 = new Parking(new Motorcycle)
  p1: Parking[Motorcycle] = Parking@193f604a

p1.parkCar
  res5: Motorcycle = Motorcycle@5562c41e

p1.parkMoto
  res6: Motorcycle = Motorcycle@5562c41e  


It seems that those type bounds have not done anything. Obviously, we have the clue that the IDE gave us: Suspicious shadowing by a type parameter means that we are redefining the type parameter.

It turns out that we were not adding bounds to our A, but defining a new type A …

And if we type the return of the method to be sure?

class Parking[A <: Vehicle](val v: A) {
    def parkMoto[B <: Motorcycle]: B = {
        println("moto") // this could call some public method of Motorcycle
        v
    }
}
    <console>:13: error: type mismatch;
        found   : Parking.this.v.type (with underlying type A)
        required: B
                  v
                  ^            


It is not enough, since we are adding restrictions on the type of return, but we want to work with v: A

In other words, the restrictions should not go on a new type B, but on the type A defined in the class. It seems that the type bound does not work for us.

Let’s try using use site variance, if we remember, use site variance allowed us to define the constraints of a generic type at the moment of defining it:

class Parking[A](val v: A) {}

def parkMoto(parking: Parking[_ <: Motorcycle]) = {
    println("moto") // this could call some public method of Motorcycle
    parking.v
}
def parkCar(parking: Parking[_ <: Car]) = {
    println("car") // this could call some public method of Car
    parking.v
}


Looks good, let’s check it out:

parkCar(new Parking(new Car))
  res1: Car = Car@17baae6e

parkCar(new Parking(new Motorcycle))
  <console>:14: error: type mismatch;
    found   : Motorcycle
    required: Car
    parkCar(new Parking(new Motorcycle))
                            ^    


It seems that this can be a solution, although we have had to sacrifice several things… we use the methods parkMoto and parkCar outside Parking, passing a parking as a parameter… In addition, the open-close principle has been broken by calling parking.v (tell, do not ask).

Although it works, it is a very poor solution to our problem.

And here is where the Generalized type constraints come into play.

The three existing generalized type constraints are =:=, <:<, and <%<. They are used by the implicit parameters (implicit ev: T =:= B) in the method.

These implicit parameters, generally called ev (“evidences”) are tests, which show that a type meets certain restrictions.

These constraints can be used in different ways, but the most interesting thing is that they allow us to delimit the type parameter of the class in the same method:

class Parking[A <: Vehicle](val v: A) {
    def parkMoto(implicit ev: A =:= Motorcycle) { println("moto") }
    def parkCar(implicit ev: A =:= Car) { println("car")}
}


By using =:=, an abstract type such as Parking forces its type parameter to be a specific one for different methods.

And what will happen if I create a Parking [Car] and call it parkMoto?

val p1 = new Parking(new Car)
  p1: Parking[Car] = Parking@5669f5b9

p1.parkCar

p1.parkMoto
  <console>:14: error: Cannot prove that Car =:= Motorcycle.
    p1.parkMoto
      ^   


Indeed, we have managed to have methods that only work when the type parameter meets certain restrictions.

Mainly, the generalized type constraints serve to ensure that a specific method has a concrete constraint so that certain methods can be used with one type and other methods with another.
However, due to the type erasure, we cannot overload a method: 

class Parking[A <: Vehicle](val vehicle: A) {
  def park(implicit ev: A =:= Motorcycle) { println("moto") }
  def park(implicit ev: A =:= Car) { println("car") }
}
  method park:(implicit ev: =:=[A,Car])Unit and
  method park:(implicit ev: =:=[A,Motorcycle])Unit at line 12
  have same type after erasure: (ev: =:=)Unit
             def park(implicit ev: A =:= Car) {} 


Another curious use case could be the next one: I want a method for parking vehicles of the same class:

class Parking[A <: Vehicle](val vehicle: A) {
    def park2(vehicle1: A, vehicle2: A) {}
}


As you have already deduced, this is not enough. The two vehicles could be any subtype of Vehicle, and if the parking we are creating is a new Parking (new Car), we could park one Jeep and one Coupe at a time.

The solution is a generalized type constraint:

class Parking[A <: Vehicle] {
    def park2[B, C](vehicle1: B, vehicle2: C)(implicit ev: B =:= C) {}
}


Now let’s try it:

val p2 = new Parking[Car]
  a: Parking[Car] = Parking@57a68215

p2.park2(new Jeep, new Jeep)

p2.park2(new Jeep, new Coupe)
  <console>:15: error: Cannot prove that Jeep =:= Coupe.
  a.park2(new Jeep, new Coupe)
              ^                  


Now, vehicle1 must be the same type as vehicle2. However …

p2.park2(new Vegetable, new Vegetable)


Oops… we have lost the Vehicle constraint. Let’s fix it! Type bounds to the rescue!

class Parking[A <: Vehicle] {
    def park2[B <: A, C](vehicle1: B, vehicle2: C)(implicit ev: B =:= C) {}
}

val p3 = new Parking[Car]
p3.park2(new Vegetable, new Vegetable)
  <console>:14: error: inferred type arguments [Vegetable,Vegetable] do not conform to method park2's type parameter bounds [B <: Car,C]
      p3.park2(new Vegetable, new Vegetable)
      ^
    <console>:14: error: type mismatch;
          found   : Vegetable
          required: B
          p3.park2(new Vegetable, new Vegetable)
                    ^
    <console>:14: error: type mismatch;
          found   : Vegetable
          required: C
          p3.park2(new Vegetable, new Vegetable)
                                   ^
    <console>:14: error: Cannot prove that B =:= C.
          p3.park2(new Vegetable, new Vegetable)*/

p3.park2(new Jeep, new Coupe)
  <console>:15: error: Cannot prove that Jeep =:= Coupe.
         p3.park2(new Jeep, new Coupe)
                     ^                            

p3.park2(new Jeep, new Jeep)


Now the Parking2 method can only receive two identical types that must also be A or a subtype of A!

And finally, let’s look at other two generalized type constraints:

A <:< B, as you may guess, means “A must be a subtype of B”. It is analogous to the type bound <:.

Its use is exactly the same as with =:=, with an implicit “evidence”.

In the previous case of parkCar and parkMoto, if we wanted to not only park cars and motorcycles but subtypes of cars and motorcycles as well, we would not have used =:=, but rather used <:< :

class Parking[A <: Vehicle](val v: A) {
    def parkMoto(implicit ev: A <:< Motorcycle) { println("moto") }
    def parkCar(implicit ev: A <:< Car) { println("car")}
}


And of course, in the case of receiving two type parameters, we can force one to be the subtype of the other: 

class Parking[A <: Vehicle] {
    def park2[B <: A, C](vehicle1: B, vehicle2: C)(implicit ev: B <:< C) {}
}


The last generalized type constraints — <%< — is deprecated and is not in use in the Scala stdlib. It refers to the concept of “view”, also deprecated. That means that in A <%< B, A must be able to be seen as B. This can be given by an implicit conversion, for example.

These three Scala Generic articles are just an introduction to generics. We looked at some complex pieces, and while I would like say that we have reached some deep levels, we have actually only scratched the surface. We have not even entered into what their implementations are!

I hope that I will have time to write more articles about Scala in the near future!

Scala (programming language)

Published at DZone with permission of Rafael Ruiz Giner. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Mastering Advanced Aggregations in Spark SQL
  • Thermometer Continuation in Scala
  • Deploying a Scala Play Application to Heroku: A Step-by-Step Guide
  • Upgrading Spark Pipelines Code: A Comprehensive Guide

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!