[Scala] - day 13 : Advanced Typing

Chúng ta đã đi đến cuối con đường cho mục đích tìm hiểu về Scala basic, nếu bạn đã theo dõi đến bài này tôi nghĩ bạn đã có 1 nắm bắt nhất định về ngôn ngữ Scala, định nghĩa 1 class theo đúng chuẩn Scala, viết những function và làm việc với collection. Kiến thức đó là đủ để bạn có thể bắt tay vào xây dựng application Scala đầu tiên của mình.
Trong quá trình xây dựng application đầu tiên có lẽ bạn sẽ bắt đầu đọc code cũng như tìm hiểu những phần code của các lập trình viên khác, hay tìm tòi về những Scala API cũng như cách Scala làm việc, đó là lý do tôi tạo thêm bài viết này trong phần Scala basic. Trong bài viết này tôi sẽ cố gắng cover thật nhiều những feature mà tôi nghĩ nó làm nên đặc trưng của ngôn ngữ này.

Tuple và Function Value Class

Tuple

Chúng ta đã biết về Tuple và Function Value, tuy nhiên có 1 chút "bí mật" đằng sau cú pháp khai báo Tuple như (1, 2, true) và khai báo function literal như (n: String) => s"Hello, $n" . Cú pháp đó là 1 shortcut để tạo ra những instance 1 cách ngắn hơn và nhanh hơn.
Tuple được implement như là một instance của case class TupleX[Y], với “X” (gọi là arity) là 1 số từ 1 tới 22 (tương ứng với số input parameter). Và “Y” ứng với kiểu của input parameter. Tuple1[A] có 1 input parameter kiểu A và Tuple2[A,B] có 2 input parameter và có kiểu lần lượt là A và B. Khi bạn tạo 1 tuple với cấu trúc (1, 2, true) tương ứng với việc bạn khởi tạo 1 tuple với case class Tuple3[Int,Int,Boolean] .
Mỗi case class TupleX[Y] đều extend một trait ProductX với cùng arity (cùng số X). Tất cả đều cung cấp những operation như productArity (trả về arity của tuple ) và productElement (là 1 cách không an toàn để access tới element của tuple). Chúng cũng bao gồm companion objects và implement function unapply.
Thử vài ví dụ :
scala> val x: (Int, Int) = Tuple2(10, 20)
x: (Int, Int) = (10,20)
scala> println("Does the arity = 2? " + (x.productArity == 2))
Does the arity = 2? true
Trong Scala nếu ta coi Tuple case class là data-centric (dữ liệu core) để implement 1 syntax thì Function Value Class sẽ là logic-centric.

Function Value Class

Function values được implement như là một instance của trait FunctionX[Y],  X (arity) từ 0 đến 22, và “Y” có ý nghĩa tương tự.
Khi bạn tạo ra 1 function literal, Scala compiler sẽ converts nó thành body của method apply() trong class mới mà extend FunctionX. Cơ chế này làm cho function value trong Scala tương thích với JVM.
scala> val hello1 = (n: String) => s"Hello, $n"
hello1: String => String = <function1>

scala> val h1 = hello1("Function Literals")
h1: String = Hello, Function Literals

scala> val hello2 = new Function1[String,String] {
         |             def apply(n: String) = s"Hello, $n"
         |  }
hello2: String => String = <function1>

scala> val h2 = hello2("Function1 Instances")
h2: String = Hello, Function1 Instances

scala> println(s"hello1 = $hello1, hello2 = $hello2")
hello1 = <function1>, hello2 = <function1>
Với những thông tin trên, tôi chắc ví dụ này hoàn toàn dễ hiểu với bạn. Function Value lưu trong hello1 và hello2 về bản chất hoàn toàn giống nhau, và bạn đã có thể lý giải kết quả trả về "String => String = <function1>" tại sao lại là "function1".
Trait Function1 có method đặc biệt (andThen và compose) mà không có trong Function0 hay bất kì trait FunctionX nào khác. Method đó hỗ trợ bạn combine 2 hoặc nhiều instances của Function1 thành một instance Function1 mới bằng cách execute tất cả function đó khi nó được gọi đến. Hạn chế duy nhất là kiểu trả về của function thứ nhất phải match với input type của function thứ 2 và cứ thế tiếp theo nếu có nhiều hơn 2 function. 
Method andThen tạo ra 1 function value mới từ 2 function value , function bên trái sẽ được chạy trước sau đó kết quả được truyền vào function bên phải,  method compose làm việc với chiều ngược lại.
scala> val doubler = (i: Int) => i*2
doubler: Int => Int = <function1>

