CHƯƠNG 1 : TỔNG QUAN VỀ LẬP TRÌNH HÀM
2.3. Các khái niệm cơ bản khi lập trình với F*
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{x>=0}-> Tot int
letrec factorial n =if n =0then1else n * factorial (n -1)
val factorial_is_positive: x:nat -> Tot (u:unit{factorial x >0})
letrec factorial_is_positive x =
match x with
|0->()
|_-> factorial_is_positive (x -1)
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 (u:unit{factorial x >0})
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 >0}. 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 -1)+ fibonacci (n -2)
val fibonacci_monotone :n:nat{n >=2}-> Lemma (fibonacci n >= n)
letrec fibonacci_monotone n =
|2|3->()
|_-> fibonacci_monotone (n-1)
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 (u: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 >2}-> Tot (u: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 >2}-> 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 >2))(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
factorial của x luôn lớn hơn x. Công thức được định nghĩa sau là điều kiện tiên quyết mà chức năng bổ đề cần phải có.Lưu ý rằng với 4 cách viết trên đều tương đương với nhau và đều được F* hiểu khi biên dịch và chứng minh các hàm nằm trong nó.
2.3.9.3. Hàm chấp nhận tính chất đúng trong F* (Admit)
Khi lập trình với F*, chúng ta có thể sử dụng hàm bổ đề để chứng minh tính chất của một hàm được viết ra, mặt khác đối với các trường hợp mà chúng ta cho rằng chắc chắn là đúng mà không cần trình biên dịch chứng minh, chúng ta có thể sừ dụng hàm
admit. Hàm được sử dụng như ví dụ sau: letrec fibonacci_monotone n =
match n with
|2|3->()
|4->admit()
|_-> fibonacci_monotone (n-1)
Chúng ta có thể hiểu như sau: với n = 4 hoàn toàn đúng và tương đương với việc n = 2 hoặc bằng 3. Hàm trên luôn được trình biên dịch F*thừa nhận là đúng đắn.
2.3.9.4. Một số kiểu quy hồi đơn giản
Dưới đây là định nghĩa tiêu chuẩn cho kiểu danh sách trong F*:
type list'a = | Nil : list 'a
| Cons : hd:'a -> tl:list 'a -> list 'a
Áp dụng vào hàm tính toán dưới đây, ta có: module AppendSumLengths
val length:list 'a -> Tot nat
letrec length l =match l with
|[]->0
|_::tl ->1+ length tl
val append :list 'a ->list 'a -> Tot (list 'a)
letrec append l1 l2 =match l1 with
|[]-> l2
| hd::tl -> hd::append tl l2
val append_len: l1:list 'a -> l2:list 'a -> Lemma (requires True)
(ensures(length (append l1 l2)= length l1 + length l2))
letrec append_len l1 l2 =
match l1 with
|[]->()
|_::tl -> append_len tl l2
Như vậy với hàm trên ta sẽ có append_len {2,3} {4} Sau mỗi vòng quy hồi ta sẽ có kết quả thu được:
{2,3} {4} luôn đúng với append {2,3} {4} = {2,3,4}. {2} {4}luôn đúng với append {2,3} {4} = {2,3,4}. {} {4}luôn đúng với append {} {4} = {4}.
Ngoài ra, chúng ta còn có thể sử dụng việc gọi quy hồi để nhờ trình biên dịch F* chứng minh tính đúng đắn của hàm theo như ví dụ dưới đây:
module Mem
// Khai báo kiểu dữ liệu cho các biến và dữ liệu trả về của hàm mem val mem: 'a ->list 'a -> Tot bool
// Khai báo hàm kiểm tra phần tử trong danh sách let rec mem a l =
match l with | [] -> false
| hd::tl ->hd=a || mem a tl
// Khai báo kiểu dữ liệu cho các biến và dữ liệu trả về của hàm append val append :list 'a ->list 'a -> Tot (list 'a)
let rec append l1 l2 = match l1 with
| [] -> l2
| hd::tl -> hd::append tl l2
//Khai báo lemma chứng minh hàm append là đúng val append_mem: l1:list 'a -> l2:list 'a -> a:'a
-> Lemma (ensures (mem a (append l1 l2)<==> mem a l1 || mem a l2)) let rec append_mem l1 l2 a =
match l1 with | [] -> ()
|_::tl -> append_mem tl l2 a
Để chứng minh được hàm append là toàn vẹn dữ liệu, chúng ta có thể định nghĩa ra các hàm để chứng minh như sau:
- Hàm mem sử dụng truy hồi trong mảng dữ liệu truyền vào để xác nhận rằng nếu
phần tử a có trong danh sách l thì trả về kết quả đúng (true).
- Sử dụngappend_mem để trình biên dịch xác nhận rằng nếu phần tử a xuất hiện trong danh sách nối l1 và l2 thì tương đương với việc hoặc a xuất hiện trong danh sách l1 hoặc a xuất hiện trong danh sách l2. Như vậy dữ liệu sau khi qua hàm nối 2 danh sách được chứng minh là toàn vẹn.
Lấy một số ví dụ khác về chứng minh quy hồi trong F*:
module Nth
let rec length l = match l with | [] ->0
| _::tl ->1+ length tl
// Khai báo hàm nth gồm 1 danh sách đầu vào và 1 số n <= length của danh sách val nth : l:list 'a -> n:nat{op_LessThan n (length l)}-> Tot 'a
let rec nth l n = match l with
| h::t -> if n =0 then h else nth t (n-1)
Trong đoạn mã trên ta có thể thấy rằng: hàm nthđược định nghĩa với mục đích có một danh sách l và một số n sao cho số n nhỏ hơn độ dài của danh sách n thì hàm trên luôn trả về biến có vị trí thứ n trong danh sách l.