Scala: Functional Programing và Pattern Matching

Theo wiki: lập trình hàm là một mô hình lập trình xem việc tính toán là sự đánh giá các hàm toán học và tránh sử dụng trạng thái và các dữ liệu biến đổi. Lập trình hàm nhấn mạnh việc ứng dụng hàm số, trái với phong cách lập trình mệnh lệnh, nhấn mạnh vào sự thay đổi trạng thái. Lập trình hàm xuất phát từ phép tính lambda, một hệ thống hình thức được phát triển vào những năm 1930 để nghiên cứu định nghĩa hàm số, ứng dụng của hàm số, và đệ quy. Nhiều ngôn ngữ lập trình hàm có thể được xem là những cách phát triển giải tích lambda.

Hiện nay có khá nhiều ngôn ngữ hỗ trợ lập trình hàm như Python, Haskell, Perl, Erlang, Elixir ... Bài viết này sẽ giới thiệu đến các bạn việc sử dụng Scala để lập trình hàm. Trong lập trình hàm, việc xử lý tính toán chủ yếu được thực hiện bởi các biểu thức. Lập trình hàm Scala khuyến khích sử dụng mô hình lập trình hướng biểu thức (EExExpression Oriented Programing). Đầu tiên chúng ta sẽ cùng tìm hiểu về EOP.

Expression Oriented Programing

Với EOP, tất cả câu lệnh đều là biểu thức. Biểu thức khác câu lệnh ở chỗ nó luôn trả lại gía trị, còn câu lệnh thì có thể không.

Pure function (Hàm thuần khiết)

Trong toán học, hàm là thuần khiết, do đó nó không có side effect. Ví dụ y = sin(x): bất kể được gọi đến bao nhiêu lần, nó không bao giờ thay đổi tính chất

Referential Transparency (Tham chiếu thuần khiết)

Biểu thức là tham chiếu thuần khiết nếu nó có thể được thay thế bởi gía trị kết qủa của chính nó mà không ảnh hưởng đến hành vi của chương trình tại bất kể nơi nào mà biểu thức được sử dụng. ví dụ: chúng ta gán tổng của 2 biết bất biến x, y vào biến z

val z = x + y

Bây giờ ta có thể thay đổi mọi nơi sử dụng biểu thức x + y bằng biến z mà sẽ luôn đảm bảo tính đúng đắn của chương trình không bị thay đổi. Giống như Hàm thuần khiết, tham chiếu thuần khiết cũng không bị ảnh hưởng bởi side effect. Ngoài Referential Transparency, có 2 khái niệm nữa chúng ta bắt buộc phải nắm được để có thiểu về lập trình hàm, đó là gía trị bất biến (immutable value) và high-order function.

Function Literal

Literal là ví dụ đơn giản nhất thể hiện cho lập trình hàm. Literal là một gía trị cố định trong chương trình, nó được dùng như cách ta khởi tạo biến. Ví dụ

int x = 1

Scala cho phép xây dựng hàm Literal (Function Literal), ta có thể gán một hàm vào một biến, hoặc sử dụng một hàm như là tham số của hàm khác. Ví dụ

val add = (x: Int, y: Int) => x + y

First Class Function và Higher Order Function

First Class Function

  • Có thể coi như một loại data type.
  • Có thể được tạo ra từ một literal.
  • Được lưu trữ như 1 value, 1 biến hay 1 data structure.
  • Có thể được sử dụng như 1 tham số cho 1 hàm khác.

Higher Order Function

1 hàm dùng hàm khác như tham số thì sẽ được gọi là higher order function.

scala> def fap(functionParam: (Int, Int) => Int){
     | println(functionParam(5,5))
     | }
fap: (functionParam: (Int, Int) => Int)Unit
scala> val add = (x: Int, y:Int) => { x + y }
add: (Int, Int) => Int = <function2>
scala> fap(add)
10

Function as variable

Có thể gán function vào 1 biến. Ví dụ:

scala> val add = (i: Int) => { i + 10 }
add: Int => Int = <function1>
scala> add(22)
res5: Int = 32

Closure

Closure là một hàm có gía trị trả về phụ thuộc vào gía trị của các biến định nghĩa bên ngoài hàm.

scala> val x = 3
x: Int = 3
scala> val add = (y: Int) => x + y
add: Int => Int = <function1>
scala> add(5)
res7: Int = 8