scala> val plus3 = (i: Int) => i+3
plus3: Int => Int = <function1>

scala> val prepend = (doubler compose plus3)(1)
prepend: Int = 8
// plus được thực hiện trước và kết quả truyền vào doubler
 
scala> val append = (doubler andThen plus3)(1)
append: Int = 5
// doubler được thực hiện trước và kết quả truyền vào plus
Hiểu cách vận hành của first-class functions trong việc implement như class FunctionX là bước đầu tiên quan trọng để học Scala’s type model.

Implicit conversion (chuyển đổi ngầm)

Scala compiler tự động sử dụng cơ chế chuyển đổi ngầm (implicit conversion) khi nó tìm thấy 1 unknown field hoặc method được gọi đến qua instance. Trong quá trình compile nếu như trình biên dịch thấy 1 unknown field hay unknown method thì nó sẽ đối chiếu tất cả implicit mà nó có được ở space đó, nếu tìm thấy implicit hợp lý nó sẽ tự động sử dụng implicit đó thay cho unknown field hay unknown method, tất nhiên khi không có implicit nào được định nghĩa có thể thay thế nó rẽ trả về lỗi unknown.

Implicit Parameters - biến ngầm định

Trong bài day 4 : First-Class Functions  chúng ta đã biết đến Partially Applied Functions, một function có thể gọi mà không cần truyền đầy đủ parameters. Điều gì sẽ xảy ra khi chúng ta gọi 1 function nhưng không truyền đầy đủ parameter ? phần bị thiếu sẽ phải đến từ "đâu đó" để chắc chắn function chạy đúng.Có 1 cách mà ta biết từ trước là sử dụng default parameter nhưng để làm được điều này ta phải chắc chắn parameter bị thiếu là gì.
Có 1 cách khác đó là sử dụng implicit parameter (biến ngầm định) .Tất cả function đều có thể define 1 implicit parameter , thường nó nằm ở group khác với những nonimplicit parameter (nếu khó hiểu bạn có thể xem lại parameter group). Invoker có thể  chỉ định local value là implicit và nó được sử dụng làm implicit parameter.Nghĩa là khi function được gọi đến mà không chỉ rõ value cho implicit thì local implicit value sẽ được chọn và tự gán vào implicit parameter của function.
Sử dụng từ khoá implicit để chỉ định một value, variable, hoặc function parameter là implicit. Một implicit value hay variable mà available trong namespace hiện tại có thể sử dụng để fill cho 1 implicit parameter trong , may be used to fill in for an implicit parameter cho function được gọi.
Ví dụ :

scala> object Doubly {
        |      def print(num: Double)(implicit fmt: String) = {
        |          println(fmt format num)
        |      }
        |  }
defined object Doubly

scala> Doubly.print(3.724)
<console>:9: error: could not find implicit value for parameter fmt: String
                        Doubly.print(3.724)

scala> Doubly.print(3.724)("%.1f")
3.7
Lần này ta sẽ sử dụng local value nhé :
scala> case class USD(amount: Double) {
        |       implicit val printFmt = "%.2f"
        |       def print = Doubly.print(amount)
        |  }
defined class USD

scala> new USD(81.924).print
81.92
Ta gọi lại  Doubly.print nhưng vẫn chỉ truyền 1 số ta thấy local value khai báo trong cùng space được tự động fill vào.
Sử dụng implicit parameter quá nhiều sẽ gây khó đọc hiểu hãy cẩn thận khi trong thiết kế của bạn cần đến implicit parameter.

Implicit Class - Lớp ngầm định

Tương tự như implicit parameter,  nhưng khác nó làm việc với class Một implicit class là 1 class mà  cung cấp 1 cơ chế chuyển đổi tự động từ 1 class khác. Với việc chuyển đổi tự động từ instance kiểu A sang kiểu B, instance kiểu A có thể sẽ có các field và method như một instance kiểu B.
Đơn giản như giờ ta gọi 1 class kiểu A với 1 method không được khai báo trong A , trình biên dịch sẽ trả về lỗi nếu không tìm thấy, vậy làm sao để khai báo "thêm" 1 method cho class A và k thay đổi chính class ấy, trường hợp này bạn có thể dùng implicit class.
Trước tiên để khai báo implicit class ta phải tuẩn thủ các điều sau :
  1. Một implicit class phải được định nghĩa trong 1 object, class hay trait. May là, implicit class định nghĩa bên trong object thì dễ import.
  2. Mỗi implicit class chỉ nhận vào 1 nonimplicit class argument 
  3. Tên của implicit class không được conflict với object, class hay trait khác trong cùng namespace => case class không thể sử dụng như là implicit class vì nó tự khởi tạo object companion, điều này break rule.
