Tư duy về lập trình hàm: Suy nghĩ theo lập trình hàm, Phần 1

Học cách suy nghĩ như một người lập trình hàm

Lập trình hàm gần đây đã tạo ra mối quan tâm lớn khi tự nhận là lỗi ít hơn và năng suất cao hơn. Nhưng nhiều nhà phát triển đã thử và đã không hiểu điều gì làm cho các ngôn ngữ lập trình hàm hấp dẫn đối với một số kiểu công việc. Việc học cú pháp của một ngôn ngữ lập trình mới là dễ dàng, nhưng việc học để suy nghĩ theo một cách khác là khó. Trong bài đăng đầu tiên của loạt bài trong mục Tư duy về lập trình hàm, Neal Ford giới thiệu một số khái niệm lập trình hàm và thảo luận cách sử dụng chúng cả trong Java™ lẫn trong Groovy.

Neal Ford, Kiến trúc phần mềm, ThoughtWorks

Neal Ford là một kiến trúc sư phần mềm và Meme Wrangler tại Thought Works, một văn phòng tư vấn CNTT toàn cầu. Ông cũng thiết kế và phát triển các ứng dụng, tài liệu hướng dẫn, các bài báo trên tạp chí, học liệu và các bài thuyết trình video/DVD; và ông là tác giả hoặc người biên tập các cuốn sách bao trùm nhiều loại công nghệ, bao gồm cả cuốn sách gần đây nhất là The Productive Programmer. Ông tập trung vào việc thiết kế và xây dựng ứng dụng doanh nghiệp có quy mô lớn. Ông cũng là một diễn giả được quốc tế hoan nghênh tại hội nghị của các nhà phát triển trên toàn thế giới



22 06 2012

Giới thiệu về loạt bài này

Loạt bài này nhằm mục đích định hướng lại cách nhìn của bạn đối với tư duy về lập trình hàm, giúp bạn xem xét các vấn đề chung theo các cách mới và tìm mọi cách để cải tiến việc viết mã hàng ngày của bạn. Loạt bài này khám phá các khái niệm, các khung công tác lập trình hàm, cho phép lập trình hàm trong phạm vi ngôn ngữ Java, các ngôn ngữ lập trình hàm chạy trên JVM và một số khuynh hướng phát triển trong tương lai của thiết kế ngôn ngữ lập trình. Loạt bài này được nhằm đến các nhà phát triển Java, những người biết về Java và cách hoạt động trừu tượng của nó nhưng có rất ít hoặc chưa có chút kinh nghiệm nào về sử dụng một ngôn ngữ lập trình hàm.

Chúng ta hãy tạm thời nói rằng bạn là một người đốn gỗ. Bạn có rìu tốt nhất trong rừng, nó giúp cho bạn trở thành một người đốn gỗ năng suất nhất trong trại. Sau đó một ngày, một ai đó xuất hiện và tán dương những ưu điểm của một mẫu hình chặt cây mới, đó là cái cưa xích. Anh chàng bán hàng đầy thuyết phục, vì vậy bạn mua một cái cưa xích, nhưng bạn không biết nó hoạt động như thế nào. Bạn cố gắng nhấc nó lên và cọ vào cái cây với một lực lớn, đó là cách mà các mẫu hình chặt cây khác của bạn làm việc. Bạn sẽ nhanh chóng kết luận rằng cái cưa xích mới lạ này chỉ là một mốt nhất thời và bạn quay trở lại chặt cây bằng rìu của mình. Sau đó, một ai đó đến và chỉ cho bạn thấy cách chặt cây bằng cưa xích.

Chắc là bạn cũng có thể liên quan đến câu chuyện này, nhưng là với lập trình hàm thay cho cái cưa xích. Vấn đề với một mẫu hình lập trình mới hoàn toàn không phải là việc học một ngôn ngữ mới. Rốt cuộc thì cú pháp ngôn ngữ chỉ là các chi tiết. Phần nan giải là học suy nghĩ theo một cách khác. Và tôi vào cuộc — tay chạy cưa xích và người lập trình hàm.

