Bước vào thế giới Lập Trình Hàm đầy nghiện ngập

Những bước đi đầu tiên để hiểu được khái niệm về Lập Trình Hàm thường là bước khó nhất và quan trọng nhất, và với quan niệm đúng đắn, bạn sẽ tiếp kiệm được vô vàn thời gian.

Học cách lái xe

Khi mới học cách lái xe, ai cũng gặp khó khăn cả. Tất nhiên, nhìn người khác làm thì dễ lắm, nhưng khi tự thân nhảy vào học lại là cả một câu chuyện khác.

Chúng ta tập lái xe của bố mẹ, lòng vòng quanh nhà, và chỉ thực sự chạy ra đường cái khi đã đôi chút thành thục.

Nhưng nhờ vào sự luyện tập bền bỉ đó, cùng những giây phút “hú vía” trên xe, ta cuối cùng cũng thi đậu được để lấy bằng.

Với tấm bằng lái xe trên tay, ta đi băng băng trên mọi nẻo đường. Với mỗi chuyển đi, ta học thêm được nhiều điều lý thú và ngày càng tự tin hơn. Và rồi cũng đến một ngày, chúng ta phải lái một chiếc xa của người khác, hoặc khi chiếc xe của chúng ta “lên bàn thờ” và phải mua chiếc mới.

Cảm giác lần đầu tiên cầm lái một chiếc xe khác như thế nào? Có giống tần đầu tiên cầm lái trong đời hay không? Khác rất khác cơ. Lần đầu tiên, tất cả mọi thứ đều thật lạ lẫm. Trước đó chúng ta đã ngồi trong xe rồi, nhưng chỉ là hành khách. Lần này, chúng ta là người cầm tay lái. Người nắm mọi quyền điều khiển.

Nhưng với chiếc xe thứ hai này, chúng ta chỉ thắc mắc trong đầu những câu hỏi vô cùng giản đơn như, đút chìa khóa vào đâu, đèn ở chỗ nào, sử dụng tín hiệu quẹo ra sao, hay làm sao để điều chỉnh gương chiếu hậu.

Sau đó, mọi thứ lại êm đẹp như cũ. Nhưng tại sao lần sau lại dễ hơn lần trước nhiều vậy?

Đó là vì chiếc xe mới cũng không khác chiếc cũ là bao cả, cũng với những thứ cơ bản cần có ở một chiếc xe hơi, và gần như cũng được đặt ở cùng một vị trí.

Điểm khác biệt xuất hiện rất ít, và có thể là thêm một vài tính năng phụ nữa, nhưng chúng ta lại chẳng cần đến chúng ở những lần lái đầu tiên làm gì. Dần dần, chúng ta đều biết rõ các tính năng mới (hoặc chí ít là những tính năng chúng ta thực sự quan tâm).

Ừ thì, học ngôn ngữ lập trình cũng không khác là mấy. Lần đầu tiên là khó nhất. Nhưng khi bạn đã thành thạo rồi, những ngôn ngữ thứ hai thứ ba trở nên dễ dàng hơn.

Khi chuyển sang ngôn ngữ thứ hai, bạn thường hay tự hỏi những câu hỏi như, “Tạo module như thế nào nhỉ? Tìm array ra sao? Tham số nào là của hàm substring?”

Bạn tự tin rằng bạn có thể học cách dùng ngôn nhữ mới này vì nó gợi lại ngôn ngữ cũ, có thể với một vài thứ mới với hy vọng trợ giúp bạn hiệu quả hơn.

Phi thuyền đầu tiên

Dù bạn có chạy một hay hàng chục chiếc xe trong đời, hãy tưởng tượng bạn chuẩn bị phải lái một chiếc tàu vũ trụ thử xem.

Nếu bạn định lái tàu vũ trụ, đừng hy vọng kỹ năng lái xe trên đừng sẽ giúp nhiều nhé. Bạn sẽ phải quay lại từ con số không.

Bạn sẽ bắt đầu luyện tập với suy nghĩ rằng mọi thứ trong không gian sẽ rất khác với mặt đất. Vật lý không hề thay đổi, sự thay đổi duy nhất là cách bạn đi lại trong cùng một vũ trụ mà thôi.