OK, tạo vì ví dụ cho rõ ràng hơn :
scala> object IntUtils {
        |      implicit class Fishies(val x: Int) {
        |          def fishes = "Fish" * x
        |      }
        |  }
defined object IntUtils
 
scala> import IntUtils._
import IntUtils._

scala> println(3.fishes)
FishFishFish
"3" là 1 instance của class Int, và khi tôi gọi method fishes thông qua 3 nó sẽ là unknown method 
=> khi này cơ chế chuyển đổi ngầm của Scala sẽ hoạt động và nó tìm thấy  tôi đã khai báo 1 implicit class nhận vào 1 số Int với method fishes 
=> instance của Int sẽ được chuyển ngầm sang instance Fishies và instance Fishies thì có method fishes 
=> ta có được kết quả như trên

Có 1 số chú ý khi bạn khai báo 1 implicit ngoài những điều trên, đó là Scala có những implicit class mặc định được khai báo trong object scala.Predef. Và tất nhiên nó được import tự động. Một trong số đó được sử dụng rất nhiều trong các phần trước đó là "->"  để khai báo 1 tuple.
Cấu trúc đơn giản của method "->" trong object scala.Predef như sau :
implicit class ArrowAssoc[A](x: A) {
    def ->[B](y: B) = Tuple2(x, y)
}
Như cấu trúc trên ta có thể implicit method "->" cho tất cả class (nếu các bạn còn nhớ tất cả các kiểu trong scala đều là 1 class - data trong Scala) , ví dụ 1 -> "a" sẽ trả về cho ta Tuple2(1,"a") hay "tên"->"HoaiPT" sẽ trả về Tuple2("ten","HoaiPT")
Implicit class sẽ rất hữu dụng nếu bạn dùng nó cẩn thận, code sẽ nhanh hơn và không cần thay đổi Class đang tồn tại, tuy nhiên khi có quá nhiều implicit class sẽ gây khó đọc, và tôi chắc chắn bất cứ lập trình viên nào cũng sẽ nhăn lại khi nhìn thấy "fishes" trong code của bạn. 

Advance Type 

Type Alias - bí danh

Chúng ta sử dụng type alias để đặt lại tên mới rõ ràng cho kiểu có sẵn (hoặc class). Tên mới được định nghĩa lại qua type alias được compiler xử lý như các class bình thường khác. Bạn có thể tạo mới một instance từ type alias.
Syntax: Defining a Type Alias 
type <identifier>[type parameters] = <type name>[type parameters]
Thử vài ví dụ với Type Alias
scala> object TypeFun {
        |       type Whole = Int
        |       val x: Whole = 5
        |    
        |       type UserInfo = Tuple2[Int,String]
        |       val u: UserInfo = new UserInfo(123, "George")
        |    
        |       type T3[A,B,C] = Tuple3[A,B,C]
        |       val things = new T3(1, 'a', true)
        |  }
defined object TypeFun

scala> val x = TypeFun.x
x: TypeFun.Whole = 5

scala> val u = TypeFun.u
u: TypeFun.UserInfo = (123,George)

scala> val things = TypeFun.things
things: (Int, Char, Boolean) = (1,a,true)
Ở ví dụ trên, kiểu Whole là 1 alias cho abstract class Int, và tương tự UserInfo là 1 alias cho tuple với 1 số Int và 1 string. Và như đã nói Tuple2 là 1 instantiable case class, chúng ta có thể tạo 1 instance trực tiếp từ type alias UserInfo.Cuối cùng, T3 không nói rõ nó có những kiểu dữ liệu nào, vậy nên nó có thể tạo instance với bất kỳ kiểu nào.
Type alias sẽ rất hữu ích trong những trường hợp bạn cần tham chiếu 1 kiểu dữ liệu tiêu chuẩn với local với 1 tên rõ ràng hơn.

Abstract Type - kiểu trừu tượng