Chào mừng bạn đến với Tư duy về lập trình hàm. Loạt bài này khám phá chủ đề về lập trình hàm, nhưng không phải là duy nhất về các ngôn ngữ lập trình hàm. Như tôi sẽ minh họa, việc viết mã theo một cách "lập trình hàm" chạm đến thiết kế, các thỏa hiệp, các khối xây dựng có thể sử dụng lại khác nhau và một loạt những hiểu biết khác. Càng nhiều càng tốt, tôi sẽ cố gắng để trình bày các khái niệm lập trình hàm trong Java (hoặc các ngôn ngữ gần-với-Java) và chuyển sang ngôn ngữ khác để chứng minh các khả năng còn chưa có trong ngôn ngữ Java. Tôi sẽ không bỏ qua nền tảng sâu và nói về những điều rất hiện đại như monads (xem Tài nguyên) ngay bây giờ (mặc dù chúng ta cũng sẽ đi tới đó). Thay vào đó, dần dần tôi sẽ cho bạn thấy một cách suy nghĩ mới về các vấn đề (mà bạn đã áp dụng ở một số nơi — bạn chỉ chưa nhận ra nó thôi).

Bài đăng này và hai bài tiếp theo coi như một chuyến du lịch ngắn về một số chủ đề liên quan đến lập trình hàm, bao gồm các khái niệm cốt lõi. Một số các khái niệm đó sẽ được quay trở lại chi tiết hơn khi tôi đã xây dựng thêm nhiều bối cảnh và sắc thái hơn trong suốt loạt bài này. Là một điểm khởi hành cho chuyến du lịch, tôi sẽ để bạn xem xét hai cách triển khai thực hiện khác nhau của một vấn đề, một cách được viết có tính chất mệnh lệnh và một cách khác với xu hướng lập trình hàm hơn.

Trình phân loại số

Để nói về các phong cách lập trình khác nhau, bạn phải có mã để so sánh. Ví dụ đầu tiên của tôi là một biến thể của bài toán viết mã xuất hiện trong cuốn sách của tôi Nhà lập trình năng suất (xem Tài nguyên) và trong "Thiết kế dựa vào kiểm tra, Phần 1" and "Thiết kế dựa vào kiểm tra, Phần 2" (hai bài đăng trong loạt bài trước của tôi trên developerWorks Kiến trúc tiến hóa và thiết kế mới nổi). Tôi chọn mã này ít nhất cũng một phần vì hai bài viết đó mô tả thiết kế mã một cách sâu sắc. Không có gì sai với thiết kế được tán dương trong hai bài viết đó, nhưng tôi sắp cung cấp một lý lẽ cơ bản cho một thiết kế khác ở đây.

Các yêu cầu nói rõ rằng, cho bất kỳ số nguyên dương lớn hơn 1 nào, bạn phải phân loại nó, là perfect (hoàn hảo), abundant (dư thừa) hay deficient (thiếu). Một số hoàn hảo là một số mà các thừa số của nó (không bao gồm chính số đó) cộng dồn lại bằng chính nó. Tương tự như vậy, tổng các thừa số của số dư thừa sẽ lớn hơn số đó và tổng các thừa số của số thiếu hụt sẽ nhỏ chính số đó.

Trình phân loại số kiểu mệnh lệnh

Một lớp kiểu mệnh lệnh đáp ứng các yêu cầu này xuất hiện trong Liệt kê 1:

