Referential transparency là gì ?
Bài đăng này đã không được cập nhật trong 9 năm
Referential transparency là gì ?
Để hiểu về khái niệm referential transparency tôi xin trích dẫn một đoạn từ Wikipedia
Referential transparency and referential opacity are properties of parts of computer programs. An expression is said to be referentially transparent if it can be replaced with its value without changing the behavior of a program (in other words, yielding a program that has the same effects and output on the same input). The opposite term is referential opacity.
Referential transparency (tham chiếu minh bạch) là một trong những nguyên tắc của functional programming, thuật ngữ này thường được nhắc đến và sử dụng nhiều trong các functional languages phổ biến như Haskell hay Scala.
Theo tôi thì gần như không có định nghĩa cụ thể về referential transparency, nhưng một biểu thức được coi là referential transparency khi và chỉ khi nó luôn trả về kết quả giống nhau trong mọi hoàn cảnh dù có thay thế giá trị ban đầu bằng bất kỳ giá trị nào, hay nói cách khác với mỗi input ta sẽ luôn nhận được một output tương ứng.
Ví dụ trong toán học, hàm f(x) = x + 1
thì với mọi giá trị x ta đều có một kết quả là y tương ứng mà không có bất kỳ tác động nào bên trong hay bên ngoài có thể thay đổi điều này => f(x) = x + 1
là một referentially transparency.
Chúng ta sẽ tìm hiểu rõ hơn qua ví dụ dưới đây
Ví dụ
Trong quá trình tìm hiểu về referential transparency, có một điều khá thú vị mà tôi biết là thuật ngữ referential transparency được xuất phát từ triết học phân tích, tư tưởng của nó rất đơn giản và rõ ràng. Thuật ngữ reference
trong triết học phân tích sử dụng để nói về những điều mà một biểu hiện nào đó mà nó muốn đề cập đến.
Giống như việc nói đến Thủ đô của Việt Nam
chính là nói đến Hà Nội
nhưng là 1 kiểu reference
Vậy Tôi đến thủ đô của Việt Nam
và Tôi đến Hà Nội
là như nhau.
Như chúng ta thấy mặc dù thay thế thủ đô của Việt Nam bằng Hà Nội không hề làm thay đổi ý nghĩa của câu nói. Nội dung câu Tôi đến Hà Nội
chính là referentially transparent
Quay lại với lập trình, xét một ví dụ với referential transparent function:
def addOne(x: Int) : Int = {
return x + 1
}
Với referential transparent function và input ta có thể thay thế nó bằng một giá trị thay vì gọi hàm. Ví dụ, thay vì gọi addOne
với tham số là 1, ta chỉ cần thay bằng giá trị là 2 mà chương trình vẫn chạy "ngon nghẻ" và không hề xảy ra lỗi.
Tuy nhiên 1 referential transparent còn phải thỏa mãn rằng nó không chịu ảnh hưởng hoặc ảnh hưởng tới thành phần nào khác của chương trình mà chỉ phụ thuộc vào input của chính nó.
Xét một ví dụ khác, tôi sẽ xây dựng 1 ví dụ thanh toán tiền để thấy tầm quan trọng và ứng dụng của referential transparent.
case class Wallet(var amount: Int) {
def paid(var anotherAmount:Int) : Wallet = {
this.amount -= anotherAmount
return this
}
}
Viết 1 object thực hiện việc thanh toán tiền
Ví dụ 1:
object Payment extends App {
val wallet1 = new Wallet(200)
val wallet2 = wallet1.paid(30)
//Paid
val sum1 = wallet2.paid(10)
println(s"Remaining 1 $sum1")
val sum2 = wallet2.paid(10)
println(s"Remaining 2 $sum2")
}
Output:
Remaining 1 Wallet(160)
Remaining 2 Wallet(150)
Bây giờ tôi sẽ thực hiện referential transparent bằng việc thay thế giá trị wallet2
của sum1
, sum2
bằng wallet1.paid(30)
.
Ta sẽ có
Ví dụ 2:
object Payment extends App {
val wallet1 = new Wallet(200)
val wallet2 = wallet1.paid(30)
val sum1 = wallet1.paid(30).paid(10) // thay wallet2 = wallet1.paid(30)
println(s"Remaining 1 $sum1")
val sum2 = wallet1.paid(30).paid(10) // thay wallet2 = wallet1.paid(30)
println(s"Remaining 2 $sum2")
}
Nhìn vào 2 dòng code mới thay thế có vẻ như tương đương với Ví dụ 1, tuy nhiên thực chất không phải vậy
Output nhận được sẽ là:
Remaining 1 Wallet(130)
Remaining 2 Wallet(90)
Thông thường chúng ta sẽ nghĩ rằng giá trị trả về sẽ không thay đổi khi thay thế 2 biến có giá trị bằng nhau, rõ ràng điều này hoàn toàn đúng nhưng trong trường hợp trên lại không đúng. Nhìn sơ qua thì ở Ví dụ 1: val wallet2 = wallet1.paid(30)
nhưng khi thay vào đoạn
val sum1 = wallet2.paid(10)
val sum2 = wallet2.paid(10)
thành
val sum1 = wallet1.paid(30).paid(10)
val sum2 = wallet1.paid(30).paid(10)
Kết quả lại bị thay đổi, như ta thấy tiền trong ví tự nhiên bị "bốc hơi" khá nhiều (yaoming)
Dĩ nhiên điều không mong muốn đã xảy ra, đã có tác động từ bên ngoài chính là đoạn khai báo val wallet2 = wallet1.paid(30)
nhìn có vẻ không liên quan nhưng lại "vô tình" làm thay đổi giá trị của wallet1
trước khi chúng ta thực hiện 2 phép tính sum ở dưới.
Việc này là trái ngược với nguyên tắc thiết kế referential transparent, vì thay đổi giá trị tuy là giống nhau nhưng làm đoạn mã và có thể cả chương trình chạy sai, và suy ra wallet2
không phải referentially transparent
.
Đây cũng là một ví dụ thể hiện ứng dụng của referential transparent, tránh được những trường hợp "tưởng đúng mà sai" như ở trên.
Do đó, object chúng ta sử dụng phải là dạng Immutable (bất biến)
Viết lại Wallet
case class WalletVIP(val amount: Int) {
def paid(anotherAmount: Int): WalletVIP = WalletVIP(this.amount - anotherAmount)
}
Bây giờ cùng thử lại với ví dụ 1
object Payment extends App {
val wallet1 = new WalletVIP(200)
val wallet2 = wallet1.paid(30)
//Paid
val sum1 = wallet2.paid(10)
println(s"Remaining 1 $sum1")
val sum2 = wallet2.paid(10)
println(s"Remaining 2 $sum2")
}
Output:
Remaining 1 Wallet(160)
Remaining 2 Wallet(160)
Test referential transparent
object Payment extends App {
val wallet1 = new WalletVIP(200)
val wallet2 = wallet1.paid(30)
val sum1 = wallet1.paid(30).paid(10)
println(s"Remaining 1 $sum1")
val sum2 = wallet1.paid(30).paid(10)
println(s"Remaining 2 $sum2")
}
Output:
Remaining 1 Wallet(160)
Remaining 2 Wallet(160)
Kết quả không bị thay đổi (honho)
Kết
Như vậy là đã có cái nhìn tổng quát về referential transparent qua một số ví dụ và việc sử dụng cũng khá dễ dàng đặc biệt là kết hợp với Immutable object sẽ giúp ứng dụng của bạn được cải thiện đáng kể.
Nguồn tham khảo:
- Wikipedia
- Programming in Scala, Second Edition
All rights reserved