Scala: Type Parameter
Chào mừng bạn đến với thế giới của lập trình hàm, hôm nay Bizfly Cloud sẽ giới thiệu đến các bạn những thông tin chi tiết nhất về Type Parameter trong Scala ngay tại bài viết này nhé.
Type Parameter trong scala
Class trong Scala có thể nhận một loại tham số đặc biệt, gọi là Type Parameter. Loại parameter này không giống như các biến bình thường như các tham số khác, mà là một tham số chỉ định kiểu. Chính vì thế nó được gọi là Type Parameter. Trong khi Class được định nghĩa, chúng ta hoàn toàn có thể thao tác trên một kiểu chưa được biết trước (sẽ chỉ định khi gọi class) và kiểu đó được "quy ước" thành Type Parameter.
class SuperSaiyan[T,R](name: T, age: R)
{
def kamehameha(power: R) = {
println(name.toString() age.toString() ':' power.toString())
}
}
val goku = new SuperSaiyan[String,Int]("Goku",100)
goku.kamehameha(1000) // Goku100:1000
goku.kamehameha("kameeee") // type mismatch
Ở đây T và R là Type Parameter. T và R được dùng để chỉ định kiểu cho tham số của class (name,age) và kiểu cho tham số của hàm (power). Khi tạo biến goku chúng ta cần chỉ định kiểu T và kiểu R bằng dấu ngoặc vuông (giống với khi định nghĩa).
Tại thời điểm biến goku được tạo ra thì một instance của class SuperSaiyan đã hình thành, và trong đó thì R thống nhất là String và T thống nhất là Int.
Pair
Mình sẽ đề cập đến một ứng dụng thực tiễn của Type Parameter. Khi viết xử lý theo từng method, đôi khi chúng ta muốn giá trị trả về không chỉ là một giá trị riêng lẻ, mà (ví dụ) là một cặp giá trị . Khi đó thường có 2 cách làm
• Để 1 giá trị trả về theo kiểu return, giá trị còn lại truyền vào là tham số và trong method sẽ thay đổi tham số đó
• Tạo ra một kiểu dữ liệu mới chứa được tất cả các giá trị, và chỉ return lại kiểu dữ liệu đó thôi.
Cách làm nào cũng có vấn đề. Nếu bạn đã có kinh nghiệm thì sẽ thấy được những bất cập như sau
• Cách 1 là thủ pháp rất "ma đạo", thay đổi giá trị tham số ra - vào, đi ngược lại với các best-practise trong programming. Người làm sau sẽ dễ bị "dính bẫy" khi không để ý sự thay đổi của tham số
• Cách làm 2 có thể dùng tốt trong các trường hợp kiểu dữ liệu mới là một kiểu dữ liệu có ý nghĩa. Nếu mục đích của bạn chỉ đơn giản là muốn gói 2 giá trị và gượng ép tạo ra một kiểu dữ liệu không có ý nghĩa thì code sẽ khó đọc và không trong sáng.
Cách làm được khuyến khích là tạo kiểu dữ liệu Pair.
class Pair[T1, T2](val t1: T1, val t2: T2) {
override def toString(): String = "(" t1 "," t2 ")"
}
Pair có T1 và T2 là 2 Type Parameter, có nghĩa là 2 giá trị sử dụng có thể là các kiểu khác nhau tùy thích. Giờ chúng ta có thể sử dụng kiểu Pair để "hứng" giá trị trả về của một method, ở đây là method teamUp.
class Saiyan(name: String, power:Int) {
def change(): Saiyan = new Saiyan(name, power * 100)
override def toString(): String = name '/' power.toString
}
class Namek(name: String, power:Int) {
def change(): Namek = new Namek(name, power * 10)
override def toString(): String = name '/' power.toString
}
def teamUp(left: Saiyan, right: Namek): Pair[Saiyan, Namek] = new Pair[Saiyan, Namek](left.change(), right.change())
val goku = new Saiyan("Kakarot",200) // Saiyan = Kakarot/200
val picolo = new Namek("Gabriel",70) // Namek = Gabriel/70
teamUp(goku,picolo) // Pair[Saiyan,Namek] = (Kakarot/20000,Gabriel/700)
Ở đây kiểu trả về của teamUp là Pair[Saiyan, Namek].Chúng ta đã vừa implement kiểu dữ liệu Tuple.
Tuple là một kiểu hay được sử dụng, vì thế Scala đã chuẩn bị sẵn 22 Tuple có sẵn từ Tuple1 đến Tuple22. 22 kiểu Tuple này cho phép tạo đến 22 giá trị trong cùng một nhóm. Tạo instance cho Tuple rất đơn giản
new Tuple2(goku, picolo)
hoặc có thể ngắn hơn nữa
(goku, picolo)
Variance
Liên quan đến Type Parameter, mình sẽ viết về Covariant (hiệp biến) và Invariant (bất biến). Phải công nhận là từ tiếng Việt khó nhớ và khó hiểu nên mình sẽ dùng thuật ngữ tiếng Anh trong bài này.
Invariant
Thế nào là Invariant?
Khi chúng ta có 1 class C, 2 type parameter T1 và T2, chỉ khi nào T1=T2 thì biểu thức gán sau mới được chấp nhận:
Cách suy nghĩ của Invariant thực ra rất tự nhiên và phù hợp với logic thông thường. Khi 2 kiểu là khác nhau thì không thể có chuyện một class khi nhận 2 kiểu đấy lại gán cho nhau được.
Array trong Java là một class, lẽ ra cần phải là Invariant, nhưng đã bị thiết kế thành Covariant. Đây là một điểm yếu trong thiết kế ngôn ngữ của Java.
Covariant
Khi chúng ta có 1 class C, 2 type parameter T1 và T2, chỉ khi nào T2 kế thừa T1 thì biểu thức gán sau mới được chấp nhận:
Trong Scala khi chỉ định một Type Parameter thì sẽ được mặc định là Invariant. Nếu muốn chỉ rõ Covariant thì cần phải viết:
class G[ T]
Lý thuyết về Invariant và Covariant hơi trừu tượng và khó hiểu. Mình sẽ lấy ví dụ ở dưới đây. Ngôn ngữ là Java. Bạn hãy nhìn G là array, T1 là String, T2 là Object.
Object[] objects = new String[1];
objects[0] = 100;
Đoạn code trên có gì bất thường ?
Bạn hãy để ý nhé, chúng ta đang lấy một String array gán vào một Object array. Đoạn code trên có thể compile bình thường nhưng khi chạy sẽ ném ra Exception java.lang.ArrayStoreException. Lý do là ở dòng 2 đã ném một giá trị int vào String array.
Cùng một logic như trên, Scala sẽ bắt lỗi được khi compile ở dòng đầu tiên.
scala> val arr: Array[Any] = new Array[String](1)
found : Array[String]
required: Array[Any]
Sự khác nhau này đến từ thiết kế. Array trong Scala là Invariant trong khi Array trong Java là Covariant như đã nói ở trên. Ở khía cạnh này thì Scala được coi là Type Safe hơn Java, vì có thể bắt lỗi được từ lúc compile.
Quay lại một chút với ví dụ về Pair ở phần trước. Mình sẽ biến Pair thành Covariant. Hãy để ý dấu nhé.
class Pair[ T1, T2](val t1: T1, val t2: T2) {
override def toString(): String = "(" t1 "," t2 ")"
}
Bây giờ thì Pair đã là Covariant và khai báo như sau sẽ hoàn toàn ko có vấn đề gì xảy ra
val pair: Pair[AnyRef, AnyRef] = new Pair[Saiyan, Namek](goku, picolo)
Ở đây biến pair, định nghĩa bởi từ khóa valđã được gán giá trị và không thể thay đổi giá trị được nữa. Những biến đã gán giá trị xong và không thể thay đổi được, gọi là Immutable . Những biến immutable thì không thể xảy ra Exception java.lang.ArrayStoreException, vì thế có thể định nghĩa theo kiểu Covariant thoải mái mà không vấn đề gì.
Contravariant
Ngoài Invariant và Covariant thì còn Contravariant nữa. Định nghĩa như sau:
Khi chúng ta có 1 class C, 2 type parameter T1 và T2, chỉ khi nào T1kế thừa T2 thì biểu thức gán sau mới được chấp nhận:
val : G[T1] = G[T2]
class G[-T]
Ứng dụng của Contravariant là trong kiểu của hàm (function type). Chúng ta chỉ có thể gán kiểu hàm AnyRef => AnyRef(T2) cho String => AnyRef(T1) mà không thể gán ngược lại.
val x1: AnyRef => AnyRef = (x: String) => (x:AnyRef)
// Type mismatch
val x1: String => AnyRef = (x: AnyRef) => (x:AnyRef)
// Ok
>> Xem thêm: Stress / Scenario test sử dụng Gatling