Liệt kê 1. NumberClassifier, giải pháp kiểu mệnh lệnh cho bài toán này
public class Classifier6 { 
   private Set<Integer> _factors;
   private int _number; 
   public Classifier6(int number) { 
      if (number < 1) throw new 
           InvalidNumberException("Can't classify negative numbers"); 
      _number = number;
      _factors = new HashSet<Integer>>(); 
      _factors.add(1); 
      _factors.add(_number);
   } 
   private boolean isFactor(int factor) { 
      return _number % factor == 0; 
   } 
   public Set<Integer> getFactors() { 
      return _factors; 
   } 
   private void calculateFactors() { 
      for (int i = 1; i <= sqrt(_number) + 1; i++) 
      if (isFactor(i)) addFactor(i); 
   }
   private void addFactor(int factor) { 
      _factors.add(factor); 
      _factors.add(_number / factor); 
   } 
   private int sumOfFactors() { 
      calculateFactors(); 
      int sum = 0; 
      for (int i : _factors) sum += i; 
      return sum; 
   } 
   public boolean isPerfect() { 
      return sumOfFactors() - _number == _number; 
   } 
   public boolean isAbundant() { 
      return sumOfFactors() - _number > _number; 
   } 
   public boolean isDeficient() { 
      return sumOfFactors() - _number < _number; 
   }
   public static boolean isPerfect(int number) { 
      return new Classifier6(number).isPerfect(); 
   } 
}

Một số thứ đáng lưu ý về đoạn mã này:

  • Nó các bài kiểm tra đơn vị có phạm vi rộng (một phần là do tôi đã viết nó để thảo luận về phát triển dựa vào kiểm tra).
  • Lớp này có chứa một số lớn các phương thức gắn kết, là một tác dụng phụ của việc sử dụng phát triển dựa vào kiểm tra trong khi xây dựng nó.
  • Một sự tối ưu hóa hiệu năng được nhúng vào trong phương thức calculateFactors(). Phần chủ yếu của lớp này gồm có việc thu thập các thừa số để sau đó tôi có thể cộng lại và cuối cùng phân loại chúng. Các thừa số luôn có thể được thu gom theo từng cặp. Ví dụ, nếu số đang xét là 16, khi tôi lấy thừa số 2 thì tôi cũng có thể lấy 8 vì 2 x 8 = 16. Nếu tôi thu gom các thừa số theo từng cặp, tôi chỉ cần kiểm tra các thừa số chưa vượt quá căn bậc hai của số đích mà thôi, đó chính xác là những gì phương thức calculateFactors() làm.

Trình phân loại số kiểu lập trình hàm (hơn một chút)

Vẫn sử dụng chính các kỹ thuật phát triển dựa vào kiểm tra, tôi đã tạo ra một phiên bản thay thế của trình phân loại này, hiển thị trong Liệt kê 2:

Liệt kê 2. Trình phân loại số kiểu lập trình hàm (hơn một chút)
public class NumberClassifier { 
   static public boolean isFactor(int number, int potential_factor) { 
      return number % potential_factor == 0; 
   } 
   static public Set<Integer> factors(int number) {
      HashSet<Integer> factors = new HashSet<Integer>(); 
      for (int i = 1; i <= sqrt(number); i++) 
      if (isFactor(number, i)) { 
         factors.add(i);
         factors.add(number / i); 
      } return factors; 
   } 
   static public int sum(Set<Integer> factors) { 
      Iterator it = factors.iterator(); 
      int sum = 0;
      while (it.hasNext()) sum += (Integer) it.next(); 
      return sum; 
   } 
   static public boolean isPerfect(int number) { 
      return sum(factors(number)) - number == number; 
   } 
   static public boolean isAbundant(int number) { 
      return sum(factors(number)) - number > number; 
   } 
   static public boolean isDeficient(int number) { 
      return sum(factors(number)) - number < number; 
   } 
}