Tương tự như vậy với Lập Trình Hàm, bạn cần biết rằng mọi thứ sẽ rất khác. Và những gì trước đó bạn biết về lập trình sẽ không thể áp dụng với lĩnh vực mới lạ này.

Lập trình là tư duy, và Lập Trình Hàm sẽ dạy bạn cách tư duy khác đi. Khác biệt đến nỗi các bạn không cách nào tư duy được như cũ.

Quên mọi thứ bạn đã biết

Học lập trình hàm cũng không khá gì bập bẹ tập đi cả. Hiển nhiên, vẫn có nhiều khái niệm tương tự nhưng tốt hơn hết bạn nên chuẩn bị tinh thần học lại hết mọi thứ là vừa.

Với cách nhìn đúng đắn, bạn có kỳ vọng chuẩn xác hơn. Và khi có kỳ vọng chuẩn xác, bạn sẽ không bỏ ngang khi kiến thức ngày càng “khó nhằn”.

Sẽ có rất nhiều điều bạn, với cương vị lập trình viên, hay làm mà bạn không thể bê được sang lập trình hàm.

Giống như trong xe hơi, bạn hay lùi lại để lái xe ra khỏi chỗ để xe. Nhưng tàu vũ trụ lại chả chạy giật lùi được. Bạn sẽ trở nên hoảng loạn “CÁI GÌ CƠ? KHÔNG CHẠY LÙI ĐƯỢC À?! KHÔNG CÓ CHẠY LÙI THÌ CHÉO LÁI CÁI QUÁI GÌ NỮA?!”

Hóa ra, tàu vũ trũ không cần chạy lùi vì khả năng chuyển động đặc biệt trong không gian ba chiều. Khi bạn bắt đầu nhận ra điều này, bỗng nhiên chạy lùi được hay không cũng không còn quan trọng nữa. Thực ra, một ngày nào đó, bạn sẽ nhìn lại và nhận ra chiếc xe hơi thực sự tù túng như thế nào.

Hãy cùng thoát khỏi thế giới Lập Trình Mệnh Lệnh lạnh giá và cùng nhảy vào suối Lập Trình Hàm ấm áp áp.

Tiếp theo sau đây, chúng tôi sẽ giới thiệu đến các bạn một số khái niệm tối quan trọng trong Lập Trình Hàm cần nắm trước khi bạn bước vào thế giới đầy lạ lẫm này.

Purity

Khi lập trình viên chuyên về Lập Trình Hàm nói về Purity, họ đang nói đến các hàm Pure (hàm thuần).

Hàm Thuần là những hàm rất đơn giản. Chúng chỉ hoạt động trên tham số nhập.

Sau đây là một ví dụ về Hàm Thuần trong JavaScript:

Chú ý rằng hàm add không động đến biến z; không đọc từ z và cũng không viết đến z. Hàm này chỉ đọc xy (input của nó) và trả kết quả cộng hai biến với nhau.

Đó là một hàm thuần. Nếu hàm add tiếp cập được z, hàm này không còn “thuần” nữa.

Sau đây là một hàm ví dụ nữa:

Trong hàm này, justTen, là hàm thuần, vì thế chỉ có thể trả hằng số thôi. Tại sao vậy?

Vì chúng ta không cung cấp bất cứ input nào cả. Như vậy, để là “thuần”, hàm không thể tiếp cận được bất cứ thông tin nào ngoài input của nó, và kết quả duy nhất có thể trả về là một hằng số.

Vì hàm thuần không tham số không làm được gì cả, nên rất ít được áp dụng. Nếu justTen được xác định làm hằng thì sẽ tốt hơn.

Đa số Hàm Thuần hữu dụng phải tiếp nhận ít nhất một tham số.

Xem thử hàm sau:

Bạn thấy hàm này không trả lại bất cứ kết quả nào đúng không. Nó cộng xy và đặt vào biến z nhưng lại không trả kết quả.

Đây chính là hàm thuần vì hàm này chỉ làm việc với input của nó thôi. Hàm có cộng, nhưng lại không trả kết quả, nên cũng khá vô dụng.

