Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Thành thạo Lập trình Hàm (Functional Programming) trong Ruby: Hướng dẫn hoàn chỉnh

Có thể bạn vừa nghe về lập trình hàm (functional programming) & có vài câu hỏi.

Như…

  • Lập trình hàm chính xác là gì?
  • Nó so sánh với lập trình hướng đối tượng (OOP) như thế nào?
  • Bạn có nên dùng lập trình hàm trong Ruby không?

Để tôi trả lời các câu hỏi này cho bạn để bạn có cái nhìn tốt hơn về cách nó hoạt động.

Lập trình Hàm (Functional Programming) là gì?

Nó không chỉ là một trào lưu hay từ ngữ sang trọng, nó là một mô hình lập trình (programming paradigm) thực tế đã tồn tại lâu nhưng gần đây lại được phổ biến lại.

Và các ý tưởng cơ bản đằng sau mô hình này dễ hiểu hơn bạn nghĩ.

Trong lập trình hàm chúng ta tránh thay đổi trạng thái (state) & cố gắng viết các hàm “thuần khiết” (pure functions).

Tránh thay đổi trạng thái nghĩa là các hàm này không thay đổi bất cứ thứ gì bên ngoài hàm, không biến instance, không thay đổi đối tượng được truyền vào…

Không có điều đó!

Trong ngôn ngữ lập trình hàm (như Haskell) tất cả dữ liệu là IMMUTABLE (bất biến).

Có thứ gọi là biến, nhưng chúng hoạt động giống như trong thế giới toán học. Một khi biến được gán giá trị, trình biên dịch sẽ không cho phép bạn định nghĩa lại biến này với giá trị khác.

Thành thạo Lập trình Hàm (Functional Programming) trong Ruby: Hướng dẫn hoàn chỉnh

Lợi ích của Lập trình Hàm

Immutability (bất biến) là lợi thế chính của lập trình hàm vì dữ liệu có thể thay đổi (mutable data) có thể dẫn đến lỗi tinh vi khó theo dõi.

Ví dụ:

def all_different_from_first?(arr)
  first = arr.shift
  arr.all? { |n| n != first }
end

arr = [1,3,5,7,9]
p all_different_from_first?(arr) # true

Trong ví dụ này tôi muốn tìm xem tất cả phần tử trong mảng có khác với phần tử đầu tiên không.

Để làm việc này chúng ta cần loại bỏ phần tử đầu tiên khỏi mảng & đồng thời lưu phần tử này để so sánh với phần còn lại.

Làm thế nào chúng ta làm được?

Chúng ta đang làm việc với mảng & nếu bạn nhìn danh sách phương thức có sẵn bạn sẽ thấy Array#shift làm chính xác điều chúng ta muốn.

Nó hoạt động tốt cho đến…

…bạn nhìn giá trị của arr sau khi gọi phương thức một lần:

all_different_from_first?(arr) # true
arr # [3,5,7,9]

Ngạc nhiên!

Mảng mất một phần tử (1) & chúng ta không nhận ra.

Đó là cách lỗi thay đổi trạng thái (mutability bugs) lén lút có thể là.

Phiên bản sửa:

def all_different_from_first?(arr)
  arr[1..-1].all? { |n| n != arr.first }
end

Functional vs OOP

Chúng ta có nên áp dụng lập trình hàm hoàn toàn không?

Có vẻ như trạng thái bất biến này làm cho lập trình hàm trở thành đối lập hoàn toàn của OOP, và ở một khía cạnh nó là, nhưng vẫn có cách hai mô hình lập trình này có thể làm việc cùng nhau.

Vì vậy không, không cần vội vã chuyển sang lập trình hàm hoàn toàn. Ruby được thiết kế cho OOP, vì vậy bạn sẽ đang chống lại cường thức.

Tin tốt:

Bạn vẫn có thể dùng những ý tưởng tốt nhất từ lập trình hàm & áp dụng cho mã Ruby của bạn.

Hãy nói về cách làm điều đó.

Giảm Mutability (Tính thay đổi) càng nhiều càng tốt

Một cách để làm điều đó là NGỪNG dùng attr_accessor, chỉ dùng attr_reader.

Sau khi bạn làm vậy thì bạn cần chú ý đến string, mảng & hash.

Có các phương thức sẽ thay đổi các đối tượng này:

  • Phương thức kết thúc bằng ! (như gsub!)
  • delete
  • update
  • clear
  • shift / unshift / pop / push

Bước đầu là nhận biết các phương thức này.

Nếu bạn phải dùng một trong các phương thức này bạn có thể làm việc trên đối tượng trùng lặp (duplicate).

Cho một string & bản sao của string đó:

str = "abcd"
dup = str.dup

Chúng ta nhận được kết quả này khi clear bản sao:

dup.clear # str => "abcd" # dup => ""

Điều này giữ string gốc an toàn.

Partial Application (Ứng dụng một phần)

Có nhiều hơn dữ liệu bất biến & hàm thuần khiết trong lập trình hàm.

Như ứng dụng một phần của hàm, còn gọi là “currying”.

Ví dụ:

def add(a,b)
  a + b
end

add_five = method(:add).curry[5]
add_five.call(5)  # 10
add_five.call(20) # 25

Chú ý cách phương thức add nhận hai đối số, nhưng bằng cách dùng phương thức curry chúng ta có thể “nạp trước” (preload) một trong các đối số.

Sau đó chúng ta nhận được một lambda có thể gọi chỉ với đối số thứ 2.

Ví dụ khác:

list = (1..10)
greater_than = ->(x,y) { y > x }.curry
list.select(&greater_than.(5)) # [6, 7, 8, 9, 10]
list.select(&greater_than.(8)) # [9, 10]

Một ví dụ nữa:

divisible_by = ->(x,y) { y % x == 0 }.curry
list.select(&divisible_by.(5)) # [5, 10]
list.select(&divisible_by.(2)) # [2, 4, 6, 8, 10]

Tóm tắt

Bạn đã học về lập trình hàm, cốt lõi của nó là hàm thuần khiết & dữ liệu bất biến, nó chỉ là một cách tư duy về mã của bạn & không hoàn toàn không tương thích với OOP.

Cảm ơn bạn đã đọc, đừng quên đăng ký bản tin nếu bạn chưa đăng ký! 🙂