Định nghĩa lập trình hàm
Phần giới thiệu về The Joy of Clojure cho biết như sau:
Lập trình hàm là một trong những thuật ngữ điện toán có định nghĩa vô định hình. Nếu bạn hỏi 100 lập trình viên về định nghĩa của họ, bạn có thể sẽ nhận được 100 câu trả lời khác nhau ...
Lập trình hàm quan tâm và tạo điều kiện cho ứng dụng và thành phần của các chức năng ... Để một ngôn ngữ được coi là chức năng, khái niệm chức năng của nó phải là hạng nhất. Các hàm hạng nhất có thể được lưu trữ, chuyển và trả lại giống như bất kỳ phần dữ liệu nào khác. Ngoài khái niệm cốt lõi này, [các định nghĩa về FP có thể bao gồm] độ tinh khiết, tính bất biến, đệ quy, sự lười biếng và tính minh bạch tham chiếu.
Lập trình trong Scala 2nd Edition p. 10 có định nghĩa sau:
Lập trình chức năng được hướng dẫn bởi hai ý chính. Ý tưởng đầu tiên là các hàm là các giá trị hạng nhất ... Bạn có thể truyền các hàm dưới dạng đối số cho các hàm khác, trả về chúng dưới dạng kết quả từ các hàm hoặc lưu trữ chúng trong các biến ...
Ý tưởng chính thứ hai của lập trình chức năng là các hoạt động của chương trình nên ánh xạ các giá trị đầu vào thành các giá trị đầu ra thay vì thay đổi dữ liệu tại chỗ.
Nếu chúng tôi chấp nhận định nghĩa đầu tiên, thì điều duy nhất bạn cần làm để làm cho mã của mình "hoạt động" là biến các vòng lặp của bạn từ trong ra ngoài. Định nghĩa thứ hai bao gồm sự bất biến.
Chức năng hạng nhất
Hãy tưởng tượng bạn hiện đang nhận được Danh sách Hành khách từ đối tượng Xe buýt của mình và bạn lặp đi lặp lại việc giảm tài khoản ngân hàng của mỗi hành khách bằng số tiền vé xe buýt. Cách chức năng để thực hiện hành động tương tự này là có một phương thức trên Bus, có thể được gọi là forEachPasbah có chức năng của một đối số. Sau đó, Bus sẽ lặp lại hành khách của mình, tuy nhiên, điều đó được thực hiện tốt nhất và mã khách hàng của bạn tính giá vé cho chuyến đi sẽ được đưa vào một chức năng và được chuyển cho forEachPasbah. Voila! Bạn đang sử dụng lập trình chức năng.
Bắt buộc:
for (Passenger p : Bus.getPassengers()) {
p.debit(fare);
}
Chức năng (sử dụng chức năng ẩn danh hoặc "lambda" trong Scala):
myBus = myBus.forEachPassenger(p:Passenger -> { p.debit(fare) })
Phiên bản Scala có đường hơn:
myBus = myBus.forEachPassenger(_.debit(fare))
Hàm không hạng nhất
Nếu ngôn ngữ của bạn không hỗ trợ các chức năng hạng nhất, điều này có thể trở nên rất xấu. Trong Java 7 trở về trước, Bạn phải cung cấp giao diện "Đối tượng chức năng" như thế này:
// Java 8 has java.util.function.Consumer, but in earlier
// versions you have to roll your own:
public interface Consumer<T> {
public void accept(T t);
}
Sau đó, lớp Bus cung cấp một trình vòng lặp nội bộ:
public void forEachPassenger(Consumer<Passenger> c) {
for (Passenger p : passengers) {
c.accept(p);
}
}
Cuối cùng, bạn chuyển một đối tượng hàm ẩn danh cho Bus:
// Java 8 has syntactic sugar to make this look more like
// the Scala solution, but earlier versions require manually
// instantiating a "Function Object," in this case, a
// Consumer:
Bus.forEachPassenger(new Consumer<Passenger>() {
@Override
public void accept(final Passenger p) {
p.debit(fare);
}
}
Java 8 cho phép các biến cục bộ được nắm bắt phạm vi của một hàm ẩn danh, nhưng trong các phiên bản trước đó, bất kỳ biến thể nào như vậy phải được khai báo cuối cùng. Để giải quyết vấn đề này, bạn có thể cần phải tạo một lớp bao bọc MutableReference. Đây là một lớp dành riêng cho số nguyên cho phép bạn thêm bộ đếm vòng lặp vào đoạn mã trên:
public static class MutableIntWrapper {
private int i;
private MutableIntWrapper(int in) { i = in; }
public static MutableIntWrapper ofZero() {
return new MutableIntWrapper(0);
}
public int value() { return i; }
public void increment() { i++; }
}
final MutableIntWrapper count = MutableIntWrapper.ofZero();
Bus.forEachPassenger(new Consumer<Passenger>() {
@Override
public void accept(final Passenger p) {
p.debit(fare);
count.increment();
}
}
System.out.println(count.value());
Ngay cả với sự xấu xí này, đôi khi cũng có ích để loại bỏ logic phức tạp và lặp đi lặp lại từ các vòng lặp trải khắp chương trình của bạn bằng cách cung cấp một trình vòng lặp nội bộ.
Sự xấu xí này đã được sửa trong Java 8, nhưng việc xử lý các ngoại lệ được kiểm tra bên trong hàm hạng nhất vẫn thực sự xấu và Java vẫn mang giả định về tính biến đổi trong tất cả các bộ sưu tập của nó. Điều này đưa chúng ta đến các mục tiêu khác thường được liên kết với FP:
Bất biến
Mục 13 của Josh Bloch là "Thích bất biến." Mặc dù rác thông thường nói ngược lại, OOP có thể được thực hiện với các đối tượng bất biến, và làm như vậy làm cho nó tốt hơn nhiều. Chẳng hạn, String trong Java là bất biến. StringBuffer, OTOH cần có thể thay đổi để xây dựng Chuỗi bất biến. Một số nhiệm vụ, như làm việc với bộ đệm vốn đã yêu cầu khả năng biến đổi.
Độ tinh khiết
Mỗi hàm ít nhất phải có khả năng ghi nhớ - nếu bạn cung cấp cho nó các tham số đầu vào giống nhau (và nó không có đầu vào nào ngoài các đối số thực tế của nó), nó sẽ tạo ra cùng một đầu ra mỗi lần mà không gây ra "tác dụng phụ" như thay đổi trạng thái toàn cầu, thực hiện I / O, hoặc ném ngoại lệ.
Người ta đã nói rằng trong Lập trình chức năng, "một số điều ác thường được yêu cầu để hoàn thành công việc." Độ tinh khiết 100% thường không phải là mục tiêu. Giảm thiểu tác dụng phụ là.
Phần kết luận
Thực sự, trong tất cả các ý tưởng ở trên, tính bất biến đã là chiến thắng lớn nhất về các ứng dụng thực tế để đơn giản hóa mã của tôi - cho dù là OOP hay FP. Truyền các chức năng cho các trình vòng lặp là chiến thắng lớn thứ hai. Các Java 8 Lambdas tài liệu có lời giải thích tốt nhất của lý do tại sao. Đệ quy là tuyệt vời để xử lý cây. Sự lười biếng cho phép bạn làm việc với các bộ sưu tập vô hạn.
Nếu bạn thích JVM, tôi khuyên bạn nên xem Scala và Clojure. Cả hai đều là những diễn giải sâu sắc về lập trình hàm. Scala là loại an toàn với cú pháp hơi giống C, mặc dù nó thực sự có nhiều cú pháp giống với Haskell như với C. Clojure không an toàn kiểu và nó là Lisp. Gần đây tôi đã đăng một so sánh về Java, Scala và Clojure liên quan đến một vấn đề tái cấu trúc cụ thể. Sự so sánh của Logan Campbell khi sử dụng Trò chơi cuộc sống bao gồm Haskell và gõ Clojure.
PS
Jimmy Hoffa chỉ ra rằng lớp Bus của tôi có thể thay đổi. Thay vì sửa bản gốc, tôi nghĩ rằng điều này sẽ chứng minh chính xác loại tái cấu trúc câu hỏi này. Điều này có thể được khắc phục bằng cách biến mỗi phương thức trên Bus thành một nhà máy để sản xuất một Bus mới, mỗi phương thức trên Hành khách một nhà máy để sản xuất một Hành khách mới. Do đó, tôi đã thêm một kiểu trả về cho tất cả mọi thứ, điều đó có nghĩa là tôi sẽ sao chép java.util.feft.Factor của Java 8 thay vì giao diện Consumer:
public interface Function<T,R> {
public R apply(T t);
// Note: I'm leaving out Java 8's compose() method here for simplicity
}
Sau đó trên xe buýt:
public Bus mapPassengers(Function<Passenger,Passenger> c) {
// I have to use a mutable collection internally because Java
// does not have immutable collections that return modified copies
// of themselves the way the Clojure and Scala collections do.
List<Passenger> newPassengers = new ArrayList(passengers.size());
for (Passenger p : passengers) {
newPassengers.add(c.apply(p));
}
return Bus.of(driver, Collections.unmodifiableList(passengers));
}
Cuối cùng, đối tượng hàm ẩn danh trả về trạng thái đã sửa đổi của một thứ (một xe buýt mới với hành khách mới). Điều này giả định rằng p.debit () hiện trả về một Hành khách bất biến mới với số tiền ít hơn so với ban đầu:
Bus b = b.mapPassengers(new Function<Passenger,Passenger>() {
@Override
public Passenger apply(final Passenger p) {
return p.debit(fare);
}
}
Hy vọng rằng bây giờ bạn có thể tự đưa ra quyết định về cách bạn muốn tạo ra ngôn ngữ bắt buộc của mình và quyết định xem có nên thiết kế lại dự án của bạn bằng ngôn ngữ chức năng hay không. Trong Scala hoặc Clojure, các bộ sưu tập và các API khác được thiết kế để giúp lập trình chức năng dễ dàng. Cả hai đều có Java interop rất tốt, vì vậy bạn có thể trộn và kết hợp các ngôn ngữ. Trong thực tế, đối với khả năng tương tác của Java, Scala biên dịch các hàm lớp đầu tiên của nó thành các lớp ẩn danh gần như tương thích với các giao diện chức năng của Java 8. Bạn có thể đọc về các chi tiết trong Scala trong giáo phái Depth. 1.3.2 .