Nếu type alias làm việc với single class, abstract type có thể làm việc với 0, 1 hoặc nhiều class. Cách làm việc tương tự với type alias but được chỉ rõ là abstract và không thể tạo instance từ nó.Hiểu đơn giản nó là 1 trait với alias type không được định nghĩa rõ ràng.
Ví dụ :
scala> class User(val name: String)
defined class User

scala> trait Factory { type A; def create: A }
defined trait Factory

scala> trait UserFactory extends Factory {
        |      type A = User
        |      def create = new User("")
        |  }
defined trait UserFactory
Bạn thấy abstract type A trong Factory được sử dụng là kiểu trả về cho method create. Kiểu A sẽ dc chỉ rõ trong 1 lớp con cụ thể như bạn thấy ở lớp UserFactory.
Hoặc ví dụ trên có thể viết lại như này :
scala> trait Factory[A] { def create: A }
defined trait Factory

scala> trait UserFactory extends Factory[User] { def create = new User("") }
defined trait UserFactory
Abstract type sẽ là 1 giải pháp rất tốt khi bạn tạo ra các class khởi tạo với input là parameter type. Ở ví dụ trên không có giới hạn nào cho kiểu có thể truyền vào, có lẽ ta nên sử dụng Bounded type.

Bounded type

Bounded type sẽ giới hạn cho 1 class cụ thể hay sbutype hoặc kiểu cơ bản. Có 2 kiểu bounded type :
  • upper bound : giới hạn đúng type đó và lớp con của nó.
  • lower bound : giới hạn đúng type đó và lớp cha của nó.
Ta khai báo trước vài class :
scala> class BaseUser(val name: String)
defined class BaseUser

scala> class Admin(name: String, val level: String) extends BaseUser(name)
defined class Admin

scala> class Customer(name: String) extends BaseUser(name)
defined class Customer

scala> class PreferredCustomer(name: String) extends Customer(name)
defined class PreferredCustomer
Syntax: Upper Bounded Types 
<identifier> <: <upper bound type>
Thử vài thử nghiệm với upper bound type :
scala> def checkUpperBound[A <: BaseUser](u: A) {
       |   if (u.name.isEmpty) println("Fail!")
       |}
check: [A <: BaseUser](u: A)Unit

scala> check(new Customer("Fred"))

scala> check(new Admin("", "strict"))
Fail!
Tôi tạo 1 check function với đầu vào nói rằng chỉ nhận vào kiểu BaseUser hoặc các lớp con của nó, và điều kiện check là name, BaseUser và các lớp con từ nó luôn có parameter name.

Syntax: Lower Bounded Types
<identifier> >: <lower bound type>
Lower bound hơi miễn cưỡng và khó kiểm soát theo hiểu biết của tôi, giờ tôi sẽ tạo 1 function tương tự trên khác là đầu vào tôi muốn là Customer và toàn bộ lớp cha của nó :
scala> def checkLowerBound[A >: Customer](u: A) {
        |      if (u.name.isEmpty) println("Fail!")
        |  }
<console>:13: error: value name is not a member of type parameter A
            def checkLowerBound[A >: Customer](u: A) { if (u.name.isEmpty) println("Fail!") }
Cũng dễ hiểu phải không, lớp cha của Custom chưa chắc có parameter name, để function này chạy được ta phải giới hạn cho nó :
scala> def checkLowerBound[A >: Customer <: BaseUser](u: A) {
        |       if (u.name.isEmpty) println("Fail!")
        |  }
checkLowerBound: [A >: Customer <: BaseUser](u: A)Unit
Tôi thêm vào giới hạn là cha của Customer nhưng là con của BaseUser , khi đó chắc chắn instance bạn truyền vào sẽ có parameter name.
Và sắp tới đây tôi sẽ nói cho bạn biết vì sao mà với tôi lower bound hơi "thừa thãi" và khó kiểm soát, ta đã biết lower bound là giới hạn chính class đó và cha của nó, tuy nhiên lớp con của nó cũng được coi là chính nó, do đó bạn có thể truyền vào PreferredCustomer mà không có thông báo lỗi, bởi vì PreferredCustomer là PreferredCustomer và cũng là Customer.
scala> checkLowerBound(new PreferredCustomer("Fred"))
*) nếu bạn có kiến thức sâu hơn và có thể kiểm soát lower bound tốt hơn ,xin hãy comment, tôi rất mong nhận được góp ý.Thanks.

Type Variance 

