Thật khó để tìm một định nghĩa nhất quán của Functional Programming. Nhưng trong bài viết này tôi sẽ đưa ra 2 khái niệm để định nghĩa về FP như sau:
- Khi bạn code ứng dụng chỉ sử dụng các Pure Function.
- Khi bạn viết code của mình chỉ sử dụng các giá trị Immutable.
Do đó, FP là cách chúng ta viết một ứng dụng phần mềm chỉ sử dụng các Pure Function và các giá trị Immutable
Pure Function
Một Pure Function có thể được định nghĩa như sau:
- Kết quả đầu ra của Pure Function chỉ dựa trên các tham số đầu vào và thuật toán bên trong hàm. Nó sẽ không giống như các phương thức OPP dựa trên các trường khác nhau trong class
- Một Pure Function sẽ không bị side effect ( kết quả trả về bị ảnh hưởng bởi tác nhân bên ngoài hàm). Nó sẽ không đọc, nhận bất kỳ cái gì nào ở bên ngoài hàm hoặc ghi bất kỳ cái gì ở bên ngoài hàm. Ví dụ nó sẽ không đọc một file, web service, UI, hoặc database và ngoài ra nó cũng không ghi bất cứ cái gì.
Để hiểu rõ hơn hai khái niệm bên trên, ví dụ chúng ta gọi một Pure Function nhiều lần với tham số đầu vào là x, nó sẽ luôn trả về cùng một kết quả là y – nếu chúng ta gọi một hàm kiểm trả độ dài của string, thì với đầu vào là “hello”, thì dù chúng ta có gọi bao nhiêu lần đi nữa thì nó luôn luôn trả về kết quả là 5
Sau đây là ví dụ về một Pure Function.
def sum(firstNumber: Int, secondNumber): Int = { firstNumber + secondNumber }
Immutable
Code FP tốt nhất là giống algebra, trong algebra bạn sẽ không bao giờ sử dụng lại biến. Do đó bằng cách tránh sử dụng lại các biến, chúng ta sẽ thu được lợi ích tương tự
Bạn có thể dựa trên một giá trị để thay thế cho một vài hàm ví dụ bạn có val a = f(x)
. Trong trường hợp này chúng ta luôn sử dụng giá trị a
thay cho hàm f(x)
, và giá trị này là không thay đổi được, nó sẽ không giống với trường hợp var a = f(x)
. Do đó chúng ta đang sử dụng giống algebra
val a = f(x) val b = g(a) val c = h(b)
Ở đây giá trị của a và b không bao giờ thay đổi được, vì thế code sẽ trông dễ hiểu hơn. Với các biến var bạn sẽ phải luôn luôn tâm niệm trong tâm trí rằng nó có bị thay đổi hay gán lại ở đoạn nào không? Luôn phải để ý nó một cách cẩn thận.
Một lý do khác nữa để chỉ sử dụng các giá trị immutable là các biến mutable (sử dụng var) sẽ không hoạt động tốt với các ứng dụng chạy parallel/concurrent. Lý do cho việc này là concurrent đang ngày trở nên quan trọng khi các CPU đang sử dụng nhiều lõi hơn.
Ưu điểm
- Pure Function là dễ hiểu hơn: Khi viết một hàm, chúng ta sẽ không đảm bảo nó hoạt động như những gì chúng ta mong muốn nếu như nó kết nối với bên ngoài, có những đầu vào ẩn, hay chỉnh sửa một state ẩn. Chính vì lý do này chúng ta cần đảm bảo hàm của mình với 2 điều sau: chính xác những gì đang hoạt động trong mỗi hàm và những gì sẽ nhận được.
- Test dễ hơn: Để thực hiện viết test cho một Pure Function sẽ dễ dàng hơn vì bạn sẽ không cần phải lo lắng về những xử lý với các state ẩn và side effect.
- Debug dễ hơn: Bởi vì Pure Function chỉ dựa trên các tham số đầu vào để thu được kết quả đầu ra của nó, do đó việc debug các Pure Function dễ hơn. Bạn sẽ không phải lo lắng những gì sẽ được xử lý trong phần còn lại của ứng dụng, bạn chỉ cần quan tâm đến đầu vào mà nguyên nhân làm cho hàm không hoạt động chính xác.
- Các chương trình hoạt động ổn định hơn: Có nhiều phần luôn luôn thay đổi như là các mutable biến và các state ẩn. Nhưng trong ứng dụng FP, nói về mặt toán học, ứng dụng về tổng thể sẻ ít phức tạp hơn.
- Pure Function là có ý nghĩa hơn: Các phương thức không phải là Pure Function có thể có side effect như là các đầu vào ẩn, kết quả đầu ra của các phương thức khác, cuối cùng ý nghĩa của hàm sẽ không còn chính xác nữa.
def doSomething(): Unit { code here ... }
Hàm bên trên không có tham số đầu vào và trả về Unit tức là không trả về gì cả, nó rất khó để phỏng đoán được hàm này đang làm gì. Ngược lại, Pure Function chỉ dựa trên đầu vào để thu được kết quả đầu ra, do đó hàm của chúng ta là vô cùng ý nghĩa.
- Lập trình Parallel/Concurent là dễ hơn: Một chương trình FP là sẵn sàng concurrent mà không cần bất cứ sửa đổi nào khác. Bạn sẽ không bao giờ phải lo lắng về deadlock và các điều kiện chạy đua bởi vì bạn không phải sử dụng lock. Không có phần dữ liệu nào trong FP bị chỉnh sửa 2 lần trong cùng một thread. Do đó bạn có thể dễ dàng thêm vào các thread mà không bao giờ gặp các vấn đề thông thường mà sẽ gây lỗi cho ứng dụng concurrent lần thứ 2.
Nhược điểm
- Viết một Pure Function là đơn giản nhưng kết nối chúng trong một ứng dụng đầy đủ là khó: Tất cả các hàm phải tuân theo pattern giống nhau: Dữ liệu đầu vào, thuật toán xử lý và dữ liệu đầu ra. Kết nối tất cả các PF thành một khối trong ứng dụng FP là vẫn đề dễ gây lỗi nhất mà bạn sẽ gặp phải
- Các khái niệm về toán học nâng cao làm cho FP trở nên đáng sợ: Khi lần đầu tiên bạn nghe thuật ngữ như là combinator, monoid, monad và functor bạn sẽ cảm giác sợ những thuật ngữ này và sự sợ hãi này là rào cản để bạn học FP
- Đối với nhiều người, đệ quy không phải là cách tự nhiên: Đối với nhiều lập trình viên đến từ mô hình OOP, đệ quy là tương đối quen thuộc, nhưng thường xuyên mức độ quen thuộc đến từ yêu cầu bài toán. Do các giá trị là immutable nên chỉ có cách lặp tất cả các phần tử sử dụng đệ quy. Nhưng nó cũng là một tiện lợi là viết code sử dụng đệ quy sẽ thú vị hơn.
- Bởi vì bạn không sử dụng mutable dữ liệu, nên thay vì sử dụng một pattern chúng ta sử dụng copy cho các trường hợp chỉnh sửa: Nó là tương đối đơn giản đối với dữ liệu mutable nhưng trong FP bạn phải làm như sau: Copy object hiện tại cho object mới, rồi copy dữ liệu hiện tại của object cũ cho object mới. Sau đó cập nhập giá trị cho bất kỳ trường nào mà chúng ta cần thay đổi.
- Pure Function và I/O không được sử dụng trộn lẫn vào nhau
- Chỉ sử dụng các giá immutable và đệ quy có thể làm cho ứng dụng của chúng ta gặp vấn đề về hiệu suất bao gồm sử dụng RAM và tốc độ: Nhưng điều này có thể giảm đáng kể bằng cách sử dụng tail recursion.