Scala: Type Parameter
Scala
50
White

Vu Nhat Minh viết ngày 31/05/2016

Chào mừng bạn đến với thế giới của lập trình hàm, hôm nay chúng ta sẽ làm quen với Type Parameter trong Scala!

Type Parameter

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 + ")"
}

PairT1T2 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 teamUpPair[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 T1T2, chỉ khi nào T1=T2 thì biểu thức gán sau mới được chấp nhận:

val : G[T1] = G[T2]

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.

Khi Java lúc đầu mới được tạo ra (lý do mang tính hoàn cảnh lịch sử :smile:) thì khái niệm Generic (a.k.a parametric polymorphism ) chưa phổ biến, vì thế creator của Java đã để array thành Covariant.

Chờ đã, vẫn chưa nói thế nào là Covariant cơ mà ?

Covariant

Khi chúng ta có 1 class C, 2 type parameter T1T2, chỉ khi nào T2 kế thừa T1 thì biểu thức gán sau mới được chấp nhận:

val : G[T1] = G[T2]

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)
<console>:7: error: type mismatch;
 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 InvariantCovariant thì còn Contravariant nữa. Định nghĩa như sau:

Khi chúng ta có 1 class C, 2 type parameter T1T2, 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]

Ngược với Covariant đúng không :smile:

Trong Scala, khi muốn định nghĩa kiểu Contravariant thì dùng dấu -

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

Kết

Học lập trình hàm cũng rèn luyện được đầu óc kha khá đấy chứ nhỉ :smile:. Phần tiếp theo sẽ là về Function nhé.

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Vu Nhat Minh

54 bài viết.
775 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
119 30
Nếu bạn thường vào trang mua sắm của amazon, chắc sẽ chẳng lạ gì với menu Shop by Department. Tốc độ hiển thị nội dung của menu là tức thì so với d...
Vu Nhat Minh viết gần 3 năm trước
119 30
White
93 4
Lời người dịch Người dịch là một developer , sau khi tìm đọc được bài viết này bằng bản gốc tiếng Anh đã cảm thấy như được "khai sáng" về khả năng...
Vu Nhat Minh viết 3 năm trước
93 4
White
61 7
Form là thành phần quan trọng nhất khi design flow đăng ký của 1 web hay 1 app, dù là view gồm nhiều bước hay chỉ là một màn hình đơn điệu. Bài này...
Vu Nhat Minh viết hơn 1 năm trước
61 7
Bài viết liên quan
White
10 0
Kí tự Regex cơ bản Về cơ bản thì các sử lý matching của scala.util.matching.Regex sẽ được "phó thác" (delegate) cho java Regex. Bạn có thể tạo một ...
huydx viết 3 năm trước
10 0
White
7 1
Trong scala kí tự _ được dùng với khá nhiều mục đích .. không liên quan đến nhau. Tạm note lại cái đã khi nào có time sẽ quay lại viết cẩn thận sa...
huydx viết 3 năm trước
7 1
White
0 0
(Bài viết hơi khó hiểu, dành cho bạn nào có hứng thú với type programming trong scala với các thư viện như shapeless chẳng hạn) Thông thường với m...
huydx viết hơn 2 năm trước
0 0
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
54 bài viết.
775 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!