Các khác biệt giữa hai phiên bản này của trình phân loại hơi khó thấy nhưng quan trọng. Sự khác biệt chính là không có trạng thái chia sẻ một cách có chủ đích trong Liệt kê 2. Việc loại bỏ (hoặc ít nhất giảm đi) trạng thái chia sẻ là một trong những trừu tượng hóa ưa thích trong lập trình hàm. Thay vì chia sẻ trạng thái xuyên qua các phương thức dưới dạng các kết quả trung gian (xem trường factors trong Liệt kê 1), tôi gọi các phương thức trực tiếp, loại bỏ trạng thái. Từ quan điểm thiết kế, nó làm cho phương thức factors() dài hơn, nhưng nó ngăn không cho trường factors () "thoát ra" ngoài phương thức. Cũng lưu ý rằng phiên bản của Liệt kê 2 có thể hoàn toàn chứa các phương thức tĩnh. Không tồn tại kiến thức chia sẻ nào giữa các phương thức, vì vậy tôi có ít nhu cầu đóng gói hơn theo phạm vi. Tất cả các phương thức làm việc hoàn toàn tốt nếu bạn cung cấp cho chúng các kiểu tham số đầu vào mà chúng mong đợi. (Đây là một ví dụ về một hàm thuần túy, một khái niệm tôi sẽ tiếp tục nghiên cứu trong một bài đăng sắp tới).


Các hàm

Lập trình hàm là một lĩnh vực rộng, trải dài trong khoa học máy tính, đã chứng kiến một sự quan tâm đột biến gần đây. Các ngôn ngữ lập trình hàm mới trên JVM (ví dụ như Scala và Clojure) và các khung công tác (như Akka và Functional Java) hiện có (xem Tài nguyên), cùng với các tuyên bố tự nhận là lỗi ít hơn, năng suất hơn, trông đẹp hơn, kiếm tiền nhiều hơn và v.v.. Thay vì cố gắng giải quyết toàn bộ chủ đề của lập trình hàm, tôi sẽ tập trung vào một số khái niệm then chốt và làm theo một số gợi ý thú vị bắt nguồn từ các khái niệm đó.

Cốt lõi của lập trình hàm là hàm, cũng như các lớp là trừu tượng hóa chính trong các ngôn ngữ hướng đối tượng. Các hàm tạo thành các khối xây dựng để xử lý và được nhúng với một số tính năng chưa thấy trong các ngôn ngữ mệnh lệnh truyền thống.

Các hàm bậc cao hơn

Các hàm bậc cao hơn có thể hoặc nhận các hàm khác làm các đối số hoặc trả chúng về như các kết quả. Chúng ta không có cấu kiện này trong ngôn ngữ Java. Cái gần nhất mà bạn có thể tiến đến là sử dụng một lớp (thường là một lớp vô danh) như là một "thùng chứa" (holder) của một phương thức mà bạn cần thực hiện. Java không có các hàm (hoặc các phương thức) đứng độc lập, vì vậy chúng không thể được trả về từ các hàm hoặc được chuyển vào như các tham số.

Khả năng này là quan trọng trong các ngôn ngữ lập trình hàm ít nhất với hai lý do. Đầu tiên, các hàm bậc cao hơn có nghĩa là bạn có thể giả định về các phần ngôn ngữ phù hợp với nhau như thế nào. Ví dụ, bạn có thể loại bỏ toàn bộ các loại phương thức trên một hệ thống phân cấp các lớp bằng cách xây dựng một cơ chế chung duyệt qua các danh sách và áp dụng một (hoặc nhiều) hàm bậc cao hơn cho mỗi phần tử. (Tôi sẽ chỉ cho bạn một ví dụ về cấu kiện này ngay sau đây). Thứ hai, bằng cách cho phép các giá trị trả về là các hàm, bạn tạo ra cơ hội để xây dựng hệ thống rất năng động, có tính thích nghi cao.

Các vấn đề có thể dẫn đến giải pháp bằng cách sử dụng các hàm bậc cao hơn không phải là duy nhất cho các ngôn ngữ lập trình hàm. Tuy nhiên, cách bạn giải quyết các vấn đề sẽ khác nhau khi bạn đang suy nghĩ theo lập trình hàm. Hãy xem xét ví dụ trong Liệt kê 3 (lấy từ một cơ sở mã lớn hơn) của một phương thức truy cập dữ liệu có bảo vệ:

Liệt kê 3. Khuôn mẫu mã có khả năng sử dụng lại tiềm năng
public void addOrderFrom(ShoppingCart cart,String userName, Order order) 
                     throws Exception { 
   setupDataInfrastructure(); 
   try {
      add(order, userKeyBasedOn(userName)); 
      addLineItemsFrom(cart, order.getOrderKey());
      completeTransaction(); 
   } 
   catch (Exception condition) { 
       rollbackTransaction(); 
       throw condition; 
   } 
   finally { cleanUp(); } 
}

Mã trong Liệt kê 3 thực hiện khởi tạo, thực hiện một số công việc, hoàn tất giao dịch nếu tất cả mọi thứ đã thành công, hủy giao dịch nếu trái lại, và cuối cùng là xóa tài nguyên. Rõ ràng, có thể sử dụng lại phần soạn sẵn của mã này và chúng tôi thường làm như vậy trong các ngôn ngữ hướng đối tượng bằng cách tạo cấu trúc. Trong trường hợp này, tôi sẽ kết hợp hai trong Các mẫu thiết kế của Bộ tứ (xem Tài nguyên): Các mẫu Lệnh và Phương thức khuôn mẫu. Mẫu của Phương thức khuôn mẫu (Template Method) gợi ý rằng tôi nên di chuyển mã soạn sẵn phổ biến lên hệ thống phân cấp thừa kế, trì hoãn các chi tiết thuật toán xuống cho các lớp con. Mẫu thiết kế Lệnh (Command) đưa ra một cách để đóng gói hành vi trong một lớp với các ngữ nghĩa thi hành đã biết rõ. Liệt kê 4 cho thấy kết quả của việc áp dụng hai mẫu này cho mã trong Liệt kê 3:

Liệt kê 4. Mã lệnh được cấu trúc lại
public void wrapInTransaction(Command c) throws Exception {
   setupDataInfrastructure(); 
   try { 
      c.execute(); 
      completeTransaction(); 
   } 
   catch (Exception condition) { 
      rollbackTransaction(); 
      throw condition; 
   } 
   finally {
      cleanUp(); 
   } 
} 

public void addOrderFrom(final ShoppingCart cart, final String
                userName, final Order order) throws Exception { 
   wrapInTransaction(new Command() {
      public void execute() { 
         add(order, userKeyBasedOn(userName)); 
         addLineItemsFrom(cart, order.getOrderKey()); 
      } 
   }); 
}

Trong Liệt kê 4, tôi trích ra các phần chung nhất của mã này thành phương thức wrapInTransaction() (bạn có thể nhận ra các ngữ nghĩa của nó — nó cơ bản là một phiên bản đơn giản của TransactionTemplate của Spring), chấp nhận một đối tượng Command làm đơn vị công việc. Phương thức addOrderFrom() thu lại thành định nghĩa một lớp bên trong vô danh, tạo ra lớp Command, gói gọn hai mục công việc.

Việc gói gọn hành vi mà tôi cần trong một lớp lệnh chỉ là một tạo phẩm của thiết kế của Java, mà không bao gồm bất kỳ loại hành vi đứng độc lập nào. Tất cả hành vi trong Java phải lưu trú bên trong một lớp. Thậm chí các nhà thiết kế ngôn ngữ đã nhanh chóng nhận thấy khiếm khuyết trong thiết kế này — với nhận thức muộn màng, thật khá ngây thơ khi nghĩ rằng không bao giờ có thể có hành vi mà không gắn liền với một lớp. JDK 1.1 đã sửa chữa khiếm khuyết này bằng cách thêm vào các lớp bên trong vô danh, mà ít nhất chúng đã cung cấp lớp vỏ cú pháp để tạo ra nhiều lớp nhỏ chỉ với một vài phương thức hoàn toàn là lập trình hàm, chứ không là cấu trúc. Để đọc một bài luận cực kỳ thú vị và hài hước về khía cạnh này của Java, hãy xem bài "Thi hành trong Vương quốc của các danh từ" của Steve Yegge (xem Tài nguyên).

