Go – Functional programming

Trong bài viết này, chúng ta cùng nhau tìm hiểu về Functional Programming trong Go. Chúng ta không đi sâu về khái niệm FP (Functional Programming), thay vào đó sẽ tập trung vào những thứ mà Go có thể làm.

Trong FP có 2 nguyên tắc vô cùng quan trọng:

  • Dữ liệu không thay đổi: Nghĩa là dữ liệu của object không nên thay đổi sau khi tạo
  • Không có state ngầm: Nên tránh state ẩn hoặc ngầm. Trong FP state không bị loại bỏ, thay vào đó, nó hiển thị và rõ ràng

Điều này nghĩa là:

  • No side effect: Một hàm hoặc phương thức không nên có bất kỳ state nào nằm ngoài phạm vi của hàm. Một  hàm chỉ nên trả về một giá trị cho bên gọi và không nên bị ảnh hưởng bởi state bên ngoài. Điều này giúp cho chương trình dễ hiểu hơn.
  • Pure function: Code của hàm không nên thay đổi giá trị. Các hàm chỉ nên trả về các giá trị dựa trên các tham số truyền vào và không nên bị ảnh hưởng (side effect) hoặc dựa trên state chung. Những hàm như thế sẽ luôn trả về cùng một kết quả với cùng một tham số truyền vào.

Ngoài ra còn những khái niệm bên dưới đây mà chúng ta có thể áp dụng trong Go,

Sử dụng FP không có nghĩa là toàn bộ hoặc không đáng kể, bạn có thể thường xuyên sử dụng khái niệm FP để bổ sung cho hướng đối tượng hoặc khái niệm bắt buộc trong Go. Lợi ích của FP có thể được tận dụng bất cứ khi nào có thể bất kể bạn đang sử dụng mô hình hay ngôn ngữ nào.

Sửa và tạo – string

Sai

Đúng

Sửa và tạo – mảng

Sai

Đúng

Sửa và tạo – map

Sai

Đúng

First-class và higher order function

Các hàm First-class nghĩa là bạn có thể gán các hàm cho các biến, truyền vào một hàm như một tham số cho một hàm khác hoặc trả về một hàm từ một hàm khác. Go hỗ trợ điều này và do đó đưa ra các khái niệm như là closures, currying, và higher-order-function một cách dễ dàng khi code.

Một hàm chỉ được coi như một higher-order-function nếu nó nhận một hoặc nhiều hàm như một tham số và nếu nó trả về một hàm khác như một kết quả.

Trong Go bạn có thể thực hiện như sau

Closures và currying cũng có thể sử dụng trong Go

Ngoài ra cũng có nhiều higher-order-function tích hợp trong thư viện chuẩn của Go. Cũng có nhiều thư viện hỗ trợ theo kiểu functional như là koazee hoặc itertools hỗ trợ map-reduce như là các phương thức functional trong Go.

Pure function

Như chúng ta đã hiểu về Pure Function chỉ nên trả về các giá trị dựa trên các tham số truyền vào và không nên bị ảnh hưởng hoặc dựa vào state chung. Chúng ta có thể thực hiện dễ dàng trong Go.

Điều này tương đối đơn giản, bên dưới đây là một Pure Function. Nó luôn luôn trả về cùng một kết quả với cùng một đầu vào và hoạt động của nó tương đối dễ hiểu. Chúng ta có thể cache phương thức nếu cần

Giả sử chúng ta chỉnh sửa code bên trên bằng cách thêm vào như bên dưới đây, hoạt động của hàm trở nên khó dự đoán vì giờ đây nó bị side effect, bi ảnh hưởng bởi state bên ngoài.

Nên hãy cố giữ cho hàm của bạn thuần tuý (Pure Function) và đơn giản.

Recursion – Đệ quy

FP khuyến khích sử dụng đệ quy thay vì dùng vòng lặp. Hãy cùng xem ví dụ sau đây dùng để tính toán giai thừa.

Tiếp cận truyền thống sử dụng vòng lặp

Chúng ta có thể hoàn thành đoạn code bên trên sử dụng đệ quy như bên dưới theo khuyến khích của FP

