Immutable in Scala

Cái tên Scala xuất phát từ Scalability mang ý nghĩa về khả năng phát triển mở rộng dễ dàng. Vậy về mặt ngôn ngữ lập trình, Scala có gì khác biệt với những ngôn ngữ khác? Rất đơn giản, Scala là sự kết hợp giữa Ngôn ngữ lập trình hướng đối tượng(Object Oriented Programming) và Lập trình chức năng (Functional Programming). Và điều tạo nên sức mạnh cho Functional Programming chính là các biến số không thể thay đổi được (Immutable). Hãy bắt đầu từ những điều cơ bản

1. Variable: Val and Var

Nếu như làm quen với Scala thông qua các bản Tutorial, chắc hẳn rất nhiều người sẽ thắc mắc về việc sử dụng valvar khi cả hai đều được sử dụng để khai báo biến số.

Var

var được dùng để khai báo các biến và các giá trị của biến số có thể thay đổi giá trị được trong suốt quá trình thực hiện chương trình.

scala> var luckyNumber: Integer = 10;
luckyNumber: Integer = 10

scala> luckyNumber
res0: Integer = 10

scala> luckyNumber = 20
luckyNumber: Integer = 20

scala> luckyNumber
res1: Integer = 20

Nếu như ai đã làm quen với Java hay Javascript thì có thể thấy biến var không có gì quá đặc biệt. Sự khác biệt của Scala đến từ val

Val

Cũng dùng để khai báo biến, thế nhưng sau khi đã được khởi tạo thì giá trị của val không thể thay đổi được

scala> val specialNumber: Integer = 7
specialNumber: Int = 7

scala> specialNumber = 10
<console>:8: error: reassignment to val
       specialNumber = 10

scala> specialNumber
res3: Int = 7

Trong quá trình làm việc với Scala, val được khuyến khích sử dụng và có thể coi là mặc định khi bắt đầu khai báo hàm, trong khi var chỉ sử dụng khi thực sự cần thiết. Điều này giúp cho "Side effect" được giảm thiểu tối đa trong Scala

2. Data Type

Xuất phát từ val và các loại dữ liệu cơ bản như Integer, String, Array... Scala tạo ra những loại dữ liệu cơ bản có cùng đặc điểm là không thể thay đổi được phục vụ cho Functional Programming.

Tất cả các định dạng dữ liệu của Scala được xây dựng thừ thư viện Collection của Scala bao gồm Scala List, Scala Tuple, Scala Map, Scala Set, Scala Option và Scala Iretator. Collection bao gồm cả những thuộc tính thay đổi được (Mutable) lẫn không thể thay thổi được (Immutable). Trong phần này chúng sẽ đi sâu vào Immutable của Collection

2.1 List

Cũng như bao ngôn ngữ khác, Scala cũng có loại dữ liệu mảng (Array) tuy nhiên các giá trị bên trong Array có thể thay đổi được

scala> val hello = Array("Hello", "the", "world")
hello: Array[String] = Array(Hello, the, world)

scala> hello
res8: Array[String] = Array(Hello, the, world)

scala> hello.update(0, "Goodbye")

scala> hello
res10: Array[String] = Array(Goodbye, the, world)

Ta có thể thấy là mảng hello đã bị thay đổi giá trị đầu tiên "Hello" -> "Goodbye" do kiểu dữ liệu Array có thể thay đổi giá trị các thành phần bằng hàm update. Để tránh được điều này, Scala đưa ra kiểu dữ liệu List dùng cho mảng dữ liệu có dùng loại (Integer, String) và đặc biệt là không thay đổi giá trị được. Chúng ta hoàn toàn có thể thây rằng trong số các hàm đặc trưng của List, không có hàm nào có khả năng thay đổi giá trị của List. Sé có nhiều người thắc mắc về toán tử ::::: của List khi có khẳ năng thêm giá trị vào List. Tuy nhiên hãy lưu ý là hai toàn tử này tạo ra List mới chứ không thay đổi giá trị List cũ.

Tại sao chúng ta không thể thêm giá trị(append) vào trong List?

Class List không cung cấp toàn tử append vì thời gian thực hiện của toán tử append sẽ tăng tuyến tính với kích thước của List trong việc tạo ra một List với toán tử :: chỉ chiếm 1 khoảng thời gian cố định

2.2 Tuple

Với List, chúng ta chỉ có thể chứa đựng những giá trị thuộc cùng một loại như List[Int] sẽ chứa toàn Integer trong khi List[String] sẽ là String. Với Tuple, chúng ta có thể lưu trữ loại dữ liệu hỗn hợp. Và cũng giống như List, Tuple là bất biến từ khi được khai báo.

scala> val address = (202, "River Side street")
address: (Int, String) = (202,River Side street)

scala> address._1
res18: Int = 202

scala> address._2
res19: String = River Side street

2.3 Set and Map

Khác với List và Tuple, chỉ để chứa đựng các giá trị bất biến (Immutable Object), cả Set và Map vừa là Mutable lẫn Immuatable. Có thể dễ dàng nhận ra rằng List và Tuple đơn giản là một dạng mở rộng từ Array, tuy vậy với Set và Map thì đã mang những đặc điểm riêng biệt của Scala.