Java buộc tôi tạo ra một cá thể của một lớp Command, mặc dù mọi thứ tôi thực sự cần là phương thức bên trong lớp đó. Chính lớp đó không mang lại lợi ích gì: nó có không có trường nào, không có hàm tạo nào (ngoài hàm tạo được sinh ra tự động từ Java) và không có trạng thái. Nó chỉ là một trình bao (wrapper) cho hành vi bên trong phương thức. Trong một ngôn ngữ lập trình hàm, việc này thường được xử lý thông qua một hàm bậc cao hơn để thay thế.

Nếu tôi sẵn sàng rời khỏi ngôn ngữ Java một lát, về ngữ nghĩa, tôi có thể đến gần lý tưởng lập trình hàm bằng cách sử dụng các bao đóng (closure). Liệt kê 5 cho thấy một ví dụ được cấu trúc lại tương tự, nhưng bằng cách sử dụng Groovy (xem Tài nguyên) thay cho Java:

Liệt kê 5. Sử dụng các bao đóng của Groovy thay cho lớp lệnh
def wrapInTransaction(command) {
   setupDataInfrastructure() 
   try { command() completeTransaction() } 
   catch (Exception ex) { 
      rollbackTransaction() throw ex 
   } finally { cleanUp() } 
}
 
def addOrderFrom(cart, userName, order) { 
   wrapInTransaction { 
      add order, userKeyBasedOn(userName) addLineItemsFrom cart, order.getOrderKey() 
   } 
}

Trong Groovy, bất cứ thứ gì bên trong các dấu ngoặc ôm {} đều là một khối mã và các khối mã có thể được chuyển giao làm các tham số, bắt chước một hàm bậc cao hơn. Sau hậu trường, Groovy đang thực hiện mẫu thiết kế Lệnh (Command) cho bạn. Mỗi khối bao đóng trong Groovy thực sự là một cá thể kiểu bao đóng của Groovy, trong đó bao gồm một phương thức call() được tự động gọi ra khi bạn đặt một cặp dấu ngoặc rỗng sau biến đang lưu giữ cá thể bao đóng. Groovy đã cho phép một số hành vi giống như lập trình hàm bằng cách xây dựng các cấu trúc dữ liệu thích hợp, với vỏ cú pháp tương ứng, đưa vào trong chính ngôn ngữ đó. Như tôi sẽ cho thấy trong các bài đăng tương lai, Groovy cũng bao gồm các khả năng lập trình hàm khác nằm ngoài Java. Tôi cũng sẽ quay lại một số so sánh thú vị giữa các bao đóng và các hàm bậc cao hơn trong một bài đăng sau.

Các hàm hạng nhất

Các hàm trong một ngôn ngữ lập trình hàm được coi là hạng nhất, có nghĩa là các hàm này có thể xuất hiện bất cứ ở đâu mà một cấu kiện ngôn ngữ lập trình khác bất kỳ (ví dụ như các biến) có thể xuất hiện. Sự có mặt của hàm hạng nhất cho phép sử dụng các hàm theo những cách bất ngờ và buộc suy nghĩ về các giải pháp một cách khác đi, ví dụ như áp dụng các phép toán chung chung (với các chi tiết sắc thái) cho các cấu trúc dữ liệu tiêu chuẩn. Điều này, đến lượt nó, lại bộc lộ rõ một sự chuyển dịch căn bản trong tư duy theo các ngôn ngữ lập trình hàm: Hãy tập trung vào các kết quả, chứ không vào các bước.

Trong các ngôn ngữ lập trình mệnh lệnh, tôi phải suy nghĩ về từng bước nguyên tử trong thuật toán của mình. Mã trong Liệt kê 1 cho thấy điều này. Để giải quyết bài toán trình phân loại số, tôi đã phải vạch ra chính xác cách thu thập các thừa số, điều này lại có nghĩa là tôi đã phải viết mã cụ thể để lặp qua các số để xác định các thừa số. Tuy nhiên khi lặp qua các danh sách, việc thực hiện các phép toán trên mỗi phần tử, nghe giống như một việc phổ biến thực sự. Hãy xem xét mã lệnh phân loại số được viết lại, bằng cách sử dụng Khung công tác Java lập trình hàm (Functional Java framework), xuất hiện trong Liệt kê 6:

Liệt kê 6. Trình phân loại số kiểu lập trình hàm
public class FNumberClassifier { 
   public boolean isFactor(int number, int potential_factor) { 
      return number % potential_factor == 0;
   } 
   public List<Integer> factors(final int number) { 
      return range(1, number+1).filter(new F<Integer, Boolean>() { 
         public Boolean f(final Integer i) { 
            return isFactor(number, i); 
         } 
      }); 
   } 
   public int sum(List<Integer> factors) {
      return factors.foldLeft(fj.function.Integers.add, 0); 
   } 
   public boolean isPerfect(int number) { 
      return sum(factors(number)) - number == number; 
   } 
   public boolean isAbundant(int number) { 
      return sum(factors(number)) - number > number; 
   } 
   public boolean isDeficiend(int number) { 
      return sum(factors(number)) - number < number;
   } 
}

Các khác biệt chính giữa Liệt kê 6Liệt kê 2 nằm trong hai phương thức: sum()factors(). Phương thức sum() lợi dụng một phương thức trên lớp List (Danh sách) trong Functional Java là phương thức foldLeft(). Phương thức này là một biến thể cụ thể dựa trên một ý niệm xử lý-danh sách được gọi là một catamorphism, là một sự tổng quát hóa về gập danh sách. Trong trường hợp này, "fold left" (gập trái) có nghĩa là:

  1. Lấy một giá trị đầu và kết hợp nó qua một phép toán với phần tử đầu tiên của danh sách.
  2. Lấy kết quả và áp dụng phép toán tương tự như thế cho phần tử tiếp theo.
  3. Hãy tiếp tục làm việc này cho đến khi danh sách được khai thác hết.

Lưu ý rằng đây chính là điều bạn làm khi bạn tính tổng các số trong một danh sách: bắt đầu bằng số không, cộng vào phần tử đầu tiên, lấy kết quả đó và cộng nó vào phần tử thứ hai và tiếp tục cho đến khi danh sách được dùng đến hết. Functional Java cung cấp hàm bậc cao hơn (trong ví dụ này là kiểu liệt kê Integers.add) và chịu trách nhiệm áp dụng nó cho bạn. (Tất nhiên, Java không thực sự có các hàm bậc cao hơn, nhưng bạn có thể viết một hàm tương tự thích hợp nếu bạn hạn chế nó cho một cấu trúc và kiểu dữ liệu đặc biệt).

Một phương thức hấp dẫn khác trong Liệt kê 6factors(), nó minh họa lời khuyên của tôi "hãy tập trung vào các kết quả, chứ không vào các bước". Bản chất của vấn đề phát hiện các thừa số của một số là gì? Nói cách khác, cho một danh sách tất cả các số có thể, tăng dần lên đến một số đích, làm thế nào để tôi xác định những số nào là các thừa số của số đó? Điều này gợi ý một phép lọc — Tôi có thể lọc toàn bộ danh sách các số, loại bỏ những số nào không đáp ứng tiêu chuẩn của tôi. Về cơ bản phương thức này hiểu giống như mô tả sau: lấy dải số từ 1 đến số của tôi (dải này không bao gồm đầu mút, do đó +1); lọc danh sách dựa trên mã trong phương thức f(), là cách của Functional Java cho phép bạn tạo ra một lớp có các kiểu dữ liệu cụ thể; và trả về các giá trị.

