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

Khám phá lập trình hàm và điều khiển

Các khung công tác và các ngôn ngữ lập trình hàm cho phép thời gian chạy điều khiển các chi tiết viết mã nhàm chán như vòng lặp, đồng thời và trạng thái. Nhưng điều đó không có nghĩa là bạn không thể nắm lại quyền điều khiển khi bạn cần. Một khía cạnh quan trọng của Tư duy lập trình hàm là phải biết bạn muốn từ bỏ bao nhiêu quyền điều khiển và khi nào.

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 lập trình hàm, giúp bạn xem xét các vấn đề phổ biến theo các cách mới và tìm mọi cách để cải thiện việc tạo 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 khi sử dụng một ngôn ngữ lập trình hàm.

Trong bài đăng đầu tiên của loạt bài này, tôi đã bắt đầu thảo luận về một số đặc điểm của lập trình hàm, cho thấy những ý tưởng đó thể hiện như thế nào trong cả ngôn ngữ Java lẫn trong các ngôn ngữ lập trình hàm nhiều hơn. Trong bài này, tôi sẽ tiếp tục chuyến du lịch qua các khái niệm này bằng cách nói về các hàm hạng nhất, tối ưu hóa và các bao đóng. Nhưng chủ đề cơ bản của bài đăng này là điều khiển: khi nào bạn muốn nó, khi nào bạn cần nó và khi nào bạn nên từ bỏ nó.

Các hàm hạng nhất và điều khiển

Khi sử dụng thư viện Functional Java (xem Tài nguyên), cuối cùng tôi đã cho thấy việc triển khai thực hiện một trình trình phân loại số với các phương thức hàm isFactor()factorsOf(), như trong Liệt kê 1:

Liệt kê 1. Phiên bản lập trình hàm của trình phân loại số
import fj.F; 
import fj.data.List; 
import static fj.data.List.range; 
import static fj.function.Integers.add; 
import static java.lang.Math.round; 
import static java.lang.Math.sqrt; 
public class FNumberClassifier { 
   public boolean isFactor(int number, int potential_factor) {
      return number % potential_factor == 0; 
   } 
   public List<Integer> factorsOf(final int number) { 
      return range(1, number+1).filter(new F<Integer, Boolean>() {
         public Boolean f(final Integer i) { 
            return number % i == 0; 
         } 
      }); 
   } 
   public int sum(List<Integer> factors) { 
      return factors.foldLeft(fj.function.Integers.add, 0); 
   } 
   public boolean isPerfect(int number) { 
      return sum(factorsOf(number)) - number == number; 
   } 
   public boolean isAbundant(int number) { 
      return sum(factorsOf(number)) - number > number; 
   } 
   public boolean isDeficiend(int number) { 
      return sum(factorsOf(number)) - number < number; 
   } 
}

Trong các phương thức isFactor()factorsOf(), tôi nhường lại quyền điều khiển thuật toán vòng lặp cho khung công tác — bây giờ nó quyết định cách tốt nhất để lặp qua toàn bộ dải các số. Nếu khung công tác (hoặc ngôn ngữ — nếu bạn chọn một ngôn ngữ lập trình hàm như Clojure hoặc Scala) có thể tối ưu hóa việc triển khai bên dưới, bạn sẽ tự động được hưởng lợi. Mặc dù lúc đầu bạn có thể miễn cưỡng từ bỏ nhiều quyền điều khiển như vậy, lưu ý rằng nó đi theo một xu hướng chung trong các ngôn ngữ lập trình và các thời gian chạy: Theo thời gian, nhà phát triển trở nên được trừu tượng hóa cao hơn tránh xa các chi tiết mà nền tảng có thể xử lý chúng hiệu quả hơn. Tôi không bao giờ lo lắng về việc quản lý bộ nhớ trên JVM vì nền tảng này cho phép tôi quên nó. Chắc chắn, đôi khi nó thực hiện một cái gì đó khó khăn hơn, nhưng đó là một sự đánh đổi tốt vì các lợi ích mà bạn nhận được trong viết mã hàng ngày. Các cấu kiện của ngôn ngữ lập trình hàm, ví dụ như các hàm bậc cao hơn và các hàm hạng nhất cho phép tôi leo thêm một bậc thang mức trừu tượng cao hơn và tập trung nhiều hơn vào mã làm gì hơn là cách nó thực hiện như thế nào.

