I was wondering whether there is a good reason to use subtype as functions type parameter? Lets consider the following example:
scala> trait Animal { def sound: String }
defined trait Animal
scala> def f1[T <: Animal](a: T) = a.sound
f1: [T <: Animal](a: T)String
scala> def f2(a: Animal) = a.sound
f2: (a: Animal)String
Has f1 some advantages over f2?
Language. Methods in Scala can be parameterized by type as well as by value. The syntax is similar to that of generic classes. Type parameters are enclosed in square brackets, while value parameters are enclosed in parentheses.
A type parameter, also known as a type variable, is an identifier that specifies a generic type name. The type parameters can be used to declare the return type and act as placeholders for the types of the arguments passed to the generic method, which are known as actual type arguments.
For example, a type constructor does not directly specify a type of values. However, when a type constructor is applied to the correct type arguments, it yields a first-order type, which may be a value type. Non-value types are expressed indirectly in Scala.
It declares the class to be covariant in its generic parameter. For your example, it means that Option[T] is a subtype of Option[S] if T is a subtype of S . So, for example, Option[String] is a subtype of Option[Object] , allowing you to do: val x: Option[String] = Some("a") val y: Option[Object] = x.
I believe there are no advantages in your example. Type parameters are usually used to connect different parts of the code, but information about T
is effectively lost as it doesn't match anything. Consider another example:
def f1[T <: Animal](a: T) = (a.sound, a)
def f2(a: Animal) = (a.sound, a)
Now things are different as T
is passed to a return type:
class Dog extends Animal { def sound = "bow-wow" }
f1(new Dog)
//> (String, Dog) = (bow-wow,Dog@141851fd)
f2(new Dog)
//> (String, Animal) = (bow-wow,Dog@5a1fe991)
In this case you can think of f1
as of a template which is "instantiated" at compile time and effectively generates a specific method based on compile-time parameter types. So if you want to use f1(new Dog)
where (String, Dog)
is required it will compile, while f2(new Dog)
won't.
Both functions f1
and f2
are very similar. If you output the bytecode, you'll see up in the constants pool that they are denoted as:
#71 = Methodref #12.#70 // Main$.f2:(LMain$Animal;)Ljava/lang/String;
#74 = Methodref #12.#73 // Main$.f1:(LMain$Animal;)Ljava/lang/String;
As far as the bytecode is concerned, they are functions that take an Animal
as a parameter and return String
.
One situation where this gets more interesting is when you want to return a specific T
(where T <: Animal
). Keep in mind, the bytecode will still be the same, but at compile-time this gives more meaning and power to the T
type parameter:
Imagine you have:
def f1[T <: Animal](a: T): T = a // silly, I know
def f2(a: Animal): Animal = a
And you try this:
val s: Dog = f1(new Dog())
val t: Dog = f2(new Dog()) // NOPE
val u: Dog = f2(new Dog()).asInstanceOf[Dog] // awkward
That second line won't compile without casting, which sacrifices compile-time type-checking.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With