Haskell #1: Haskell

1.0. Lời tựa

Hình 1.0.1. Logo Haskell, lấy cảm hứng từ Monad >>= (một khái niệm trừu tượng trong Haskell) và Lambda $\lambda$ (chữ cái Hy Lạp)
Hình 1.0.1. Logo Haskell, lấy cảm hứng từ Monad >>= (một khái niệm trừu tượng trong Haskell) và Lambda λ (chữ cái Hy Lạp)

Tôi quyết định viết những trang blog này vì muốn học lại kiến thức về Haskell (theo phương pháp của Feynman) và trong tương lai gần, khi lập trình hàm lên ngôi, tôi mong mình có thể giúp những người chưa biết gì về Haskell học tư duy của ngôn ngữ này nhanh chóng. Cho đến hiện tại, hầu như không có hướng dẫn chuẩn nào trên mạng (đặc biệt là tiếng việt), đa số chỉ là những blog lẻ tẻ. Khi bắt đầu học Haskell, học từ một nguồn tài liệu duy nhất là không đủ, cách học của tôi là đọc vài cuốn sách khác nhau, bài báo khoa học nữa, vì mỗi tài liệu đều có cách diễn giải riêng.

Hình 1.2. Một số nguồn tự học Haskell
Hình 1.2. Một số nguồn tự học Haskell

Blog này chủ yếu dành cho những người đã có kinh nghiệm với C, C++, Java, Python, … nhưng chưa từng biết đến lập trình hàm (Haskell, ML, OCaml …). Tuy nhiên nếu bạn chưa từng lập trình thì vẫn có thể dễ tiếp thu một cách dễ dàng.

Trước khi hiểu Haskell, tôi đã thất bại nhiều lần vì nó khá dị và đi ngược lại với tư tưởng OOP trước giờ. Nhưng một khi đã “vào guồng”, mọi chuyện sẽ thuận buồm xuôi gió. Học Haskell rất giống với lần đầu học lập trình - rất vui.

1.1. Hướng dẫn đọc

Trong series này sẽ có khá nhiều hình minh họa và ký hiệu liên quan đến toán học, do đó tôi quy ước vài điều như sau:

1.1.1. Ký hiệu

fg, … là hàm, hàm đơn giản là một thực thể nhận đầu vào, xử lý gì đó và trả về kết quả. Ở trong Haskell chúng ta xem mọi thứ đều là hàm, kể cả những giá trị như 5, 100, … (hàm hằng).

ab, … hay những chữ cái thường (hoặc từ có chữ cái đầu thường) đại diện cho tham số (hay giá trị cụ thể) được truyền vào và sử dụng trong thân hàm. Ví dụ: a=5newList=[1,2,3], …

FAM, … hay những chữ cái in hoa (hoặc từ có chữ cái đầu in hoa) ký hiệu cho kiểu dữ liệu trừu tượng. Ví dụ: Ord là kiểu dữ liệu có thứ tự, Bool là kiểu dữ liệu thuộc về đại số Boolean, ….

1.1.2. Quy ước về hình ảnh minh họa

Tôi sẽ sử dụng hình vuông để ký hiệu cho hàm và hình vuông có cạnh là nét đứt ký hiệu cho phép áp dụng hàm (gọi hàm).

Hình vuông bo tròn ở góc biểu thị cho ngữ cảnh (context) ở các kiểu dữ liệu trừu tượng.

Mũi tên biểu thị cho liên kết giữa các hàm và mũi tên nét đứt biểu diễn thứ tự biến đổi của các đối tượng (trình tự thời gian).

1.2. Haskell là gì?

Hình 1.2.1. Một hàm số cơ bản
Hình 1.2.1. Một hàm số cơ bản

Haskell là một ngôn ngữ lập trình hàm thuần túy (purely functional programming language). Nó khác với những ngôn ngữ lập trình bạn đã gặp ở chỗ chúng ta thường giải quyết bài toán bằng cách đặt ra một loạt các bước làm thay đổi trạng thái của các biến trong chương trình. Chẳng hạn, gán a = 5 là giá trị khởi điểm, rồi gán lại một giá trị khác hoặc xài forwhile do, … Chương trình giống như một cái máy, ta yêu cầu chúng lặp đi lặp lại công việc nào đó. Trong lập trình hàm, lập trình viên chỉ cần nói cho máy tính biết cần phải làm điều gì thông qua định nghĩa hàm. Ví dụ, n! là tích các số nguyên từ 1 đến n, i=0mxi là x0+i=1mxi và cứ như vậy cho đến khi i=m. Bạn có thể tưởng tượng lập trình hàm giống như chúng ta giải quyết bài toán theo cách ngắn gọn nhất bằng cách sử dụng các định nghĩa, định lý, phép biến đổi, …