Ngay cả với khung công tác Functional Java, viết mã theo phong cách này bằng Java là nặng nề vì ngôn ngữ này không thực sự có cú pháp và các cấu kiện dành cho nó. Vậy thì viết mã kiểu lập trình hàm sẽ trông giống như thế nào trong một ngôn ngữ mà nó được viết ra?

Trình phân loại trong Clojure

Clojure là một Ngôn ngữ lập trình (Lisp) hàm được thiết kế cho JVM (xem Tài nguyên). Hãy xem xét trình phân loại số được viết trong Clojure, được hiển thị trong Liệt kê 2:

Liệt kê 2. Thực hiện trình phân loại số bằng Clojure
(ns nealford.perfectnumbers) 
(use '[clojure.contrib.import-static :only (import-static)]) 
(import-static java.lang.Math sqrt) 
(defn is-factor? [factor number] (= 0 (rem number factor)))
(defn factors [number] (set (for [n (range 1 (inc number)) 
                                    :when (is-factor? n number)] n))) 
(defn sum-factors [number] (reduce + (factors number))) 
(defn perfect? [number] (= number (- (sum-factors number) number))) 
(defn abundant? [number] (< number (- (sum-factors number) number))) 
(defn deficient? [number] (> number (- (sum-factors number) number)))

Hầu hết mã trong Liệt kê 2 khá dễ theo dõi, ngay cả khi bạn không phải là một nhà phát triển Lisp bảo thủ — đặc biệt khi bạn có thể học đọc từ trong ra ngoài. Ví dụ, phương thức is-factor? có hai tham số và hỏi xem hiệu số có bằng 0 hay không khi nhân factor (thừa số) với nhau rồi so với number (số) đích. Tương tự như vậy, các phương thức perfect? (hoàn hảo?), abundant? (dư thừa?) và deficient? (thiếu hụt?) cũng dễ giải mã, đặc biệt khi bạn dựa vào việc thực hiện bằng Java trong Liệt kê 1.

Phương thức sum-factors sử dụng phương thức reduce dựng sẵn. Phương thức sum-factors giảm dần mỗi lần một phần tử của danh sách, bằng cách sử dụng hàm (trong trường hợp này là +) được cung cấp như là tham số đầu tiên trên mỗi phần tử. Phương thức reduce xuất hiện dưới những vỏ bọc khác nhau trong một vài ngôn ngữ và các khung công tác, bạn đã thấy nó trong phiên bản Functional Java ở Liệt kê 1 dưới dạng phương thức foldLeft(). Phương thức factors trả về một danh sách các số, vì thế tôi đang xử lý một danh sách tại một thời điểm, thêm mỗi phần tử vào tổng tích lũy, là giá trị trả về của phương thức reduce. Bạn có thể thấy rằng một khi bạn trở nên quen với tư duy về các hàm bậc cao hơn và các hàm hạng nhất, bạn có thể làm giảm bớt (chơi chữ có chủ ý) khá nhiều dữ liệu thừa trong mã của bạn.

Phương thức factors có vẻ giống như việc thu gom ngẫu nhiên các ký hiệu. Nhưng nó có ý nghĩa một khi bạn thấy tính năng hiểu danh sách (list comprehensions), một trong vài tính năng thao tác danh sách mạnh mẽ trong Clojure. Như trước đây, dễ nhất là hiểu factors từ trong ra ngoài. Đừng bị nhầm lẫn bởi sự xung đột thuật ngữ giữa các ngôn ngữ. Từ khóa for trong Clojure không có nghĩa một vòng lặp for. Thay vào đó, hãy nghĩ về nó như là ông của tất cả các cấu kiện lọc và chuyển đổi. Trong trường hợp này, tôi đang yêu cầu nó lọc dải các số từ 1 đến (number + 1), sử dụng vị từ is-factor? (là phương thức is-factor mà tôi đã định nghĩa trước đó trong Liệt kê 2 — lưu ý việc sử dụng rất nhiều các hàm hạng nhất), trả về các số phù hợp. Kết quả trả về từ phép toán này là một danh sách các số đáp ứng tiêu chuẩn lọc của tôi, mà tôi ép nó thành một tập hợp để loại bỏ các trùng lặp.

Mặc dù việc học một ngôn ngữ mới là một điều khó khăn, nhưng bạn nhận được nhiều so với đồng tiền bỏ ra khi học các ngôn ngữ lập trình hàm, một khi bạn hiểu các tính năng của chúng.

Tối ưu hóa

Một trong những lợi ích của việc chuyển đổi sang phong cách lập trình hàm là khả năng sử dụng sự hỗ trợ của hàm bậc cao hơn do ngôn ngữ hoặc khung công tác cung cấp. Nhưng còn về thời gian thì sao khi bạn không muốn từ bỏ quyền điều khiển đó? Trong ví dụ trước đây của tôi, tôi đã so sánh hành vi bên trong của các cơ chế vòng lặp với các hoạt động bên trong của trình quản lý bộ nhớ: hầu hết thời gian bạn hài lòng không phải lo lắng gì về các chi tiết đó. Nhưng đôi khi bạn lại quan tâm về chúng, như trong trường hợp tối ưu hóa và những chỉnh sửa tương tự.

Trong hai phiên bản Java của trình phân loại số mà tôi đã cho thấy trong bài "Tư duy về lập trình hàm, Phần 1," tôi đã tối ưu hóa mã xác định các thừa số. Việc triển khai thực hiện đơn giản ban đầu đã sử dụng toán tử modulus (%), rất không hiệu quả, để kiểm tra tất cả các số từ 2 lên đến chính số đích, để xác định xem nó có là một thừa số không. Bạn có thể tối ưu hóa thuật toán bằng cách nhận ra rằng các thừa số đi theo từng cặp. Ví dụ, nếu bạn đang tìm kiếm các thừa số của số 28, khi bạn tìm thấy 2 bạn cũng có thể lấy cả 14. Nếu bạn có thể thu nhận các thừa số theo từng cặp, bạn chỉ cần kiểm tra các thừa số cho tới khi gặp căn bậc hai của số đích.

Việc tối ưu hóa rất dễ thực hiện trong phiên bản Java dường như là không thể có trong phiên bản Functional Java bởi vì tôi không điều khiển trực tiếp việc thực hiện cơ chế vòng lặp. Tuy nhiên, một phần của việc học tư duy theo lập trình hàm yêu cầu từ bỏ các khái niệm về loại điều khiển đó, cho phép bạn dùng loại điều khiển khác.

Tôi có thể phát biểu lại bài toán ban đầu theo lập trình hàm: lọc tất cả các thừa số từ 1 đến number, chỉ giữ lại các thừa số phù hợp với vị từ isFactor() của tôi. Vị từ này được thực hiện trong Liệt kê 3:

Liệt kê 3. Phương thức isFactor()
public List<Integer> factorsOf(final int number) { 
   return range(1, number+1).filter(new F<Integer, Boolean>() { 
     public Boolean f(final Integer i) { 
        return number % i == 0; 
     } 
   }); 
}

Mặc dù ngắn gọn theo quan điểm khai báo, mã trong Liệt kê 3 là rất không hiệu quả vì nó kiểm tra tất cả các số. Khi tôi hiểu sự tối ưu hóa (thu nhận các thừa số theo từng cặp, chỉ cần đến bằng căn bậc hai), tôi có thể trình bài lại bài toán này như sau:

  1. Lọc tất cả các thừa số của số đích từ 1 đến căn bậc hai của số đó.
  2. Chia số đích cho từng thừa số này để có được thừa số đối xứng và thêm nó vào danh sách các thừa số.

Với mục tiêu này trong suy nghĩ, tôi có thể viết phiên bản tối ưu hóa của phương thức factorsOf() bằng cách sử dụng Functional Java, như trong Liệt kê 4:

Liệt kê 4. Phương thức tìm các thừa số tối ưu hóa
public List<Integer> factorsOfOptimzied(final int number) { 
    List<Integer> factors = range(1, (int) round(sqrt(number)+1))
                .filter(new F<Integer, Boolean>() { 
       public Boolean f(final Integer i) { 
          return number % i == 0; 
       }
    }); 
    return factors.append(factors.map(new F<Integer, Integer>() { 
       public Integer f(final Integer i) { 
          return number / i; 
       }
    })) 
    .nub();
}

Mã trong Liệt kê 4 dựa vào thuật toán mà tôi đã nói trước đây, với một số cú pháp rất mới do khung công tác Functional Java yêu cầu. Trước tiên, tôi lấy một dải các số từ 1 đến căn bậc hai của số đích cộng 1 (để chắc chắn tôi bắt giữ tất cả các thừa số). Thứ hai, tôi lọc các kết quả dựa vào việc sử dụng các toán tử modulus như trong các phiên bản trước, được bao bọc trong một khối mã của Functional Java. Tôi lưu danh sách đã lọc này trong biến factors. Thứ tư (đọc từ trong ra ngoài), tôi lấy danh sách các thừa số này và thực hiện hàm map(), cung cấp một danh sách mới bằng cách thực hiện khối mã của tôi đối với mỗi phần tử (ánh xạ mỗi phần tử vào một giá trị mới). Danh sách các thừa số của tôi có chứa tất cả các thừa số của số đích, nhỏ hơn căn bậc hai của nó; tôi cần chia số đích cho từng thừa số để thu được thừa số đối xứng của nó, đó là điều mà khối mã được gửi đến phương thức map() thực hiện. Thứ năm, bây giờ tôi có danh sách các cặp thừa số đối xứng, tôi thêm nó vào danh sách ban đầu. Cuối cùng, tôi phải tính đến thực tế là tôi đang giữ các thừa số trong một List thay vì một Set. Các phương thức List thuận tiện cho các kiểu xử lý này, nhưng tác dụng phụ của thuật toán của tôi là sẽ có mục trùng lặp khi gặp số chính phương. Ví dụ, nếu số đích là 16, căn của số đó bằng 4, dẫn đến nó xuất hiện hai lần trên danh sách các thừa số. Để tiếp tục sử dụng các phương thức List thuận tiện, tôi chỉ cần gọi phương thức nub() của nó ở cuối để loại bỏ tất cả các số trùng lặp.

Chỉ vì bạn thường bỏ qua kiến thức thực hiện chi tiết khi sử dụng trừu tượng hóa mức cao hơn giống như lập trình hàm không có nghĩa là bạn có thể bị ngăn cấm khi bạn phải làm. Nền tảng Java chủ yếu bảo vệ bạn khỏi những thứ ở mức thấp, nhưng nếu bạn quyết tâm, bạn có thể đào sâu đến mức bạn cần. Tương tự như vậy, trong các cấu kiện lập trình hàm, bạn thường sẵn sàng nhường lại các chi tiết cho trừu tượng hóa, nhưng vẫn giữ quyền không nhường lại khi nó thực sự quan trọng.

Hình ảnh trực quan chiếm ưu tế nổi bật trong tất cả mã Functional Java mà tôi đã thể hiện cho đến nay là cú pháp khối, trong đó sử dụng generic (một phương tiện lập trình chung trong Java) và các lớp bên trong vô danh như là một loại cấu kiện khối-mã-giả kiểu bao đóng. Các bao đóng là một trong những tính năng phổ biến của các ngôn ngữ lập trình hàm. Điều gì làm cho chúng có ích đến thế trong thế giới này?


Có gì đặc biệt về bao đóng?

Một bao đóng (closure) là một hàm mang theo một kết buộc ngầm định cho tất cả các biến được tham chiếu bên trong nó. Nói cách khác, hàm (hoặc phương thức) này bao bọc một bối cảnh xung quanh những thứ mà nó tham chiếu. Các bao đóng được sử dụng khá thường xuyên như là một cơ chế thi hành di động trong các ngôn ngữ và khung công tác lập trình hàm, được chuyển giao đến các hàm bậc cao hơn như map() dưới dạng mã chuyển đổi. Functional Java sử dụng các lớp bên trong vô danh để bắt chước một số hành vi của bao đóng "thực sự", nhưng chúng không thể làm từ A đến Z bởi vì Java không hỗ trợ cho các bao đóng. Nhưng điều đó muốn nói gì?

Liệt kê 5 cho thấy một ví dụ về điều gì làm cho bao đóng đặc biệt đến thế. Nó được viết bằng Groovy, hỗ trợ các bao đóng thông qua cơ chế khối-mã của nó.

Liệt kê 5. Mã Groovy minh họa các bao đóng
def makeCounter() { 
   def very_local_variable = 0 return {
       return very_local_variable += 1 
   } 
} 
c1 = makeCounter() 
c1() 
c1() 
c1() 
c2 = makeCounter() 
println "C1 = ${c1()}, C2 = ${c2()}" // output: C1 = 4, C2 = 1

Phương thức makeCounter() trước hết định nghĩa một biến cục bộ có một tên thích hợp, sau đó trả về một khối mã sử dụng biến đó. Lưu ý rằng kiểu trả về của phương thức makeCounter() là một khối mã, không phải là một giá trị. Khối mã đó là không làm bất kỳ thứ gì, ngoài việc làm tăng giá trị của biến cục bộ và trả về nó. Tôi đã viết các cuộc gọi return rõ ràng trong mã này, cả hai đều là tùy chọn trong Groovy, nhưng không có chúng mã này thậm chí còn khó hiểu hơn!

Để sử dụng phương thức makeCounter(), tôi gán khối mã đó cho một biến C1, sau đó gọi nó ba lần. Tôi đang sử dụng cú pháp đặc biệt của Groovy để thực hiện một khối mã, dùng để đặt một tập các cặp dấu ngoặc bên cạnh biến của khối mã. Tiếp theo, tôi gọi lại phương thức makeCounter() gán một cá thể mới của khối mã cho C2. Cuối cùng, tôi thực hiện lại C1 cùng với C2. Lưu ý rằng mỗi khối mã đã giữ vết một cá thể riêng biệt của very_local_variable. Đó là điều mà việc bao bọc bối cảnh muốn nói tới. Mặc dù một biến cục bộ được định nghĩa trong phương thức đó, khối mã được kết buộc vào biến này vì nó tham chiếu biến này, có nghĩa rằng nó phải giữ vết biến này trong khi cá thể khối mã còn hoạt động.

Chỗ gần nhất mà bạn có thể gặp hành vi tương tự bằng Java có trong Liệt kê :

Liệt kê 6. MakeCounter bằng Java
public class Counter { 
   private int varField; 
   public Counter(int var) { 
      varField = var; 
   } 
   public static Counter makeCounter() { 
      return new Counter(0); 
   } 
   public int execute() { 
      return ++varField; 
   }
}

Có thể có một vài biến thể của lớp Counter nhưng bạn vẫn còn bị dính với việc quản lý trạng thái của mình. Điều này minh họa tại sao việc sử dụng các bao đóng là ví dụ điển hình cho tư duy lập trình hàm: cho phép thời gian chạy quản lý trạng thái. Thay vì buộc bạn xử lý việc tạo biến trường và trông nom trạng thái (bao gồm viễn cảnh gây sốc về việc sử dụng mã của bạn trong một môi trường đa luồng), hãy để cho ngôn ngữ hoặc khung công tác quản lý một cách vô hình trạng thái đó cho bạn.

Cuối cùng rồi chúng ta sẽ có các bao đóng trong một bản phát hành Java sắp tới (may quá là một cuộc thảo luận về bản phát hành đó nằm ngoài phạm vi của bài này). Sự xuất hiện của các bao đóng trong Java sẽ có hai lợi ích đáng mừng. Đầu tiên, nó sẽ đơn giản hóa rất nhiều các khả năng của các tác giả viết khung công tác và thư viện trong khi cải thiện cú pháp của chúng. Thứ hai, nó sẽ cung cấp một mẫu số chung mức thấp để hỗ trợ bao đóng trong tất cả các ngôn ngữ chạy trên JVM. Mặc dù nhiều ngôn ngữ JVM hỗ trợ các bao đóng, tất cả chúng đều phải thực hiện các phiên bản riêng của mình, làm cho việc chuyển giao bao đóng giữa các ngôn ngữ thành nặng nề. Nếu ngôn ngữ Java đã định nghĩa một định dạng duy nhất, thì tất cả các ngôn ngữ khác có thể sử dụng nó.


Kết luận

Việc nhường lại quyền điều khiển các chi tiết mức thấp của bạn là một xu hướng chung trong phát triển phần mềm. May thay chúng ta đã thôi chịu trách nhiệm về thu gom rác, quản lý bộ nhớ và các khác biệt phần cứng. Lập trình hàm tiêu biểu cho bước nhảy vọt trừu tượng hóa tiếp theo: nhường lại thêm nhiều chi tiết nhàm chán, ví dụ như vòng lặp, xảy ra đồng thời và trạng thái cho thời gian chạy càng nhiều càng tốt. Điều này không có nghĩa là bạn không thể nắm lại quyền điều khiển khi bạn cần — nhưng bạn phải muốn thế, chứ không phải nó ép buộc bạn.

Trong bài đăng tiếp theo, tôi sẽ tiếp tục nghiên cứu của mình về các cấu kiện lập trình hàm trong Java và các quan hệ thân thuộc của chúng bằng cách giới thiệu phương thức curryingáp dụng phương thức một phần (partial method application).

Tài nguyên

Học tập

Lấy sản phẩm và công nghệ

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=820291
ArticleTitle=Tư duy về lập trình hàm: Suy nghĩ theo lập trình hàm, Phần 2
publish-date=06222012