Set Set là một dạng dữ liệu thuộc Collection với mục tiêu chứa dựng những giá trị khác nhau có cùng thuộc tính. Như đã đề cập ở trên, Set có cả Mutable và Immutable, tuy nhiên mặc định khi khởi tạo, Set ở trạng thái ko thể thay đổi được giá trị (Immutable), điều này có nghĩa là Set không có khả năng tự thay đổi giá trị khi đã được khai báo dưới thư viện scala.collection.immutable.Set

scala> var b = Set(1,2,3,4)
b: scala.collection.immutable.Set[Int] = Set(1, 2, 3, 4)

scala> b += 5

scala> b
res3: scala.collection.immutable.Set[Int] = Set(5, 1, 2, 3, 4)

Vậy tại sao trong ví dụ trên, Set đã bị thay đổi? Trong Set, bao gồm cả Immutable lẫn Mutable, toán tử + đều có thể sử dụng được. Tuy nhiên cách thức thực hiện lại khác nhau. Nếu như trong Mutable Set, chỉ đởn giản là Set cũ với giá trị mới được thêm vào trong khi Immutable Set sẽ tạo ra một Set hoàn toàn mới được với giá trị đã được mở rộng từ Set cũ. Tuy nhiên, nếu bạn muốn Set thực sự không thay đổi được, hãy khai báo bằng val.

Map

Map là một thuộc tính của Collection nhằm lưu trữ dữ liệu dưới dạng key -> value. Cũng giống như Set, Map có hai loại cơ bản là thay đổi được (Mutable) và không thể thay đổi được (Immutable) và khởi tạo mặc định của Map cũng thuộc dạng Immutable.

scala> val speech = Map(1 -> "Hello", 2 -> "World")
speech: scala.collection.immutable.Map[Int,String] = Map(1 -> Hello, 2 -> World)

scala> speech += (3 -> "The God")
<console>:9: error: value += is not a member of scala.collection.immutable.Map[Int,String]
              speech += (3 -> "The God")
                     ^

Trong khi đó, chúng ta có thể thay đổi giá trị dễ dàng khi sử dụng thư viện `scala.collection.mutable.Map

scala> import scala.collection.mutable.Map
import scala.collection.mutable.Map

scala> val speech: Map[Int, String] = Map()
speech: scala.collection.mutable.Map[Int,String] = Map()

scala> speech += (1 -> "Beautiful")
res1: speech.type = Map(1 -> Beautiful)

scala> speech += (2 -> "World")
res2: speech.type = Map(2 -> World, 1 -> Beautiful)

3. Immutable in Functional Object

Để có minh hoạ cho phần này, chúng ta hãy nhìn vào class sau

class Rational(n: Int, d: Int) {
    require(d != 0)
    val numer: Int = n
    val denom: Int = d
    override def toString = numer +"/"+ denom
    def add(that: Rational): Rational =
      new Rational(
        numer * that.denom + that.numer * denom,
        denom * that.denom
) }

Ở đây, class Rational được xây dựng để khởi tạo một phân số có dạng n/d trong đó n là tử số còn d là mẫu số. Ngoài ra, class này còn định nghĩa thêm hai hàm là làm cộng hai phân số và hàm hiển thị phân số (toString). Hãy bắt đầu từ các tham số:

  • n và d: đây là hai tham số của class được sử dụng để xây dựng hàm. Về mặc định nd là hai biến val, không thể thay đổi được giá trị. Vậy nếu Rational được viết như sau thì sao?
class Rational(n: Int, d: Int) {
	require(d != 0)
	override def toString = n +"/"+ d
	def add(that: Rational): Rational =
		new Rational(n * that.d + that.n * d, d * that.d)
}

Đoạn code trên sẽ không thể thực thi khi hai params n và d không phải là thành phần của class Rational. Đó là lý do tại sao bên trong chúng ta phải khai báo thêm hai biến không đổi tương ứng là numberdenom

  • Hàm add: đây là hàm tính toán phép cộng hai phân số.chúng ta có thể thấy rằng hàm này trả ra giá trị là một phân số mới thuộc dạng Rational: new Rational(...). Điều này đảm bảo tính bất biến của đối tượng, chúng ta không thay đổi giá trị gốc truyền vào mà thay vào đó chúng ta sẽ tạo ra một đối tượng mới. Nếu chúng ta định nghĩa thêm các toán tử khác như minus, multiply hay divide thì giá gốc vẫn được giữ nguyên và không thay đổi.

Dưới đây là một ví dụ cho class Rational:

scala> val oneHalf = new Rational(1, 2)
oneHalf: Rational = 1/2

scala> val twoThird = new Rational(2, 3)
twoThird: Rational = 2/3

scala> val res = oneHalf.add(twoThird)
res: Rational = 7/6

scala> oneHalf
res2: Rational = 1/2

scala> twoThird
res3: Rational = 2/3

scala> res
res4: Rational = 7/6

Hai phân số oneHalftwoThird không hề thay đổi giá trị kể cả sau khi thực hiện hàm add. Điều này sẽ đảm bảo tính bất biến (Immutable) cho đối tượng Rational

4. Conclusion

Vậy thì trong Scala yếu tố nào nên là Immutable và yếu tố nào là Mutable? Hãy ghi nhớ

"Prefer vals, immutable objects, and methods without side effects. Reach for them first. Use vars, mutable objects, and methods with side effects when you have a specific need and justification for them."

Đây là khẩu quyết dành scala programmer, chỉ sử dụng Mutable khi thực sự cần thiết.