Partially Applied Functions

Để tránh việc phải gọi đi gọi lại 1 hàm mà tham số truyền vào là giống nhau, Scala hỗ trợ Partially Applied Functions. Ví dụ:

val add = (x: Int, y: Int) => x + y
//Giả sử có nhiều chỗ ta cần truyền cố định biến y = 10, thay vì liên tục gọi hàm và truyền y vào thì ta tận dụng Partially Applied Functions
val partiallyAdd = add(_:Int, 10)
scala> partiallyAdd(5)
res8: Int = 15

Curried Function

Phân tách các tham số thành các phần nối tiếp nhau như các tham số đơn lẻ. Ví dụ:

scala> def curriedAdd(a: Int)(b: Int) = a + b
curriedAdd: (a: Int)(b: Int)Int
scala> curriedAdd(2)(2)
res1: Int = 4

Pattern matching

scala> def printNum(int: Int){
     | int match {
     | case 0 => println("Zero")
     | case 1 => println("One")
     | case _ => println("more than one")
     | }
     | }
printNum: (int: Int)Unit
scala> print
print   printNum   printf   println
scala> printNum(0)
Zero
scala> printNum(1)
One
scala> printNum(2)
more than one

Hãy nhìn đoạn code trên, chúng ta sử dụng từ khóa match, nhìn tương tự như switch case bên java đúng không nào. Nhưng để hiểu được sức mạnh của match, chúng ta phải hiểu được các pattern của nó, dưới đây là một vài pattern phố biến

Wildcard patterns (mẫu kí tự đại diện)

Trong scala wildcard pattern được sử dụng là (_) nó đại diện cho tất cả các object. Nó được sử dụng như default trong java. Như ví dụ trên, khi truyền vào tham số khác 0 hoặc 1 thì sẽ gọi đến case _

case _ => println("more than one")

Constant patterns

Sử dụng Constant khi ta muốn chỉ rõ một gía trị nào đó, ví dụ:

scala> def assign(x: Any) = x match {
     | case 10 => "ten"
     | case false => "wrong"
     | case Nil => "It is empty"
     | case _ => "another"
     | }
assign: (x: Any)String
scala> assign(10)
res3: String = ten
scala> assign(Nil)
res4: String = It is empty
scala> assign("xyz")
res6: String = another

Variable patterns

Một Variable patterns sẽ nhận 1 giá trị bất kỳ giống như wildcard. Nhưng khác biệt ở đây là ta có thể sử dụng giá trị đó.

scala> def variable(x: Any) = x match {
     | case 0 => "zero"
     | case somethingElse => "not zero: " + somethingElse
     | }
assign: (x: Any)String
scala> variable(2)
res0: String = not zero: 2
scala> variable(0)
res0: String = zero

Vậy nếu ta thêm 1 wildcard thì điều gì sẽ xảy ra

case _ => "something different"

Ta sẽ nhận được cảnh báo warning: unreachable code .... Để không nhận cảnh báo, hay đưa biến vào trong cặp dấu ``

Sequence patterns

Kiểm tra tuần tự đầu vào có thỏa mãn điều kiện. Ví dụ:

scala> def check (x: Any)= x match {
     | case List(0, _) => println("match")
     | case _ => println("invalid")
     | }
check: (x: Any)Unit
scala> check(List(0,1))
match
scala> check(List(1,0))
invalid
scala>

Tuple patterns

Kiểm tra có match 1 tập hay không. Ví dụ:

scala> def tuple (x: Any)= x match {
     | case (Ha, Noi) => println("matched")
     | case _ => println("invalid")
     | }
tuple: (x: Any)Unit
scala> tuple("Ha", "Noi")
matched
scala> tuple("Da", "Nang")
invalid
scala>

Typed patterns

Kiểm tra có match kiểu được chỉ định hay không. Ví dụ

scala> def checkType(x: Any) = x match {
     | case s: String => println("string length: " + s.length)
     | case i: Int => println("value is: " + i)
     | case _ => println("another type")
     | }
checkType: (x: Any)Unit
scala> checkType("111")
string length: 3
scala> checkType(111)
value is: 111
scala> checkType(111.111)
another type
scala>

Tham khảo từ ebook Beginning Scala, 2nd Edition và bài viết

http://docs.scala-lang.org/tutorials/tour/pattern-matching.html