Tất cả Hàm Thuần hữu ích phải có trả kết quả.

Hãy xét lại hàm add đầu tiên ở trên:

Để ý rằng, add(1, 2) sẽ luôn là 3. Không quá ngạc nhiên nhưng chỉ vì hàm này thuần. Nếu hàm add sử dụng một số giá trị bên ngoài, thì bạn không bao giờ có thể dự đoán hành vi của nó.

Hàm Thuần sẽ luôn luôn tạo ra cùng một output khi được chỉ định cùng input.

Vì Hàm Thuần không thể thay đổi bất cứ biến ngoài nào, nên tất cả hàm sau đều impure (bất thuần):

Tất cả các hàm trên đều cùng có điểm chung là Side Effects (hiệu ứng phụ). Khi bạn call những hàm này, chúng thay đổi file và database table, gửi data đến server hoặc call OS để lấy socket. Chúng không chỉ dừng lại ở các thao tac trên input của chúng và trả output, nên bạn không cách nào dự đoán được những hàm này sẽ trả kết quả gì.

Hàm Thuần không có hiệu ứng phụ

Trong các Ngôn Ngữ Lập Trình Mệnh Lệnh như JavaScript, Java, và C#, bạn sẽ bắt gặp Hiệu Ứng Phụ ở khắp mọi nơi. Từ đó khiến việc debug trở nên khó khăn hơn vì một biết có thể được thay đổi bất cứ đâu trong chương trình. Vậy khi bạn gặp bug vì giá trị của một biến bị thay đổi sang giá trị sai lệch tại thời điểm sai lệnh, bạn sẽ phải tìm ở đâu? Mọi nơi? Không ổn rồi.

Tại thời điểm này, chắc bạn đang nghĩ, “THẾ CHỈ DÙNG HÀM THUẦN THÌ LÀM QUÁI GÌ NÊN VIỆC ĐÂY?!”

Trong Lập Trình Hàm, bạn không chỉ viết hàm Thuần.

Ngôn Ngữ Hàm không thể loại bỏ Hiệu Ứng Phụ, mà chỉ có thể hạn chế mà thôi. Vì chương trình phải tương tác với thế giới thật, phần nào đó của mỗi ứng dụng phải impure. Mục tiêu của lập trình hàm là giảm thiểu lượng code impure và tách chúng khỏi phần còn lại của chương trình.

Bất biến

Bạn có nhờ khi lần đầu nhìn thấy đoạn code này không:

Và giáo viên, dù họ là ai, cũng sẽ dặn bạn quên đi những gì đã học ở lớp toán đúng không? Vì x không bao giờ có thể bằng x+1 được.

Nhưng trong lập trình lệnh, đoạn code này sẽ tiếp nhận giá trị hiện tại của x thêm 1 vào đó và đặt kết quả trở lại x. Còn với lập trình hàm, x = x + 1 sẽ không được chấp nhận.

Không có biến trong lập tình hàm.

Các giá trị được lưu trữ vẫn được gọi là biến (vì lịch sử) nhưng chúng lại là hằng số (vd: khi x nhận một giá trị, giá trị sẽ vĩnh viễn ở đó).

Đừng lo, x thường là biến cục bộ và có vòng đời khá ngắn. Nhưng miễn là nó còn sống, nó sẽ không bao giờ thay đổi.

Sau đây là một ví dụ về biến không đổi trong Elm (Ngôn Ngữ Lập Trình Thuần cho Lập Trình Web):

Nếu bạn không quen thuộc với cú pháp ML-Style, để tôi giải thích. addOneToSum là hàm tiếp nhận hai tham số, yz.

Trong block let, x được gắn với giá trị 1. Ví dụ: bằng với 1 cho đến cuối đời. Vòng đời của nó sẽ kết thúc khi hàm exit, hoặc chính xác hơn, khi block let được eveluate.

Trong block in, phép toán có thể bao gồm các giá trị được xác định trong block let, viz. x. kết quả của phép tính x + y + z được trả về; hoặc chính xác hơn, 1 + y + z được trả về vì x = 1.