Nhược điểm của tiếp cận sử dụng đệ quy là nó sẽ chậm hơn khi so sánh với vòng lặp trong hầu hết các lần (nhưng ưu điểm mà chúng ta nhắm đến là code trông đơn giản và dễ đọc) và có thể dẫn đến lỗi tràn bộ nhớ do tất cả các lần gọi hàm đều cần được lưu như một frame cho stack. Để tránh điều này nên dùng đệ quy đuôi (tail recursion) hơn và đặc biệt với trường hợp phải sử dụng đệ quy nhiều lần. Đệ quy đuôi giúp tránh được tạo Stack mới khi lần gọi cuối cùng trong đệ quy là hàm chính nó. Hầu hết trình biên dịch có thể tối ưu hoá code đệ quy đuôi giống như cách vòng lặp được tối ưu hoá do đó tránh được vấn đề hiệu suất. Đáng tiếc trình biên dịch của Go không tối ưu cho điều này.

Bây giờ chúng ta sẽ thử sử dụng đệ quy đuôi cho hàm như bên dưới đây

Hãy cân nhắc khi sử dụng đệ quy khi viết code Go sao cho dễ đọc và tính bất biến, nhưng hãy cẩn trọng hiệu suất hoặc nếu số vòng lặp lớn thì nên chuyển sang dùng vòng lặp.

Lazy evaluation – tính toán Lazy

Hãy cùng tìm hiểu một chút về tính toán Eager vs Lazy

  • Tính toán Eager: Biểu thức được tính toán tại thời điểm biến được gán hoặc hàm được gọi…
  • Tính toán Lazy: Trì hoãn việc tính toán cho tới khi nó cần thiết.
  • Tính hiệu quả bộ nhớ: Không có bộ nhớ nào được sử dụng để lưu trữ hoàn toàn cấu trúc
  • Tính hiệu quả CPU: Không cần tính toán hoàn toàn kết quả trước khi trả về

Tính toán Lazy là quá trình trì hoãn việc tính toán của một biểu thức cho tới khi nó thật sự cần. Nói chung, về cơ bản thì Go tính toán Eager nhưng đối với các toán tử như là && và || nó thực hiện tính toán Lazy. Chúng ta có thể sử dụng higher-order-function, closure, goroutine, và channel bắt chước tính toán Lazy.

Hãy cùng xem ví dụ sau đây

Kết quả được hiển thị như bên dưới và cả hai hàm đang thực thi bình thường

Chúng ta có thể sử dụng higher-order-function để viết lại theo phiên bản tính toán lazy

Kết quả

Có một cách khác để thực hiện điều này là sử dụng Sync & Futures như link này và sử dụng goroutine và channel như link này. Thực hiện tính toán lazy trong Go có thể là không hữu ích với code phức tạp nhưng nếu hàm nặng về xử lý thì nó chắc chắn là hữu ích khi sử dụng tính toán lazy.

Kiểu hệ thống

Go có kiểu hệ thống mạnh và cũng có kiểu interface hợp lệ. Nhưng điều duy nhất còn thiếu khi so sánh với ngôn ngữ lập trình FP những thứ như là Pattern Matching và Case Class.

Minh bạch tham chiếu

Đáng tiếc là không có nhiều cách để giới hạn việc thay đổi dữ liệu trong Go, tuy nhiên bằng cách sử dụng Pure function và bằng cách rõ ràng cũng giúp tránh được thay đổi dữ liệu và được gán lại sử dụng khái niệm khác mà chúng ta sẽ sớm được nhìn thấy. Go mặc định truyền các biến bằng giá trị ngoại trừ slice và map. Nên cần hạn chế truyền vào như tham chiếu (con trỏ) nhiều nhất có thể.

Ví dụ, bên dưới đây state bên ngoài sẽ thay đổi khi chúng ta truyền tham số bằng tham chiếu và do đó không đảm bảo tính minh bạch tham chiếu.

Nếu chúng ta truyền vào tham số bằng giá trị chúng ta có thể đảm bảo tính minh bạch thậm trí nếu có thay đổi dữ liệu của tham số bên trong hàm

Chúng ta không thể sử dụng cách này khi các tham số được truyền là slice hoặc map.

Cấu trúc dữ liệu

Khi sử dụng kỹ thuật FP nó thường khuyến khích sử dụng kiểu dữ liệu functional như là Stack, Map và Queue. Do đó map là tốt hơn array hoặc hash set trong FP dưới dạnh lưu trữ dữ liệu.

You May Also Like

About the Author: Nguyen Dinh Thuc

Leave a Reply

avatar
  Subscribe  
Notify of