Hình 1.2.2. Vòng đời của một hàm, nhận tham số, xử lý và trả về kết quả
Hình 1.2.2. Vòng đời của một hàm, nhận tham số, xử lý và trả về kết quả

Việc gán a = 5, rồi lại thay đổi giá trị của nó được gọi hiệu ứng lề (side effect), vốn đã quá quen thuộc trong quá trình chúng ta lập trình, tuy nhiên Haskell đã loại bỏ hoàn toàn chúng bằng cách định nghĩa mọi thứ bằng hàm. Điều duy nhất hàm thực hiện là lấy tham số (1), tính toán (2) và trả lại giá trị (3). Thoạt đầu, điều này có vẻ tù túng nhưng về sau chúng ta sẽ nhận ra lợi ích của nó. Thứ nhất, nếu một hàm được gọi nhiều lần với cùng tham số thì nó luôn luôn trả về cùng kết quả, tính chất này được gọi là minh bạch tham chiếu (referential transparency) và cho phép trình biên dịch hiểu được hành vi của chương trình. Thứ hai, nó giúp bạn suy luận (thậm chí chứng minh) rằng hàm đó đúng, để từ đó xây dựng những hàm phức tạp hơn bằng cách kết hợp những hàm đơn giản lại.

Lazy

Haskell có tính lười (lazy). Tức là trừ khi đưa ra yêu cầu cụ thể thì Haskell sẽ không thực thi cho đến khi nó thực sự phải trả về kết quả. Đặc tính này kết hợp tốt với referential transparency, giúp cho chúng ta hình dung chương trình như là một loạt những phép biến đổi (transformation). Hay cho phép tồn tại những điều thú vị như cấu trúc dữ liệu vô hạn. Giả sử bạn có List xs = [1,2,3,4,5,6,7,8] và hàm doubleMe có nhiệm vụ nhân mỗi phần tử lên 2 lần rồi trả lại một List mới. Nếu ta muốn nhân List này lên 8 lần, bằng cách dùng ngôn ngữ lập trình khác như C và viết doubleMe(doubleMe(doubleMe(xs))), thì nó sẽ duyệt qua List một lần để tạo một bản sao (tempt) rồi trả lại List, sau đó làm tương tự hai lần nữa. Đối với Haskell, việc gọi doubleMe đối với một List mà không yêu cầu kết quả ngay lập tức thì chương trình sẽ có tính trì hoãn. Khi bạn muốn xem kết quả, thì doubleMe (1) sẽ nói với doubleMe (2) rằng nó muốn kết quả, doubleMe (2) sẽ đẩy lại cho doubleMe (3) và cái thứ ba miễn cưỡng thực thi phép tính 2*1, tức là 2. doubleMe (2) nhận lấy giá trị này và đưa số 4 cho doubleMe (1)doubleMe (1) nhận lại và báo với bạn là xs[0] = 8. Như vậy chỉ có một lần duyệt qua List và thao tác tính toán chỉ thực thi khi thực sự cần.

Statically typed

Haskell có kiểu tĩnh (statically typed). Tức là khi biên dịch, compiler sẽ hiểu được kiểu dữ liệu cho đối tượng. Nếu cố gắng cộng một int và một string với nhau, compiler sẽ báo lỗi. Khác với những ngôn ngữ bạn đã từng biết, Haskell có một hệ thống kiểu rất tốt và có khả năng suy luận kiểu (type inference). Nếu khai báo a = 5 + 4, chúng ta sẽ không cần khai báo a kiểu float. Type inference giúp cho code tổng quát hơn, (giống như Python hay JavaScript, tuy nhiên cần phân biệt rõ hai ngôn ngữ này thuộc về kiểu khai báo động).

Haskell tinh tế và ngắn gọn. Vì dùng nhiều khái niệm cấp cao, chương trình Haskell thường ngắn hơn các chương trình tương đương. Và chương trình ngắn thì dễ bảo trì hơn và ít bug hơn so với chương trình dài. Tuy nhiên đây cũng là một điểm hạn chế khiến người mới bắt đầu học Haskell khó tiếp cận.


1 nhận xét:

nhận xét
lúc 11:53 28 tháng 4, 2022 ×

eee

Congrats bro admin you got PERTAMAX...! hehehehe...
Reply
avatar