2010-11-12 37 views
6

Tôi đang cố gắng tìm hiểu một chút về tư duy lập trình chức năng trong F #, vì vậy mọi mẹo đều được đánh giá cao. Ngay bây giờ tôi đang tạo một hàm đệ quy đơn giản, lấy danh sách và trả về phần tử i: th.F #, cách hợp lý để đi khi kiểm tra đối số hợp lệ là bao nhiêu?

let rec nth(list, i) = 
    match (list, i) with 
    | (x::xs, 0) -> x 
    | (x::xs, i) -> nth(xs, i-1) 

Bản thân chức năng dường như hoạt động nhưng cảnh báo tôi về mẫu không hoàn chỉnh. Tôi không chắc chắn nên trả lại những gì khi tôi khớp với danh sách trống trong trường hợp này, vì nếu tôi ví dụ như sau:

| ([], _) ->() 

Toàn bộ hàm được coi như một hàm nhận đơn vị làm đối số. Tôi muốn nó được coi là một chức năng đa hình.

Trong khi tôi đang ở đó, tôi cũng có thể hỏi làm thế nào đến nay là hợp lý để đi kiểm tra đối số hợp lệ khi thiết kế một chức năng khi phát triển nghiêm túc. Tôi có nên kiểm tra tất cả mọi thứ, do đó, "lạm dụng" của chức năng được ngăn chặn? Trong ví dụ trên, tôi có thể xác định hàm để cố gắng truy cập một phần tử trong danh sách lớn hơn kích thước của nó. Tôi hy vọng câu hỏi của tôi không quá khó hiểu :)

Trả lời

2

Có lẽ, nếu lấy một danh sách trống không hợp lệ, tốt nhất bạn nên bỏ một ngoại lệ? Nói chung các quy tắc về cách phòng thủ bạn không nên thay đổi từ ngôn ngữ sang ngôn ngữ - tôi luôn đi theo hướng dẫn rằng nếu công chúng hoang tưởng về việc xác thực đầu vào, nhưng nếu đó là mã riêng, bạn có thể ít nghiêm ngặt hơn . (Trên thực tế nếu đó là một dự án lớn, và đó là mã riêng, hơi nghiêm ngặt ... về cơ bản mức độ nghiêm ngặt là tỷ lệ thuận với số lượng nhà phát triển có thể gọi mã của bạn.)

+0

"mức độ nghiêm ngặt là tỷ lệ thuận với số lượng nhà phát triển có thể gọi mã của bạn" Tóm tắt tuyệt vời. – TechNeilogy

+0

Điều này nghe có vẻ hơi phức tạp, nhưng bạn không bao giờ có thể sai với mã "đá rắn". Sau khi tất cả, nó không thường xuyên bạn có thể đảm bảo làm thế nào mã của bạn sẽ được sử dụng, hoặc bởi ai, trong tương lai. – Daniel

+0

@Daniel, Đúng, nhưng có một chi phí liên quan đến quyết định này, trong cả thời gian phát triển và bảo trì. Nếu mọi phương pháp đơn lẻ đều là hoang tưởng này, bạn sẽ kết thúc với một cơ sở mã có đầy đủ bản mẫu, cũng như các nhà phát triển không có tiền. –

5

Nếu bạn muốn hàm trả về kết quả có ý nghĩa và để có cùng loại như hiện tại, bạn không có cách nào khác ngoài việc ném một ngoại lệ trong trường hợp còn lại. Một thất bại phù hợp sẽ ném một ngoại lệ, vì vậy bạn không cần để thay đổi nó, nhưng bạn có thể tìm thấy nó thích hợp hơn để ném một ngoại lệ với những thông tin phù hợp hơn:

| _ -> failwith "Invalid list index" 

Nếu bạn mong đợi chỉ số danh sách không hợp lệ để được hiếm, thì điều này có lẽ là đủ tốt. Tuy nhiên, thay thế khác sẽ được thay đổi chức năng của bạn để nó trả về một 'a option:

let rec nth = function 
| x::xs, 0 -> Some(x) 
| [],_ -> None 
| _::xs, i -> nth(xs, i-1) 

này đặt một gánh nặng thêm trên người gọi, người bây giờ phải đối phó một cách rõ ràng với khả năng thất bại.

9

Bạn có thể tìm hiểu rất nhiều về thiết kế thư viện "thông thường" bằng cách xem các thư viện chuẩn F #. Đã có một chức năng mà những gì bạn muốn gọi List.nth, nhưng ngay cả khi bạn đang thực hiện điều này như một bài tập, bạn có thể kiểm tra các chức năng xử:

> List.nth [ 1 .. 3 ] 10;; 
System.ArgumentException: The index was outside the range 
    of elements in the list. Parameter name: index 

Chức năng ném System.ArgumentException với thêm một số thông tin về ngoại lệ, để người dùng có thể dễ dàng tìm ra điều gì đã xảy ra. Để thực hiện các chức năng tương tự, bạn có thể sử dụng chức năng invalidArg:

| _ -> invalidArg "index" "Index is out of range." 

Đây có lẽ là tốt hơn so với chỉ sử dụng failwith đó ném một ngoại lệ tổng quát hơn. Khi sử dụng invalidArg, người dùng có thể kiểm tra một loại ngoại lệ cụ thể.

Như kvb đã lưu ý, một tùy chọn khác là trả lại option 'a. Nhiều chức năng thư viện chuẩn cung cấp cả phiên bản trả về option và phiên bản ném ngoại lệ. Ví dụ: List.pickList.tryPick. Vì vậy, có thể thiết kế tốt trong trường hợp của bạn sẽ có hai chức năng - nthtryNth.

0
let rec nth (list, i) = 
    match list, i with 
    | x::xs, 0 -> x 
    | x::xs, i -> nth(xs, i-1) 
    | [], _ -> () 

Chức năng này thực sự sẽ có (không mong muốn) chữ ký bạn đề cập:

val nth : unit list * int -> unit 

Tại sao? Nhìn vào bên phải của ba quy tắc của bạn. Nếu không phải là (), bạn không thể nói loại giá trị cụ thể nào mà hàm của bạn trả về. Nhưng ngay sau khi bạn thêm quy tắc cuối cùng, F # sẽ thấy biểu thức () (có loại unit) và từ đó có thể suy ra loại trả về của hàm của bạn; mà bây giờ không còn chung chung nữa. Vì bất kỳ hàm nào chỉ có thể có một kiểu trả về cố định, nó sẽ suy ra rằng x, xs cũng liên quan đến loại unit, dẫn đến chữ ký ở trên.

Như đã lưu ý bởi kvb, bạn muốn đôi khi trả lại giá trị và trong danh sách đầu vào trống, bạn không muốn trả về bất kỳ giá trị nào ... nghĩa là giá trị trả về của bạn phải là 'a option (cũng có thể là . viết như option<'a> btw)

let rec nth (list, i) = 
    match list, i with 
    | x::xs, 0 -> Some(x) 
    | x::xs, i -> nth(xs, i-1) // <-- nth already returns an 'a option, 
    | [], _ -> None   //  no need to "wrap" it once more 

Bây giờ chữ ký báo cáo có vẻ đúng:

val nth : 'a list * int -> 'a option 

về bạn se câu hỏi cond, tôi thừa nhận rằng tôi không thể trả lời đầy đủ vì tôi vẫn là một F # newbie bản thân mình. Một gợi ý, mặc dù: Nếu bạn lấy hàm trên ở dạng chính xác của nó (phiên bản chung trả về số 'a option), bạn thực sự không thể không kiểm tra tất cả các giá trị trả về:

Tại sao? Bởi vì nếu bạn muốn để có được giá trị trả về thực tế (tên x trong mã chỉ được hiển thị), bạn cần phải "giải nén" nó sử dụng một khối match:

let result = nth (someList, someIndex) 

match result with 
| Some(x) -> ... 
| None -> ... 

Và kể từ khi quy tắc của bạn nên luôn luôn đầy đủ (kẻo trình biên dịch phàn nàn), bạn sẽ tự động phải thêm quy tắc để kiểm tra khả năng của None.

Trình biên dịch sẽ thực sự buộc bạn cũng nên cân nhắc điều gì sẽ xảy ra trong trường hợp "lỗi"; bạn không được phép quên nó. Bạn chỉ có thể chọn cách đối phó với nó!

(Tất nhiên, ngay sau khi bạn làm lập trình .NET và phải đối phó với các loại có thể được null, mọi thứ có thể trông hơi khác một chút, vì null không phải là một khái niệm có nguồn gốc của F #.)


Đề xuất cải tiến thêm: Nếu bạn thay đổi cách chức năng nth của bạn chấp nhận đối số của nó, bạn có khả năng áp dụng một phần, nghĩa làmà bạn có thể sử dụng nó với các nhà điều hành pipelining:

let rec nth i list = // <-- swap order of arguments, don't pass them in 
    ...     //  as a tuple but as two separate arguments 

Bây giờ bạn có thể làm điều này:

someList 
|> nth someIndex 

Hoặc này:

let third = nth 2 

someList |> third 

Nếu, mặt khác, chỉ có chức năng của bạn chấp nhận một tuple, điều đó sẽ không hoạt động. Vì vậy, hãy xem xét nếu bạn thực sự cần tham số tuple: Trong trường hợp này, nó thực sự hạn chế tính linh hoạt và nhiều hơn nữa, "ý nghĩa"/nội dung của hai tham số không cho thấy chúng luôn được giữ và xuất hiện cùng nhau. Do đó, tôi khuyên bạn không nên sử dụng một bộ tuple trong trường hợp này.

Các vấn đề liên quan