Một lần nữa, chắc bạn lại tự hỏi “KHÔNG CÓ BIẾN THÌ LẬP TRÌNH SAO ĐÂY?!”

Như vậy trong trường hợp nào chúng ta muốn điều chỉnh biến? Có hai trường hợp: thay đổi đa giá trị (như thay đổi một giá trị duy nhất của và object hay record) và thay đổi đơn giá trị (như bộ đếm lặp).

Lập Trình Hàm làm việc với các thay đổi đến giá trị trong một record bằng cách tạo bản sao của record với các giá trị đã được thay đổi. Vì không phải sao chép tất cả các phần của record (thông qua các cấu trúc dữ liệu đặc biệt) nên phương pháp này vẫn tỏ ra vô cùng hiệu quả.

Lập trình hàm giải quyết thay đổi đơn giá trị theo hướng tương tự, tạo bản sao.

Và tất nhiên là không có vòng lặp.

“KHÔNG CÓ BIẾN, KHÔNG CÓ VÒNG LẶP NỮA À?!”

Khoan nào. Không có vòng lặp đâu có nghĩa chúng ta không lặp được, chỉ có điều là ta sẽ không có các lệnh lặp cụ thể như for, while, do, repeat,… mà thôi.

Lặp Trình Hàm dùng đệ quy để lặp.

Trong JavaScript, bạn có hai cách lặp:

Bạn thấy đấy, đệ quy (phương pháp hàm) có kết quả giống như vòng lặp for bằng cách tự call chính nó với một khởi đầu mới (start + 1) và bộ chứa mới (acc + start). Phép đệ quy trên không chỉnh sửa các giá trị cũ, thay vào đó sử dụng các giá trị mới được tính toán từ giá trị cũ.

Không may thay, dù bạn có dành chút thời gian nghiên cứu, đây vẫn là một điểm khó nhận biết trong JavaScript vì hai lý do. Thứ nhất, cú pháp của JavaScript khá lộn xộn, và hai, có thể bạn chưa quen với lỗi nghĩ đệ quy.

Trong Elm, phép đệ quy dễ đọc hơn, và vì thể, dễ hiểu hơn:

Lệnh trênh chạy như sau:

Chắc bạn đang nghĩ vong lặp for dễ hiểu hơn nhiều. Dù có đúng hay sai, hay chỉ là do thân quen, vòng lặp không đệ quy cần tính đột biến, và đột biến không hay đâu.

Tôi vẫn chưa thể hoàn toàn giải thích những lợi thế đế từ Tính Bất Biến ở đây, nhưng các bạn có thể đọc thử phần Global Mutable State trong phần Why Programmers Need Limits để tìm hiểu thêm.

Một lợi ích rõ rệt là nếu bạn có thể tiếp cận với một giá trị trong chương trình của bạn, bạn chỉ có quyền đọc mà thôi, đồng nghĩa rằng sẽ không có ai khác thay đổi được giá trị đó, kể cả chính bạn. Nên sẽ không có đột biến bất ngờ.

Hơn nữa, nếu chương trình của bạn là đa luồn, thì sẽ không có luồn nào khác có thể “phá bĩnh” bạn. Giá trị đó là bất biến và nếu một luồn khác muốn thay đổi, nó sẽ phải tạo một giá trị mới từ giá trị cũ.

Hồi những thập niên 90s, tôi từng viết một Game Engine cho Creature Crunch và lý do bug nhiều nhất là vấn đề đa luồn. Ước gì tôi biết được về “tính bất biến” ngày đó. Nhưng lúc ấy, thú thật tôi lo hơn về ảnh hưởng từ sự khác biết giữa tốc độ 2x hoặc 4x trên ỗ đĩa CD lên hiệu năng game.

Tính bất biến cho ra code đơn giản và an toàn hơn.

Ôi đâu đầu quá!!!!

Chắc bây nhiêu đây cũng đủ rồi rồi nhỉ. Trong các bài viết tiếp theo, chúng ta sẽ nói tiếp về Hàm cấp cao (Higher-order Functions), Phức Hợp Hàm (Functional Composition), Currying,…

ITZone via medium

Chia sẻ bài viết ngay