Mã này cũng minh họa một ý niệm lớn hơn nhiều, như là một xu hướng trong ngôn ngữ lập trình nói chung. Trước đây, các nhà phát triển đã phải xử lý tất cả các thứ phiền nhiễu như cấp phát bộ nhớ, thu gom rác và các con trỏ. Theo thời gian, các ngôn ngữ đã đảm nhận trách nhiệm đó nhiều hơn. Khi các máy tính đã có nhiều sức mạnh hơn, chúng ta đã chuyển giao càng ngày càng nhiều nhiệm vụ nhàm chán (có thể tự động hóa được) cho các ngôn ngữ và các thời gian chạy. Là một nhà phát triển Java, tôi đã khá quen với việc nhường lại tất cả các vấn đề bộ nhớ cho ngôn ngữ lập trình. Lập trình hàm đang mở rộng nhiệm vụ đó, bao gồm cả các chi tiết cụ thể hơn. Rồi thời gian trôi qua, chúng ta sẽ dành ít thời gian lo lắng về các bước cần thiết để giải quyết một vấn đề và suy nghĩ nhiều hơn về các quá trình. Theo tiến độ của loạt bài này, tôi sẽ chỉ ra rất nhiều ví dụ về điều này.


Kết luận

Lập trình hàm là một quan niệm hơn là một bộ các công cụ hoặc các ngôn ngữ. Trong bài đăng đầu tiên này, tôi đã bắt đầu trình bày một số chủ đề về lập trình hàm, trải dài từ các quyết định thiết kế đơn giản đến một vài tư duy lại vấn đề đầy tham vọng. Tôi viết lại một lớp Java đơn giản để làm cho nó mang phong cách lập trình hàm nhiều hơn, sau đó bắt đầu đi sâu vào một số chủ đề đã đưa lập trình hàm vượt ra ngoài việc sử dụng các ngôn ngữ mệnh lệnh truyền thống.

Hai khái niệm dài hạn, quan trọng đầu tiên đã hiện ra ở đây. Trước tiên, hãy tập trung vào các kết quả, chứ không vào các bước. Lập trình hàm sẽ cố gắng trình bày các bài toán khác đi vì bạn có các khối xây dựng khác nhau, thúc đẩy các giải pháp. Xu hướng thứ hai tôi cần chỉ ra trong suốt loạt bài này là chuyển giao các chi tiết nhàm chán cho các ngôn ngữ lập trình và các thời gian chạy, cho phép chúng ta tập trung vào các khía cạnh duy nhất của các bài toán lập trình của chúng ta. Trong bài đăng tiếp theo, tôi tiếp tục xem xét các khía cạnh chung về lập trình hàm và nó áp dụng cho việc phát triển phần mềm hiện nay như thế nào.

Tài nguyên

Học tập

Thảo luận

Bình luận

developerWorks: Đăng nhập

Các trường được đánh dấu hoa thị là bắt buộc (*).


Bạn cần một ID của IBM?
Bạn quên định danh?


Bạn quên mật khẩu?
Đổi mật khẩu

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Ở lần bạn đăng nhập đầu tiên vào trang developerWorks, một hồ sơ cá nhân của bạn được tạo ra. Thông tin trong bản hồ sơ này (tên bạn, nước/vùng lãnh thổ, và tên cơ quan) sẽ được trưng ra cho mọi người và sẽ đi cùng các nội dung mà bạn đăng, trừ khi bạn chọn việc ẩn tên cơ quan của bạn. Bạn có thể cập nhật tài khoản trên trang IBM bất cứ khi nào.

Thông tin gửi đi được đảm bảo an toàn.

Chọn tên hiển thị của bạn



Lần đầu tiên bạn đăng nhập vào trang developerWorks, một bản trích ngang được tạo ra cho bạn, bạn cần phải chọn một tên để hiển thị. Tên hiển thị của bạn sẽ đi kèm theo các nội dung mà bạn đăng tải trên developerWorks.

Tên hiển thị cần có từ 3 đến 30 ký tự. Tên xuất hiện của bạn phải là duy nhất trên trang Cộng đồng developerWorks và vì lí do an ninh nó không phải là địa chỉ email của bạn.

Các trường được đánh dấu hoa thị là bắt buộc (*).

(Tên hiển thị cần có từ 3 đến 30 ký tự)

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Thông tin gửi đi được đảm bảo an toàn.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=70
Zone=Công nghệ Java
ArticleID=820290
ArticleTitle=Tư duy về lập trình hàm: Suy nghĩ theo lập trình hàm, Phần 1
publish-date=06222012