Tuy nhiên các kiểu được sử dụng để gán cho các biến đầu vào của ngôn ngữ là có sẵn và được gán một cách tự động khiến người lập trình khó có thể tùy biến được trong F#.. CHƯƠNG 1: TỔNG Q
Trang 1ĐẠI HỌC QUỐC GIA HÀ NỘI TRƯỜNG ĐẠI HỌC CÔNG NGHỆ
Trang 2ĐẠI HỌC QUỐC GIA HÀ NỘI TRƯỜNG ĐẠI HỌC CÔNG NGHỆ
LUẬN VĂN THẠC SĨCÔNG NGHỆ THÔNG TIN
NGƯỜI HƯỚNG DẪN KHOA HỌC: PGS TS Trương Anh Hoàng NGƯỜI ĐỒNG HƯỚNG DẪN KHOA HỌC: TS Nguyễn Như Sơn
Trang 3LỜI CAM ĐOAN
Tôi xin cam đoan luận văn này là công trình nghiên cứu của cá nhân tôi, dưới sự hướng dẫn trực tiếp từ phía PGS.TS.Trương Anh Hoàng và TS Nguyễn Như Sơn Các số liệu, nội dung tham khảo được trích dẫn có nguồn gốc rõ ràng, tuân thủ tôn trọng quyền tác giả Kết quả cuối cùng đạt được của luận văn là thành quả của quá trình nghiên cứu bản thân, chưa từng được công bố dưới bất kỳ hình thức nào
Tôi xin chịu trách nhiệm về nghiên cứu trong luận văn
Vũ Quang Hưng
Trang 4LỜI CẢM ƠN
Để hoàn thành đề tài luận văn này, bên cạnh sự chủ động cố gắng của bản thân, tôi
đã nhận được sự ủng hộ và giúp đỡ nhiệt tình từ các tập thể, cá nhân trong và ngoài trường
Qua đây, cho phép tôi được bày tỏ lòng cảm ơn sâu sắc tới thầy giáo PGS.TS.Trương Anh Hoàng, giảng viên bộ môn Công nghệ phần mềm trường Đại học công nghệ – Đại học Quốc gia Hà Nội, người đã trực tiếp động viên, định hướng và hướng dẫn tận tình trong quá trình học tập và hoàn thành đề tài luận văn này
Đồng kính gửi lời cảm ơn đến tập thể các thầy, cô giáo trong trường Đại học Công Nghệ – Đại học Quốc gia Hà Nội đã trau dồi kiến thức cho tôi, điều đó là nền tảng quí báu góp phần to lớn trong quá trình vận dụng vào hoàn thiện luận văn
Cuối cùng, tôi xin được gửi lòng biết ơn sâu sắc đến gia đình, bạn bè, đồng nghiệp
đã tạo điều kiện về vật chất cũng như tinh thần, luôn sát cánh bên tôi, động viên giúp tôi yên tâm học tập và kết thúc khóa học
Xin chân thành cảm ơn!
Tác giả
Vũ Quang Hưng
Trang 5MỤC LỤC
LỜI CAM ĐOAN 3
LỜI CẢM ƠN 4
MỤC LỤC 5
DANH MỤC CÁC KÝ HIỆU, THUẬT NGỮ, CHỮ VIẾT TẮT 7
DANH MỤC CÁC BẢNG 8
DANH MỤC CÁC HÌNH VẼ 9
PHẦN MỞ ĐẦU 10
Tính cấp thiết của đề tài 10
Mục tiêu của luận văn 10
Công cụ phần mềm 10
Phương pháp nghiên cứu 11
Bố cục luận văn 11
CHƯƠNG 1: TỔNG QUAN VỀ LẬP TRÌNH HÀM 12
1.1 Giới thiệu chung về ngôn ngữ lập trình hàm 12
1.2 Các đặc điểm nổi bật của ngôn ngữ lập trình hàm 13
1.3 Sự phổ biến của ngôn ngữ lập trình hàm 14
1.4 Giới thiệu tổng quan về ngôn ngữ lập trình hàm F# 15
CHƯƠNG 2: NGHIÊN CỨU LÝ THUYẾT VỀ NGÔN NGỮ F* 17
2.1 Giới thiệu chung 17
2.1.1 Giới thiệu về ngôn ngữ F* 17
2.1.2 Giới thiệu về kiểu phụ thuộc, hệ thống kiểu phụ thuộc 17
2.2 Các đặc điểm nổi bật của ngôn ngữ F* 18
2.2.1 Ngôn ngữ tự chứng thực F* 18
2.2.2 Trình biên dịch từ F* sang mã JavaScript 19
2.3 Các khái niệm cơ bản khi lập trình với F* 20
2.3.1 Các định nghĩa kiểu và loại thường dùng trong F* 20
2.3.2 Các khái niệm chung về khai báo kiểu trong F* 23
Trang 62.3.3 Lý thuyết về tập hợp: 23
2.3.4 Định nghĩa, sử dụng mảng dữ liệu trong F* 24
2.3.5 Kiểu của số tự nhiên (NAT) 26
2.3.6 Chứng minh tính chất cơ bản trong F* 26
2.3.7 Loại suy luận và các ảnh hưởng tới tính toán 28
2.3.8 Sử dụng F* lập trình với các bài toán đơn giản 29
2.3.9 Chứng minh bổ đề (Lemmas) 31
2.3.10 Chứng minh tính kết thúc của chương trình trong F* (Proving termination) 35 2.4 Kết luận chương 38
CHƯƠNG 3: BÀI TOÁN ỨNG DỤNG 39
3.1 Ứng dụng F* vào các bài toán lập trình 39
3.1.1 Ứng dụng trong bài toán sắp xếp mảng nổi bọt (Buble sort) 39
3.1.2 Ứng dụng trong lập trình sắp xếp mảng nhanh (Quick sort) 40
3.1.3 Ứng dụng trong cái bài toán làm việc với các tập tin, thư mục 45
3.2 Ứng dụng F* trong bài toán tính tổng tài nguyên sử dụng chương trình 48
3.2.1 Giới thiệu bài toán 48
3.2.2 Giải quyết bài toán 50
3.2.3 Tính toán giá trị mức giới hạn trên tổng chi phí tài nguyên cho chương trình 57 KẾT LUẬN 60
TÀI LIỆU THAM KHẢO 61
PHỤ LỤC CÁC CÔNG CỤ HỖ TRỢ CÀI ĐẶT THỰC NGHIỆM 62
Trang 7DANH MỤC CÁC KÝ HIỆU, THUẬT NGỮ, CHỮ VIẾT TẮT
3 TFJ string Là một ngôn ngữ mở rộng của FJ tích hợp mô
7 AST (Abstract Syntax Tree) Cây cú pháp trừu tượng
10 Lock-based synchronization Đồng bộ hóa dựa trên khóa
11 Nested transactions Các giao tác lồng
13 Join Hàm khử dấu trừ trong chuỗi có dấu chính tắc
16 Joint commits Các commit của các luồng song song đồng thời
thực hiện kết thúc một giao tác chung
KÝ HIỆU
chuỗi số có dấu, m thao tác onacid liên tiếp
chuỗi số có dấu, m thao tác commit liên tiếp
chuỗi số có dấu, m các giao tác lồng nhau
4 ¬m Mô tả thành phần ¬ thể hiện số lượng joint
commit trong hệ thống kiểu dựa trên chuỗi số có
Trang 8dấu
DANH MỤC CÁC BẢNG
Bảng 2.1: Khai báo biểu thức, kiểu, loại trong ngôn ngữ F* 20
Bảng 3.1 Bảng kết quả kiểm thử phép toán chính tắc chuỗi số có dấu 52
Bảng 3.2 Bảng kết quả kiểm khử dấu “−” thành dấu “¬” 53
Bảng 3.3 Bảng kết quả kiểm khử hàm merge 54
Bảng 3.4 Bảng kết quả kiểm khử hàm joint commit 57
Trang 9DANH MỤC CÁC HÌNH VẼ
Hình 2.1: Kết quả cho bài toán tính giai thừa 20
Hình 2.2: Các kiểu, loại dữ liệu được sử dụng trong F* [4] 22
Hình 3.1: Kết quả cho bài toán sắp xếp nổi bọt 40
Hình 3.2: Kết quả cho bài toán sắp xếp nhanh 45
Hình 3.3: Ví dụ mô hình giao tác lồng, đa luồng và joint [13] 49
Trang 10PHẦN MỞ ĐẦU
Tính cấp thiết của đề tài
Hiện nay ngành công nghiệp phần mềm đang rất phát triển ở nhiều lĩnh vực Trên thực tế, tùy theo yêu cầu của mỗi lĩnh vực mà chúng ta có thể lựa chọn các ngôn ngữ lập trình sao cho phù hợp Chúng ta có thể thấy rất nhiều ngôn ngữ lập trình được sử dụng ngày nay, trong đó phải kể đến một số ngôn ngữ lập trìnhrất được phổ biến nhưlà Java, C#, Objective-C, JavaScript, SQL, PHP Các ngôn ngữ lập trình hàm như OCaml, ML, F# cũng dần được phổ biến trong khoảng thời gian gần đây Ngôn ngữ lập trình hàm F# là
ngôn ngữ có kiểu mạnh(strongly-typed) và tự suy luận kiểu (không cần phải khai báo kiểu
cho các biến đầu vào), trình biên dịch có thểtự suy luận ra kiểu của các biến đầu vào đó khi dịch chương trình Tuy nhiên các kiểu được sử dụng để gán cho các biến đầu vào của ngôn ngữ là có sẵn và được gán một cách tự động khiến người lập trình khó có thể tùy biến được trong F#
Từ nhu cầu này, ngôn ngữF* đã ra đời Ngôn ngữ F* có hệ thống kiểu được xây dựng dựa trên nền tảng lý thuyết System Fω [1] nhưng được mở rộng hơn với hệ thống kiểu phụ thuộc, các kiểu được tùy chỉnh, cho phép người lập trình có thể làm mịn kiểu dữ liệu cho chặt hơn, phù hợp hơn với ý đồ.Ngoài ra F* có thể kiểm chứng tính đúng đắncủa chương trình theo kiểu dữ liệu mới được làm mịn này.Tức làthông thường, chúng ta chỉ
có kiểu số nguyên (int) nhưng với ngôn ngữ F* chúng ta có thể khai báo kiểu số nguyên
nằm trong khoảng 0 đến 10, hay chỉ gồm các số chẵn hoặc chỉ có các số lẻ Hơn nữangôn ngữ F* có thể được dịch sang những ngôn ngữ khác như OCaml, F# hoặc JavaScript để
thực thi Những vấn đề trên là cơ sở khoa học và thực tiễn để tôi thực hiện đề tài “Nghiên
cứu ứng dụng ngôn ngữ F* trong phát triển phần mềm”
Mục tiêu của luận văn
Trên cơ sở nghiên cứu lý thuyết, cài đặt, thử xây dựng các chương trình cơ bản, luận văn đã ứng dụng ngôn ngữ F* vào việcxây dựng công cụ tính tổng tài nguyên được sử dụng trong chương trình đa luồng có dùng bộ nhớ giao dịch, theo bài báo nghiên cứu của thầy hướng dẫn
Trang 11Phương pháp nghiên cứu
Để đề tài có thể đạt được kết quả như mục tiêu đặt ra, trong luận văn, tôi đã đề xuất
và áp dụng các phương pháp nghiên cứu như sau:
- Nghiên cứu các tài liệu liên quan đến lập trình hàm nói chung và lập trình ngôn ngữ F* nói riêng
- Thử nghiệm với một số chương trình cơ bảnsử dụng ngôn ngữ F*
- Ứng dụng xây dựng công cụ phần mềm: Cài đặt các thuật toán cho bài toán tính
giá trị giới hạn trêntổng chi phí tài nguyên sử dụng của chương trình đa luồng có
dùng bộ nhớ giao dịch trong trường hợp xấu nhất
Bố cục luận văn
Trong luận văn này sẽ bao gồm các phần cơ bản sau:
- Phần mở đầu: Đưa ra tính cấp thiết của đề tài, công cụ phần mềm được sử dụng, phương pháp nghiên cứu và bố cục của luận văn
- Chương 1: Giới thiệu tổng quan về ngôn ngữ lập trình hàm, đưa ra các đặc điểm nổi bật của ngôn ngữ lập trình hàm và giới thiệu về sự phổ biến của ngôn ngữ lập trình hàm trong ngành phát triển phần mềm hiện nay
- Chương 2: Giới thiệu tổng quan về ngôn ngữ F*,phương pháp xây dựng một chương trình trên nền tảng F* Đưa ra các khái niệm, tính năng cơ bản và một số tính năng nâng cấp của F* so với các ngôn ngữ khác Khái quát và tóm tắt lại để đưa ra kết luận chung về ngôn ngữ F*
- Chương 3: Ứng dụng F* trên một số bài toán lập trình trong thực tế.Tiến hành thực nghiệm và kiểm tra chương trình xây dựng bài toán tính giá trị giới hạn trên tổng chi phí tài nguyên của chương trình đối tượng bằng ngôn ngữ F* Trình bày các thuật toán được sử dụng trong bài toán qua đó kết hợp các thuật toán lại với nhau để đưa ra kết quả cuối cùng cho bài toán
- Kết luận: Tổng hợp các kết quả đã đạt được, các vấn đề cần giải quyết và hướng
mở rộng của đề tài
Trang 12CHƯƠNG 1: TỔNG QUAN VỀ LẬP TRÌNH HÀM
1.1 Giới thiệu chung về ngôn ngữ lập trình hàm
Lập trình hàm là một mô hình lập trình trong đó việc tính toán là sự đánh giá của các hàm toán học, không sử dụng các trạng thái, dữ liệu biến đổi và các lệnh gán biến Lập trình hàm nhấn mạnh việc ứng dụng các hàm số, trái với phong cách lập trình mệnh lệnh, nhấn mạnh vào sự thay đổi trạng thái [2] Các ngôn ngữ lập trình hàm, đặc biệt là các loạn thuần về lập trình hàm có ảnh hưởng lớn trong giới học thuật hơn là dùng để phát triển các phần mềm thương mại Mục đích của việc thiết kế ngôn ngữ lập trình hàm là mô phỏng các hàm toán học một cách nhiều nhất có thể Trong lập trình hàm, biến là không cần thiết, như trong các bài toán học thường gặp
Trong logic toán học và khoa học máy tính, phép tính lambda là một hệ thống hình thức được sử dụng trong việc định nghĩa hàm số, ứng dụng hàm số và đệ quy Phép tính lambda được phát triển để trở thành công cụ quan trọng trong việc nghiên cứu các vấn đề
lý thuyết tính toán và lý thuyết đệ quy, và hình thành nên nền tảng cơ bản của mô hình lập trình hàm Đôi khi người ta sử dụng các biểu thức lambda để biểu diễn cho các hàm không tên (ví dụ: λ(x) x * x thay thế cho binh_phuong(x) ≡ x*x) Các tham số của biểu thức lambda gọi là các tham số biến kết ghép Khi biểu thức lamda được định trị với một tham số đã cho thì biểu thức được áp dụng cho tham số đó (Ví dụ (λ(x) x * x * x) (2) = 8)
Hiện nay, ngôn ngữ lập trình hàm đang dần được phổ biến với sự phát triển đáng kể của ngôn ngữ F# Ngôn ngữ F# có thể tận dụng hầu hết các công cụ phát triển trong Visual Studio F# hỗ trợ lập trình hướng đối tượng Khi lập trình với F#, các đoạn mã được viết ra cũng đơn giản hơn so với các ngôn ngữ như C# hoặc Java tuy nhiên điều khó khăn nhất đối với những lập trình viên chưa biết về lập trình hàm đó chính là cú pháp hoàn toàn mới của nó, gần như sẽ rất khác so với PHP, Java, C
Tại sao ngôn ngữ lập trình hàm lại xứng đáng để bạn bỏ thời gian để học và làm việc? Nếu là lập trình viên với nhiều đam mê thì ngôn ngữ lập trình hàm sẽ khơi gợi sự tò
mò theo một cách nào đó Trong lập trình hàm, lập trình viên sẽ làm việc thường xuyên hơn với các hàm hồi quy và quên đi các hàm như if else, while, do Việc lập trình nêu trên
có thể khiến lập trình viên cảm thấy khó khăn hơn rất nhiều, tuy nhiên khi đã quen với nó, bạn sẽ thấy kỹ năng lập trình của mình ở một trình độ cao hơn Ngoài ra, trong các hàm
Trang 131.2 Các đặc điểm nổi bật của ngôn ngữ lập trình hàm
Hệ thống kiểu có thể biết đến là đặc điểm nổi bật nhất trong ngôn ngữ lập trình hàm Trong F#, việc khai báo kiểu của biến là hoàn toàn không cần thiết Khi định nghĩa một biến và gán giá trị cho nó, trình biên dịch sẽ tự suy luận ra biến đó sẽ sử dụng kiểu gì và gán cho biến Tương tự như vậy, khi khai báo một hàm, lập trình viên có thể cân nhắc xem có cần khai báo kiểu dữ liệu đầu vào và kiểu dữ liệu trả về hay không vì sau khi định nghĩa các hàm tính toán, F# sẽ tự suy luận ra kiểu của các hàm đó Việc sử dụng các kiểu
dữ liệu đại số và so sánh với mẫu làm cho việc thao tác trên các cấu trúc dữ liệu phức tạp trở nên thuận tiện và rõ ràng hơn Sự tồn tại của việc kiểm tra kiểu mạnh mẽ trong thời gian biên dịch khiến cho các chương trình trở nên đáng tin cậy hơn, việc luận kiểu cũng giúp lập trình viên không cần khai báo thủ công các kiểu để biên dịch
Một số ngôn ngữ lập trình hàm định hướng nghiên cứu được biết đến hiện nay như Coq [3], Agda [4] Các kiểu được sử dụng trong các ngôn ngữ trên được gọi là các kiểu phụ thuộc Các kiểu phụ thuộc này có thể được mô tả bằng các mệnh đề tự do trong logic
mệnh đề Các chương trình có kiểu tốt (well-typed) trong những ngôn ngữ nêu trên sẽ trở
thành phương tiện cho việc viết các hàm chứng minh toán học hình thức mà từ đó trình biên dịch sẽ sinh ra các mã được chứng nhận
Ngoài ra, một đặc điểm khác có thể kể đến trong lập trình hàm đó là việc sử dụng đệ quy Vòng lặp trong các ngôn ngữ lập trình hàm thường được thực hiện thông qua đệ quy Hàm đệ quy sẽ tự gọi chính nó, cho phép thực hiện đi thực hiện lại một tác vụ Đa số các ngôn ngữ lập trình hàm đa mục đích đều cho phép đệ quy không giới hạn, tuy nhiên việc
đó có thể gây ra sự thiếu căn cứ cho việc suy diễn công thức, đòi hỏi phải có các khái niệm không nhất quán trong logic do hệ thống kiểu của ngôn ngữ quy định
Các chương trình trong lập trình hàm là các định nghĩa hàm và các áp dụng hàm Sự thực hiện là việc đánh giá các áp dụng hàm trong đó một hàm luôn cho cùng một kết quả trả về khi ta gán cho nó cùng một đối số ví dụ như f(x) + f(x) và 2*f(x) luôn cùng một kết quả Ngoài ra, ngữ nghĩa của ngôn ngữ lập trình hàm đơn giản hơn ngữ nghĩa của ngôn ngữ lập trình mệnh lệnh
Trong các ngôn ngữ lập trình hàm, các lời gọi chương trình con được viết thành biểu thức đơn giản Các ngôn ngữ hàm cũng là các ngôn ngữ bậc cao và mang tính trừu tượng hơn so với các ngôn ngữ mệnh lệnh Ngoài ra, lập trình hàm thường tránh sử dụng các biến toàn cục, trong khi đó, việc sử dụng biến toàn cục của người lập trình mệnh lệnh là cần thiết và nên dùng Khi lập trình với ngôn ngữ hàm, người lập trình cần phải định nghĩa các hàm toán học dễ suy luận, dễ hiểu mà không cần quan tâm chúng được cài đặt
Trang 14như thế nào ở trong máy Mặt khác, trong ngôn ngữ mệnh lệnh, việc thay đổi trạng thái toàn cục được cho rằng hoàn toàn bất lợi Nhiều phần khác nhau của chương trình tác động không trực tiếp lên các biến và do vậy làm cho chương trình khó hiểu Các thủ tục thường được gọi sử dụng ở các phần khác nhau của chương trình gọi nên rất khó xác định các biến bị thay đổi như thế nào sau lời gọi Như vậy, sự xuất hiện hiệu ứng phụ làm cản trở việc chứng minh tính đúng đắn (correctness proof), cản trở tối ưu hóa (optimization),
và cản trở quá trình song song tự động (automatic parallelization) của chương trình[5]
Bên cạnh những tính ưu việt, ta cũng cần xem xét những bất lợi của lập trình hàm: Thứ nhất đó là thiếu các lệnh gán và các biến toàn cục, thứ hai đó là có sự khó khăn trong việc mô tả các cấu trúc dữ liệu và khó để kiểm soát quá trình vào ra của dữ liệu Một vấn
đề khác gây khó khăn cho lập trình viên đó là rất khó để thay đổi một cấu trúc dữ liệu trong mảng Trong ngôn ngữ mệnh lệnh, sự thay đổi một phần tử mảng rất đơn giản Tuy nhiên trong ngôn ngữ lập trình hàm, một mảng không thể bị thay đổi Người ta cần sao chép mảng, loại bỏ những phần tử sẽ bị thay đổi và thay thế ngay lập tức các giá trị mới cho phần tử này Cách tiếp cận này kém hiệu quả hơn so với phép gán cho phần tử [5]
Kết luận lại, các ngôn ngữ hàm dựa trên việc tính giá trị của các biểu thức, các biến toàn cục và phép gán kiểu bị loại bỏ, giá trị được tính bởi một hàm phụ thuộc vào các tham đối, tuy nhiên thông tin trạng thái được đưa ra tưởng minh nhờ các tham đối của hàm và kết quả [5]
1.3 Sự phổ biến của ngôn ngữ lập trình hàm
Các ngôn ngữ lập trình hàm hiện nay được biết đến khá nhiều và một trong số đó có thể kể đến là F# Ngôn ngữ F# được cài đặt bởi tiến sĩ Don Syme tại Microsoft Research tại Cambridge Hiện tại được tiếp quản bởi Microsoft và vẫn đang được tiếp tục phát triển bởi nhóm chuyên gia ở cả Cambridge và Redmond
Ngôn ngữ F# mang đến cho bạn một ngôn ngữ lập trình hàm an toàn, gọn nhẹ mà hiệu quả Theo giấy phép chia sẻ nguồn của Microsoft, các đoạn mã nguồn được cung cấp sẵn cho lập trình viên Microsoft cho phép tải về miễn phí trình biên dịch dưới dạng phiên bản nhị phân như là một gói phần mềm độc lập hoặc là một bộ cài cho Visual Studio Với việc phát hành phiên bản F# 2.0, F# 3.0 và mới nhất là F# 4.0, nhóm phát triển sẽ chuyển
sang mô hình mới mà họ gọi là thả mã (Code drop) trong đó phiên bản mới của trình biên
Trang 151.4 Giới thiệu tổng quan về ngôn ngữ lập trình hàm F#
Như đã nhắc đến ở các phần trên, việc lập trình với ngôn ngữ F# khá là khác biệt so với các ngôn ngữ lập trình hiện nay như C#, Java Chúng ta có thể xem qua một số đoạn
mã sau đây để thấy được sự khác biệt của ngôn ngữ này:
let l1 = [ "a" ; "b" ; "c" ]
let l2 = [ "d" ; "e" ; "f" ]
Việc quyết định kiểu dữ liệu cho hàm sẽ tùy vào việc định nghĩa mảng dữ liệu nào trước Chúng ta định nghĩa l1 là mảng chuỗi kí tự thì l2 cũng phải là mảng chuỗi kí tự chứ không thể là chuỗi số tự nhiên được
Ngôn ngữ F# sử dụng thư viện NET cũng giúp cho việc lập trình với F# trở nên dễ dàng hơn Các thư viện khi lập trình với F# như System.IO, Microsoft.FSharp.Text.Lexing, Microsoft.FSharp.Text.Parsing.ParseHelpers và Microsoft.FSharp.Collections giúp ích rất nhiều trong các bài toán như đọc ghi dữ liệu, phân tích cú pháp trong một đoạn mã cho trước Mặt khác trong ngôn ngữ F#, chúng ta có thể tự khai báo một số kiểu mới cho chương trình ví dụ như đoạn mã dưới đây:
type TagNum = Tag * int
type TagSeq = TagNum list
type Tree =
| Branch of Tree list
Trang 16| Leaf of TagNum
Ở đây chúng ta có thể khai báo một kiểu dữ liệu mới đó là Tag Trong kiểu Tag chứa các giá trị có thể có như Plus, Minus, Max, Join Tiếp theo chúng ta khai báo một kiểu khác là TagNum được hiểu nôm na là 1 cặp gồm kiểu Tag và số tự nhiên (Ví dụ: (Plus, 1), (Minus,3), (Max,4)) Kiểu TagSeq là kiểu danh sách bao gồm mảng dữ liệu mà trong đó các phần tử có kiểu là TagNum Không chỉ dừng lại ở đó, khi làm việc với F#, chúng ta có thể khai báo và làm việc với cây cú pháp trừu tượng giống như các ngôn ngữ lập trình khác và cao cấp hơn, chúng ta có thể định nghĩa kiểu dữ liệu cho gốc và lá của cây cú pháp này Chúng ta có thể xây dựng nên cây cú pháp này theo như ví dụ sau:
letrec addTree lst1 =
match lst1 with
| [] -> []
| '-' ::xs -> List.append [Leaf (Tag.Minus, 1)] (addTree xs)
| '+' ::xs -> List.append [Leaf (Tag.Plus, 1)] (addTree xs)
| '(' ::xs -> [Branch (addTree xs)]
| ')' ::xs -> addTree xs
| x::xs -> addTree xs
Với giá trị đầu vào của mảng l1 là['(';'+';'1';'-';'2';')';'-';'1']ta sẽ có cây
cú pháp có dạng như sau: [Branch [Leaf (Plus,1); Leaf (Minus,2)];Leaf (Minus,1)]
Như vậy chúng ta có thể thấy rằng việc sử dụng ngôn ngữ lập trình hàm rất trực quan và giúp ích cho lập trình viên rất nhiều trong việc tự suy luận kiểu của hàm Qua một số ví dụ về lập trình với F#, ta có thể thấy rằng việc lập trình với ngôn ngữ này khá là đơn giản với cú pháp rõ ràng, các hàm được xây dựng có tính tổng quát cao Mặt khác, việc hỗ trợ xây dựng kiểu cũng là một điểm khá thú vị trong ngôn ngữ lập trình hàm này
Một ngôn ngữ lập trình hàm mới, kế thừa gần như toàn bộ các tính năng của F#, tuy nhiên bổ sung thêm việc khai báo các kiểu tùy chỉnh tinh vi hơn so với kiểu gốc, các chức năng xác thực tính đúng đắn của đoạn mã chương trình và đang được phát triển hiện nay
đó chính là ngôn ngữ F* Các chương dưới đây sẽ giới thiệu cái nhìn chung nhất, các đặc điểm và một số bài toán áp dụng khi lập trình với ngôn ngữ F*
Trang 17CHƯƠNG 2: NGHIÊN CỨU LÝ THUYẾT VỀ NGÔN NGỮ F*
2.1 Giới thiệu chung
2.1.1 Giới thiệu về ngôn ngữ F*
Fstar (F*) [2] là ngôn ngữ lập trình hàm có thể xác thực tính đúng đắn của các đoạn
mã viết ra, được phát triển bởi trung tâm nghiên cứu của Microsoft, MSR-Inria, Inria và trung tâm phần mềm IMDEA Với các phương thức và cách hoạt động gần giống với F7 [5], Coq [6], Agda [7] và một số ngôn ngữ ML [3] khác, F* có đầy đủ các tính năng của lập trình hàm như hệ thống kiểu, tính chặt chẽ, và là một ngôn ngữ lập trình bậc cao Hệ thống kiểu của F sao đa dạng phong phú hơn các ngôn ngữ sử dụng hệ thống kiểu phụ
thuộc (dependent type) [4] khác ở chỗ nó cho phép người sử dụng đặc tả chính xác hơn về
thông tin kiểu của biến và kiểu dữ liệu trả về của một hàm trong chương trình Các đặc tả trên sẽ được trình biên dịch kiểm tra một cách bán tự động về tính đúng đắn của chúng Nhờ vậy, nó thường được thiết kế để xây dựng các hệ thống có tính an toàn và độ tin cậy cao
Mặc dù là một ngôn ngữ mới nhưng người dùng có thể thiết lập, cài đặtF* một cách
dễ dàng với tốc độ biên dịch khá nhanh Mã nguồn của F* là mở và có thể chạy trên nhiều
hệ điều hành Chúng ta có thể tải bản cài đặt (binary) của trình biên dịch F* trên
Windows, Linux hoặc MacOS hay có thể tự mình xây dựng trình biên dịch F* từ mã nguồn mở trên github Bên cạnh đó, người dùng có thể dễ dàng lập trình với ngôn ngữ F* bởicú pháp, câu lệnh khi lập trình bằng ngôn ngữ này khá giống với F#, một ngôn ngữ khá phổ biến trong Visual Studio của Microsoft
Tuy nhiên cho đến nay, ngôn ngữ F* chưa thể chạy trực tiếp các đoạn mã được viết sẵn mà chỉ dừng lại ở việc xác thực tính đúng đắn của các đoạn mã trên Chính vì vậy, F*
hỗ trợ chuyển đổi các đoạn mã F* sang OCaml hoặc F#, JavaScript để lập trình viên có thể chạy hoặc gỡ lỗi chương trình được một cách linh hoạt hơn Điều đó đòi hỏi người dùng cần phải tìm hiểu thêm về các ngôn ngữ khác trong quá trình sử dụng F*
2.1.2 Giới thiệu về kiểu phụ thuộc, hệ thống kiểu phụ thuộc
Để hiểu rõ hơn về ngôn ngữ F*, sau đây tôi sẽ giải thích một số khái niệm về kiểu phụ thuộc và hệ thống kiểu phụ thuộc
Kiểu phụ thuộc:
Kiểu phụ thuộc[8] là kiểu mà phụ thuộc vào giá trị của các kiểu khác[9]
Trang 18Ví dụ:
- Kiểu An là kiểu mảng gồm n phần tử mà các phần tử mang kiểu A
- Kiểu Am * n là một ma trận có kích thước m*n với các phần tử có kiểu là A
Hệ thống kiểu phụ thuộc:
Hệ thống kiểu phụ thuộc là phương pháp phân tích tĩnh dựa trên các quy tắc kiểu được định nghĩa để đưa ra các khẳng định chương trình có xây dựng đúng không và cũng hạn chế các rủi ro có thể xảy ra Hệ thống kiểu có thể biết chính xác việc sử dụng các giá trị gán cho biến hoặc hàm mà không cần thực thi nó [10]
Các ngôn ngữ lập trình với hệ thống hỗ trợ kiểu phụ thuộc:
- Agda (programming language)
- ATS (programming language)
- Cayenne (programming language)
- Epigram (programming language)
- F* (programming language)
- Idris (programming language)
2.2 Các đặc điểm nổi bật của ngôn ngữ F*
2.2.1 Ngôn ngữ tự chứng thực F*
Ngôn ngữ tự chứng thực (Self Certification) là ngôn ngữ có khả năng kiểm chứng
tính đúng đắn của các chương trình được xây dựng bởi ngôn ngữ đó Một số ngôn ngữ trước đó cũng có khả năng này như Coq [11], Agda[7] Trình biên dịch F* có thể tự kiểm chứng tính đúng đắn của các chương trình xây dựng bằng F* Hơn nữa trình biên dịch F* lại được xây dựng bằng ngôn ngữ F* nên có thể sử dụng trình biên dịch của F* để kiểm chứng đúng những đoạn mã xây dựng ra trình biên dịch này.Khi thử tự kiểm chứng kiểu trên F* và chứng thực đúng những kiểu đó trên Coq, chúng ta có một kết quả tương đương [11] Điều đó chứng tỏ rằng, bộ kiểm tra kiểu trong trình biên dịch của F* hoàn toàn có thể cho một kết quả chính xác Mặt khác trong tương lai, ngôn ngữ F* còn có khả năng sử dụng linh hoạt trên các máy tính cá nhân và đặc biệt là trong các đoạn mã dành cho di động, nơi mà Coq không khả dụng
Trang 192.2.2 Trình biên dịch từ F* sang mãJavaScript
2.2.2.1 Giới thiệu
Nhiều công cụ lập trình hiện nay cho phép các lập trình viên phát triển các ứng dụng của họ ở các ngôn ngữ bậc cao và đưa chúng lên web thông qua trình biên dịch JavaScript[12].Trong khi thực tế và sử dụng, không có sự đảm bảo nào cho tính xác thực của các trình biên dịch đó, không có tính an toàn cho các chương trình thực thi với JavaScript Tuy nhiên khi kết hợp giữa trình biên dịch F* và JavaScript,lập trình viên có thể triển khai một cách hiệu quả các đoạn mã của mình so với việc chỉ sử dụng đơn thuầnJavaScript[13] do F* có khả năng tự chứng thực tính đúng đắnvà đảm bảo độ bảo mật thông tin của chương trình được viết
Đến đây, ngoài khả năng tự chứng thực như đã trình bày ở trên, chúng ta bắt gặp ưu điểm hỗ trợ bảo mật thông tin của ngôn ngữ F* Thư viện như jQuery và Prototype đang được sử dụng rất phổ biến, cung cấp các khả năng hỗ trợ cho thiết kế website nhưng chỉ làm bằng cách sử dụng các tính năng của JavaScript Như vậy chỉ cần thay đổi một số thông tin viết bằng JavaScript trên website, kẻ xấu có thể lấy được thông tin của người dùng Tuy nhiên khi sử dụng với F*, một số thông tin khiến kẻ xấu khó có thể khai thác như tính kiểu của biến đầu vào, khả năng tự kiểm tra tính đúng đắn của hàm nếu có sự thay đổi, chứng minh bằng các bổ đề định nghĩa trước cho các hàm thực thi trong chương trình F* đánh giá đoạn mã được xây dựng dựa trên các chương trình mẫu và các thư viện
an toàn [4]
2.2.2.2 Triển khai F* trong các trình duyệt web thông qua JavaScript
Ví dụ sau đây sẽ cho ta thấy làm sao để sử dụng ngôn ngữ F* kết hợp với JavaScript trên nền web Sử dụng F* cho bài toán tính giai thừa trên nền web, người sử dụng có thể nhập số nguyên vào ô văn bản có sẵn và bấm“CalculateFactorial!” để có được kết quả
10 let n = string2num ( elt getValue () ) in
11 match DOM getElementById "output" with
Trang 2012 | None -> ()
13 | Some elt -> elt setInnerText ( concat_l [ "Factorial of " ;
num2string n ; " is " ; num2string ( factorial n )])
Đoạn mã từ dòng 3 đến dòng 6 định nghĩa hàm tính giai thừa với việc khai báo kiểu
dữ liệu cho hàm tại dòng 3 và hàm tính toán từ dòng 4 đến dòng 6 Hàm để thực thi lấy dữ liệu và trả về kết quả trên web từ dòng 7 đến dòng 13 Tại dòng 7, hàm tìm phần tử có id
là number trên trang web, nếu có phần tử đó, thực hiện tiếp việc lấy dữ liệu trong phần tử trên Sau đó tại dòng 11, hàm tiếp tục tìm kiếm phần tử có id là output, nếu có hàm sẽ thực hiện gán kết quả cho phần tử đó là giá trị giai thừa tìm được Chúng ta có đoạn mã nhúng HTML cho đoạn mã trên như sau:
<input id = "number" type = "text"/>
<input type = "submit" value = "Calculate Factorial" onclick =
"Factorial.doit()"/>
<div id = "output"></div>
Ta sẽ có kết quả như sau:
Hình 2.1: Kết quả cho bài toán tính giai thừa
Như vậy thay vì ta viết một đoạn mã JavaScript để tính giai thừa thì ta có thể viết một hàm bằng ngôn ngữ F* và có thể làm được công việc tương tự Lập trình viên có thể
sử dụng tính năng codegen của trình biên dịch F* để triển khai các đoạn mã sau khi đã
viết trên các ngôn ngữ khác nhau như JavaScript, F#, OCaml Tuy nhiên tại thời điểm
hiện tại, trình biên dịch mới chỉ giới hạn tính năng codegen từ F* sang các đoạn mã
OCaml
2.3 Các khái niệm cơ bản khi lập trình với F*
2.3.1 Các định nghĩa kiểu và loại thường dùng trong F*
Sau đây chúng ta sẽ tìm hiểu một số khái niệm lập trình cơ bản, các từ khóa cần biết đến khi lập trình với ngôn ngữ F*:
Bảng 2.1: Khai báo biểu thức, kiểu, loại trong ngôn ngữ F*
Trang 21k ::=
| Type // Kiểu thường
| x:t => k'// Phụ thuộc loại từ giá trị đến loại (Dependent product from value to kinds)
| a:k => k'// Phụ thuộc từ kiểu đến loại (Dependent product from types to kinds)
Kiểu
t ::= 'a
| Tc//Hằng số
| x:t[{phi}] -> t' // Gán biến thông thường với kiểu tùy chỉnh
| 'a:k -> t//Hàm định nghĩa từ loại k sang kiểu t
| x:t{phi} //Khai báo kiểu được tùy biến
| let x = e1 in e2// Ràng buộc trong hàm khác
| let rec f = v// Hàm gọi đệ quy
| match e with br1 brn// Tìm và kết hợp giá trị với biến hoặc biểu thức
| assert phi// khẳng định kiểm tra đúng
| raise e// Báo cáo ngoại lệ
| try e with br1 brn// Xử lý ngoại lệ
Top level
tl ::= val x : t
| open M// Mở thư viện
| tdef// Định nghĩa kiểu
Trang 22Hình 2.2: Các kiểu, loại dữ liệu được sử dụng trong F*[4]
Một đoạn mã F* thường bao gồm các định nghĩa biến, tùy chọn các hàm chính
(main) và các hàm tính toán khác.Giống như các ngôn ngữ khác, ta cũng làm quen với F*
Trang 23Một chương trình F* là tổng hợp của nhiều module Mỗi module được bắt đầu với từ khóa module và tên của module đó được viết hoa chữ cái đầu.Một module được định
nghĩa là hàm chính nếu nó có cấu trúc let _ = e ở cuối mỗi module Trong ngôn ngữ
F*, kiểu được dùng để mô tả các thuộc tính cho các biến hoặc các hàm của chương
trình.Module Prims định nghĩa một số kiểu mặc định như unit, int và string.Kiểu được xác định và có thể được khai báo ngay từ đầu của mỗi module Ví dụ một số cách gán
kiểu cho một biến:
- Định nghĩa kiểu “<:”
- Gán biến: “:” hoặc “=”
module ASCRIBING
let u =( () <: unit ) (*u được định nghĩa là 1 unit rỗng*)
let z =( 0 <: int ) (*z bằng 0 và định nghĩa kiểu int*)
let z' : int = – 1(*z’ bằng 0 và định nghĩa kiểu int*)
let z'' =( 1 - <: int ) (*z’’ bằng 0 và định nghĩa kiểu int*)
let hello : string = "hello" (*value = Hello, type = string*)
2.3.2 Các khái niệm chung về khai báo kiểu trong F*
Các hàm được xây dựng ở chương trình F* khá đặc biệt, khác biệt hoàn toàn đối với các ngôn ngữ khác đó là sử dụng các kiểu được tùy chỉnh, phù hợp trong nhiều trường hợp nhất định mà bài toán đòi hỏi Các định nghĩa kiểu trong F* có dạng
x t phi ( )}trong đó t là một kiểu với mọi x đều thỏa mãn được hàm phi(x) phi(x) có thể
là bất kì biểu thức nào (Ví dụ: x>=0) Trong các ngôn ngữ lập trình hàm như ML, lập trình viên có thể chỉ cần định nghĩa int -> int tuy nhiên ở F*, để thể hiện ảnh hưởng của hàm tới kết quả cần phải định nghĩa kết quả có dạngint -> Tot intgiống như ví dụ sau:
val is_positive :int->Totbool
let is_positive i = i > 0
Hoặc ta có thể định nghĩa 1 hàm với nhiều biến đầu vào như sau:
val min :int->int->Totint
let min i 1 i 2 = if i 1 > i 2then i 2else i 1
Đối với một hàm xác định, giống với các ngôn ngữ lập trình khác, chúng ta cần khai báo các kiểu giá trị cho một hàm, kết quả trả về và F* cũng vậy Sử dụng val để khai báo kiểu dữ liệu của một hàm, let để khai báo cho các hàm thực thi
2.3.3 Lý thuyết về tập hợp:
Trong lý thuyết về tập hợp, F* sẽ cung cấp 1 cú pháp khá thú vị dành cho bộ dữ liệu như ví dụ sau:
Trang 24| MkTupl e 2:_1:'a->_2:'b->tuple2'a'b
Đối với kiểu tuple, lập trình viên có thể lưu trữ dữ liệu dưới dạng cặp đôi ví dụ như (5, 7) với dạng tổng quát là (int * int)hoặc có thể định nghĩa tổng quát cho kiểutuple2làtuple 2 int int Mặt khác, hoàn toàn có thể định nghĩa việc lưu trữ dữ liệu này với cặp ba bằng đoạn mã dưới đây:
typetriple'a'b'c={fst:'a ; snd:'b; third:'c}
let f x = x.fst + x.snd + x.third
F* chấp nhận chương trình này và tạo cho hàmf một loại dữ liệu (tripleintintint->Totint) Chúng được định nghĩa ngắn gọn nhưng hoàn toàn có thể tương đương với đoạn mã dưới đây:
typetriple'a'b'c=
| MkTriple:fst:'a->snd:'b->third:'c->triple'a'b'c
let f x = MkTriple fst x + MkTriple snd x + MkTriple third x
2.3.4 Định nghĩa, sử dụng mảng dữ liệu trong F*
Ta có ví dụ như sau:
type iList : int => S => S =
| INil : ilist 0 'a
| ICons : n : int -> ilist n 'a -> x : 'a -> ilist ( n + ) 'a
Kiểu iList Rất hữu ích và tối ưu bởi hai lý do sau đây Đầu tiên, định nghĩa của ngôn ngữ F* không cho phép viết như sau iList (-1)'a, điều đó là vô lý vì không có
danh sách có độ dài -1 Trong F*, việc nối 2 danh sách (string hoặc int) được thực hiện
Trang 25Chúng ta có thể viết các chức năng để thao tác với các danh sách lập chỉ mục cũng giống như với các danh sách bình thường Nhưng, bây giờ, việc lập trình với kiểu sẽ giúp chúng ta đảm bảo rằng chúng ta luôn theo dõi độ dài thích hợp của danh sách Ví dụ, đây
là một chức năng mà gắn thêm hai danh sách Chú ý kiểu sẽ ghi nhận chiều dài của danh sách cuối là tổng độ dài của 2 danh sách ban đầu
1 module ILIST1
2 type ilist : int => S => S =
3 | INil : ilist 0 'a
4 | ICons : x : 'a -> n : int -> ilist n 'a -> ilist ( n + ) 'a
5 val iappend : n1 : int -> ilist n1 'a -> n2 : int -> ilist n2 'a -> ilist ( n1 +
n2 ) 'a
6 letrec iappend n1 l1 n2 l2 = match l1 with
7 | INil -> l2
8 | ICons x n1_minus_1 l1tl ->
9 ICons x ( n1_minus_1 + n2 )( iappend n1_minus_1 l1tl n2 l2 )
Các thuật toán được sử dụng bởi F* để kiểm tra kiểu như sau:
Khi khởi tạo một hàm, lập trình viên có thể mô tả tường minh các kiểu của biến đầu vào và kiểu kết quả trả về của hàm hoặc lập trình viên có thể không cần khai báo F* giả định rằng tất cả các biến đều có kiểu được mô tả rõ ràng bởi lập trình viênhoặc là được suy ra bởi các thuật toán suy luận nếu kiểu đó không được cung cấp đầy đủ Ví dụ ở trên, F* cố gắng để có thể chứng minh rằng trong hàmiappend đó có kiểu kết quả trả về là ilist (n1+n2) 'a, giả định rằng iappend, n1, l1, n2 và l2 đều có kiểu được mô tả như trong định nghĩa(val) dòng thứ 5, và ilist với việc xây dựng kiểu dữ liệu của nó đều
Trang 26được mô tả rõ ràng.Thu thập những giả định trên, mục tiêu của F* đưa cho Z3 là cần phải chứng tỏ rằng ((n1_minus_1 + n2) + 1)=(n1 + n2) Với công việc trên, Z3 có thể chứng minh dễ dàng
Kế thừa nền tảng suy luận kiểu của ngôn ngữ F#, lập trình viên có thể định nghĩa hoặc không định nghĩa kiểu đầu vào cho các hàm trong F* và hoàn toàn có thể định nghĩa các ràng buộc có thể có cho giá trị trả về của hàm được viết
2.3.5 Kiểu của số tự nhiên (NAT)
Việc điều chỉnh IList để có thể mô tả được danh sách thực tế với độ dài không âm
Đối với điều này, chúng tôi bắt đầu bằng cách xác định kiểu nat,định nghĩa lại các số
nguyên và lớn hơn 0 (Kiểu int và lớn hơn 0)
module NAT
type nat = : int { <= i }
Trong định nghĩa trên thì nat là kiểu có giá trị int và lớn hơn 0 Khi thực thi, F* gọi đến bộ kiểm tra kiểu (typechecker) (sử dụng Z3) thì có thể chứng minh được với giá trị n thuộc kiểunat cũng lớn hơn hoặc bằng 0
Ngoài ra, F* cho phép chúng ta định nghĩa nhiều kiểu cùng 1 lúc theo cú pháp như
trên thay vì phải định nghĩa từng kiểu (line by line) Không chỉ tạo ra kiểu của số tự nhiên,
chúng ta có thể tùy biến tạo ra các kiểu mới như kiểu chỉ gồm các số chẵn hoặc chỉ gồm các số lẻ như ví dụ dưới đây:
1 module EvenOddNumber
2 type evenNumber = i : int { i % = }
3 type oddNumber = i : int { i % = }
4 val testTypeEvenNumber : int -> Tot evenNumber
10 let test = assert (( testTypeEvenNumber 3 )= 4
11 let test1 = assert (( testTypeOddNumber 3 )= 3
Với một sự đa dạng về kiểu như vậy, lập trình viên có thể tự tùy biến và tạo ra những kiểu phù hợp trong các bài toán lập trình của mình mà vẫn có thể đảm bảo các giá trị trả về đúng với yêu cầu được đưa ra
Trang 27type nat = i : int { <= i }
let z : nat = 0
let bad : nat =- 1(* Sai do “nat” là kiểu số tự nhiên và lớn hơn 0 *)
let int2nat x : nat = if x < then0else x (*Nếu x nhỏ hơn 0 thì = 0*)
let abs x : nat = if x < then - x else x (*Trị tuyệt đối, kết quả luôn lớn hơn hoặc bằng 0*)
let sqr x : nat = x * x (* Mũ 2 một số tự nhiên cho kết quả luôn lớn hơn hoặc bằng 0*)
Nhờ việc định nghĩa thêm kiểu mà chúng ta có thể xây dựng nên những kiểu dữ liệu
mới tinh vi hơn so với kiểu dữ liệuint Nếu không khai báo kiểu cho hàm ví dụ dưới đây,
mặc nhiên F* sẽ tự suy luận ra kiểu của hàm đó có dạng int -> int:
letrec factorial n = if n = 0then1else n * factorial ( n - )
Theo như cách khai báo trên, cáchlập trình với ngôn ngữ F* sẽ hoàn toàn giống với ngôn ngữ F# Tuy nhiên, điểm thú vị là chúng ta còn có thể khai báo một kiểu dữ liệu mới tinh vi hơn cho các biến đầu vào và trình biên dịch có thể kiểm tra tính đúng đắn khi sử dụng kiểu dữ liệu trên như đoạn mã dưới đây:
val factorial : x : int { >= 0 }-> Tot int
Định nghĩa trên xác định rằng factorial là một hàm trả về giá trị có kiểu số nguyên không âm (int*) và đối số n là kiểu số tự nhiên với xluôn lớn hơn hoặc bằng 0.Ngoài ta khi ta định nghĩa type nat = i : int { i >= 0 ta có thể định nghĩa lại hàm factorialvới cách khác như sau:
module Factorial
type nat = x : int { >= 0
val factorial : x : nat -> Tot int
letrec factorial n = if n = 0then1else n * factorial ( n - )
Các kiểu định nghĩa khác cho hàm factorial có thể có như sau:
val factorial : nat -> Tot int
val factorial : nat -> Tot nat
val factorial : nat -> Tot ( : int { x > })
val factorial : int -> int
val factorial : int -> x : int { x > }
Trở lại ví dụ với hàm fibonacci ta có
letrec fibonacci n =
if n <= 1then1else fibonacci ( n - )+ fibonacci ( n - )
Các định nghĩa có thể có cho hàmfibonacci như sau:
val fibonacci : nat -> Tot nat
val fibonacci : int -> int
module Fibonacci
Trang 28val fibonacci : int -> Tot ( x nat { > })
letrec fibonacci n =
if n <= 1then1else fibonacci ( n - )+ fibonacci ( n - )
Để chứng minh hàm trên là đúng đắn ta định nghĩa bằng cách sử dụng loại suy luận
“Tot” trong đó dữ liệu trả về của hàm có kiểu nat tuy nhiên giá trị trả về không được phép bằng 0 Chúng ta có thể xem qua một số ví dụ khác như hàm countTo100 dưới đây, với số
tự nhiên n có kiểu int và nằm trong phạm vi từ [0-100], sau các lần gọi đệ quy, hàm sẽ
cho kết quả trả về là 100 và điều đó đúng với việc đã khai báo các giá trị kết quả có thể có trong hàm này Hàm countTo100luôn đúng với n >=0 và n < 100
let test1 = assert (( countTo100 20 )= 100 )
Sử dụng hàmassertđểkiểm soát được các tính chất của chương trình và F* có thể
kiểm tra các tính chất trong quá trình kiểm tra kiểu(typechecking) là đúng đắn Khác với
các ngôn ngữ khác thì hàmassert trong F* không kiểm tra tại thời gian chạy mà luôn kiểm tra trong quá trình biên dịch và nếu sau khi chạy qua hàmassertvà trả lại giá trị failsau quá trình kiểm tra kiểu (typechecker) thì F* sẽ tạm dừng quá trình biên dịch Lưu
ý rằng các đối số để xác định tính đúng đắn luôn phải là hằng số Ví dụ ở đây ta có chứng minh hàm CountTo100 luôn trả về giá trị bằng 100 Chú ý rằng để có thể sử dụng được hàm assert, chúng ta cần xác định rằng hàm đó có thể kết thúc (Ở đây kết quả trả về luôn giảm dần do n tăng dần)
Kết luận lại ta có thể sử dụng các kiểu tùy chỉnh tinh tế hơn khi làm việc với kiểu,
như ví dụ trên: nat là một kiểu dữ liệu tinh tế hơn của int, bắt buộc trình biên dịch cần
kiểm soát các dữ liệu được gán bởi kiểu đó có thực sự đúng hay không
2.3.7 Loại suy luận và các ảnh hưởng tới tính toán
Trong các phần trên ta có nói đến định nghĩa “Tot” Vậy tại sao cần phải định nghĩa
“Tot”, có thực sự cần thiết hay không Ngoài việc suy diễn tính chất của kiểu, F* cũng suy
Trang 29vòng lặp vô hạn nào, bất cứ ngoại lệ nào xảy ra thì kết quả trả về vẫn có kiểu bool Ta có Tot int được hiểunhư là một kiểu dữ liệu với “Tot” là ảnh hưởng có thể có và int là kiểu kết quả trả về Ngoài “Tot”, chúng ta còn có thêm một số kiểu ảnh hưởng khác như sau:
- ML: Giống với “Tot” , cùng là kiểu và bao hàm cả các ảnh hưởng trong biểu thức
như ném ra ngoại lệ, vòng lặp, biến đổi dữ liệu trong biểu thức nhưng luôn trả về kết quả như đã định nghĩa Và ML được biết đến là ảnh hưởng mặc định cho tất cả các chương trình ML
- Dv: Ảnh hưởng của tính toán mà trong đó có sự phân tách dữ liệu
- ST: Ảnh hưởng của tính toán mà trong đó dữ liệu được đọc, ghi hoặc phân bổ tài nguyên mới trong cấu trúc của hàng đợi
- Exn: Ảnh hưởng tính toán mà trong đó có sự phân tách hoặc có ném ra các ngoại lệ 2.3.8 Sử dụng F* lập trình với các bài toán đơn giản
Trên nền tảng F*, xây dựng các bài toán có nhiệm vụ lập ra mảng dữ liệu một chiều
có độ dài xác định và có các chức năng như sau reverse : list 'a -> list 'a, trong đó reverse là hàm để đảo chiều một mảng danh sách Bài toán 2 tasử dụng hàm mapđể biến tất cả các số tự nhiên trong danh sách a thành danh sách b mớimap :( 'a -> 'b )-> list 'a -
> list 'b
Đối với trình biên dịch F# ta có thể viết ví dụ trên như sau:
let reverseList list = List fold ( fun acc elem -> elem :: acc ) []list
printfn "%A" ( reverseList list1 )
Hàm đảo ngược vị trí phần tử trong mảng, trên nền tảng F* ta có thể được viết như đoạn mã sau:
module Reverse
maival append : list 'a -> list 'a -> Tot ( list 'a )
letrec append l1 l2 = match l1 with
| [] -> l2
| hd::tl -> hd::append tl l2
val reverse : list 'a -> Tot ( list 'a )
letrec reverse = function
| [] -> []
| hd :: tl -> append ( reverse tl )[ hd ]
Ta có thể hiểu bài toán trên được diễn đạt như sau:
- Định nghĩa hàm có chức năng ghép hai mảng với nhau như ví dụ sau:
{0, 1, 2, 3} và {4} thành {0, 1, 2, 3, 4}
Trang 30- Định nghĩa hàm có chức năng xoay chiều dữ liệu trong một mảng với đầu vào
assert (map ( fun x -> x + 1) [0;1;2] = [1;2;3]);
Một số ví dụ khác cho bài toán sử dụng F* để lập trình, cần xử lý nhân hai số tự nhiên cho trước áp dụng trên nền web Đối với bài toán này, ta có thể viết một đoạn mã đơn giản như ví dụ dưới đây:
let n1 = string2num ( elt getValue () ) in
match DOM getElementById "number2" with
| None -> ()
| Some elt2 ->
let n2 = string2num ( elt2 getValue () ) in
match DOM getElementById "output" with
| None -> ()
| Some elt -> elt setInnerText ( concat_l [ "Product of " ; num2string n1 ; " and
" num2string n2 ; " is " ; num2string ( product n1 n2 )])
Mã HTML được sử dụng làm công cụ truyền các tham số đầu vào như sau:
<input id = "number1" type = "text"/>
<input id = "number2" type = "text"/>
<input type = "submit" value = "Calculate Product!" onclick = "Product.doit()"/>
<div id = "output"></div>
Ta có thể hiểu đoạn mã trên như sau:
Trang 31- Hàm thực thi để có thể nhận các giá trị đầu vào, chuyển đổi từ chuỗi kí tự (string) sang kiểu số (number) với số 1 và số 2
- Thực hiện tính toán và trả về các giá trị dưới dạng kí tự (string), hiển thị thông báo
cho người dùng
2.3.9 Chứng minh bổ đề (Lemmas)
Trở lại hàm giai thừa với kiểu là (nat -> Tot nat) Vậy các thuộc tính của giai thừa
có thể có là sẽ như thế nào: Ví dụ nếu x > 2 thì giai thừa của x luôn lớn hơn x Có một lựa chọn cho lập trình viên để có thể rà soát lại hàm này bằng đoạn mã như sau:
module Factorial
val factorial : x : int { >= 0 }-> Tot int
letrec factorial n = if n = 0then1else n * factorial ( n - )
val factorial_is_positive : x : nat -> Tot ( : unit { factorial x > })
letrec factorial_is_positive x =
match x with
| -> ()
| -> factorial_is_positive ( x - )
Như vậy với hàm factorial_is_positive: tham số truyền vào là một số x có kiểu
nat, hàm trả về unit()trong đó với giá trị của x thì giai thừa của x luôn lớn hơn 0 Chúng ta
có thể nhận thấy điều đó bằng việc chứng minh bằng quy nạp trên x Từ đó ta có thể xây dựng nên bổ đề đầu tiên của giai thừa, chứng minh nó luôn trả về một số dương
2.3.9.1Hàm có kiểu phụ thuộc (Dependent function types)
Với các ví dụ như trên, vậy hàm có kiểu phụ thuộc là gì ?Với biểu thức sau:
val factorial_is_positive : x : nat -> Tot ( : unit { factorial x > })
Trên đây là một ví dụ về hàm có kiểu phụ thuộc trong ngôn ngữ F* Tại sao lại phụ thuộc? Ở đây kiểu dữ liệu trả về của hàm trên có dạng là u unit { factorial x > } Kiểudữ liệu trên có thể được hiểu là với giá trị trả về của hàm factorial_is_positive, khi thay giá trị đó vào hàm factorial thì luôn cho ra kết quả lớn hơn 0.Dưới đây là một
ví dụ minh chứng rõ hơn cho việc hàm phụ thuộc trong F*:
module Ex3cFibonacci
val fibonacci : nat -> Tot nat
letrec fibonacci n =
if n <= 1then1else fibonacci ( n - )+ fibonacci ( n - )
val fibonacci_monotone : : nat { n >= 2 }-> Lemma ( fibonacci n >= n )
letrec fibonacci_monotone n =
match n with
Trang 32| | -> ()
| -> fibonacci_monotone ( - )
Với đoạn mã trên, ta có thể thấy rằng với một mệnh đề đầu vào là một biến n lớn hơn hoặc bằng 2 thì luôn luôn có kết quả của Fibonaccin luôn lớn hơn hoặc bằng n
Thật vậy, với n >=2 thay vào hàm fibonacci_monotone với biến đầu vào là 2 hoặc
3 ta luôn trả về unit() nên hàm đó luôn đúng Mặt khác với n =4, Fibonacci_monotone 4 gọi đến Fibonacci_monotone 3 và trả về lại unit() Tiếp theo F* sẽ chứng minh rằng với
biến đầu vào như vậy ta luôn có kết quả là fibonaccicủa n >= n
Khi thay định nghĩa từ dấu “>=” thành “>” trongval fibonacci_monotone :
n nat { n >= 2 }-> Lemma ( fibonacci n > n )ta sẽ có thông báo lỗi như sau:
An unknown assertion in the term at this location was not provable
2.3.9.2 Một số cú pháp kiểu và bổ đề (lemmas)
Khi định nghĩa hàm trong F*, chúng ta có thể bắt chương trình kiểm tra các kiểu đầu vào hoặc đầu ra của hàm như ví dụ khi lập trình căn bản với F* như sau:
val factorial_is_monotone1 : x :( y nat { y > 2 })-> Tot ( : unit { factorial x > x })
Ở ví dụ trên ta có x là biến, x có kiểu tương đương với y trong đó y được dùng để
hạn chế các miền số có thể có: y là kiểu nat và y luôn lớn hơn 2 Tiếp theo, cần trả về
unit() mà trong đó với mọi biến x ta luôn có factorial của x luôn lớn hơn x Tuy nhiên chúng ta có thể giản lược hơn cú pháp của F* và viết lại là:
val factorial_is_monotone2 : x : nat { x > }-> Tot ( : unit { factorial x > x })
Trong ví dụ trên ta giản lược rằng x kế thừa kiểu nat và x luôn lớn hơn 2 Kết quả
trả về được định nghĩa không có gì thay đổi so với ví dụ 1 Ví dụ sau đây cũng tương dương tuy nhiên có một chút dễ dàng hơn để đọc và viết:
val factorial_is_monotone3 : x : nat { x > }-> Lemma ( factorial x > x )
Hoặc chúng ta biến đổi nó cho phù hợp hơn với cách lập trình của chúng ta như sau:
val factorial_is_monotone4 : x : nat -> Lemma ( requires ( x > ))( ensures
( factorial x > x ))
Tương tự với ví dụ như ở mục 2.8.1, ở đây biến đầu vào có thể chỉ cần có kiểu nat
tuy nhiên khi chứng minh bổ đề, ta có thể yêu cầu x phải lớn hơn 2 và luôn đảm bảo rằng