Nếu upper bound và lower bound giới hạn parameter truyền vào thì type variance lại làm cho nó mở rộng hơn.Type variance chỉ rõ làm sao để type parameter thích hợp với kiểu cơ bản hay subtype.
Mặc định type parameter là bất biến ( invariant ) , ví dụ :
scala> class Car { override def toString = "Car()" }
defined class Car

scala> class Volvo extends Car { override def toString = "Volvo()" }
defined class Volvo

scala> val c: Car = new Volvo()
c: Car = Volvo()
Mọi thứ vẫn trong tầm chúng ta kiểm soát, Volvo extends từ Car nên tất nhiên Volvo cũng được coi là Car. Nhưng nếu ta dùng chúng cho type parameter thì sao :
scala> case class Item[A](a: A) { def get: A = a }
defined class Item

scala> val c: Item[Car] = new Item[Volvo](new Volvo)
<console>:12: error: type mismatch;
  found : Item[Volvo]
  required: Item[Car]
Note: Volvo <: Car, but class Item is invariant in type A.
You may wish to define A as +A instead. (SLS 4.5)
               val c: Item[Car] = new Item[Volvo](new Volvo)
Oh,không được với type parameter,  Item[Volvo] không gán được cho Item[Car], và chúng ta cần tới covariant . Covariant type parameter có thể tự động chuyển đổi sang kiểu cơ bản nếu cần (A extends B => B có thể coi là kiểu cơ bản của A) .Thử lại ví dụ trên với covariant :
scala> case class Item[+A](a: A) { def get: A = a }
defined class Item

scala> val c: Item[Car] = new Item[Volvo](new Volvo)
c: Item[Car] = Item(Volvo())

scala> val auto = c.get
auto: Car = Volvo()
Covariant thật tuyệt vời đúng không , tuy nhiên không phải lúc nào nó cũng làm việc tốt, ví dụ khi nó là type parameter cho input của method. Ví dụ :
scala> class Check[+A] { def check(a: A) = {} }
<console>:7:error: covariant type A occurs in contravariant position in type A of value a
                class Check[+A] { def check(a: A) = {} }
Scala compiler nói rằng type parameter được sử dụng ở method là contravariant, không phải covariant .
Để khai báo 1 contravariant ta dùng dấu trừ ( - ) đằng trước parameter type.Chúng có thể sử dụng cho input parameter chứ không dùng để khai báo kiểu trả về.Return type luôn phải là covariant.
Ta viết lại ví dụ :
scala> class Check[-A] { def check(a: A) = {} }
defined class Check
Thử thêm vài ví dụ để biết sự khác biệt giữa chúng :
scala> class Car; class Volvo extends Car; class VolvoWagon extends Volvo
defined class Car
defined class Volvo
defined class VolvoWagon

scala> class Item[+A](a: A) { def get: A = a }
defined class Item

scala> class Check[-A] { def check(a: A) = {} }
defined class Check

scala> def item(v: Item[Volvo]) { val c: Car = v.get }
item: (v: Item[Volvo])Unit

scala> def check(v: Check[Volvo]) { v.check(new VolvoWagon()) }
check: (v: Check[Volvo])Unit
Covariancecontravariance giảm bớt giới hạn cho type parameters , nhưng chúng lại có những giới hạn riêng về cách sử dụng chúng.

Packgage Object

Chúng ta đã biết về package ở phần trước , và như bạn đã biết nếu chúng ta muốn dùng lại 1 class hay 1 package nào đó chúng ta phải import nó vào space hiện tại, có 1 cách để việc này tự động đó là sử dụng package object.
scala.Predef object được tự động thêm vào toàn bộ trong namespace trong Scala.Vậy làm sao để định nghĩa 1 package object , ví dụ :
package object oreilly {
    type Mappy[A,B] = collection.mutable.HashMap[A,B]
}
Và tất cả class, trait và object định nghĩa cùng package sẽ tự động import Mappy[A,B] và có thể sử dụng chúng. Và phần core của Scala cũng được định nghĩa tương tự.
 Package Object sẽ rất hữu dụng cho bạn khi bạn muốn định nghĩa những phần core , hay advance type cho toàn bộ project.
Ok, ngày hôm nay quá dài rồi , tạm biệt các bạn ở phần Scala basic , phần tới ta hãy cùng tìm hiểu về Play Framework, 1 frameword vô cùng mạnh mẽ cho Scala.



 

Comments