2010-11-12 13 views
6

Próbuję nauczyć się trochę mentalności programowania funkcjonalnego w F #, więc wszelkie wskazówki są mile widziane. W tej chwili tworzę prostą funkcję rekursywną, która pobiera listę i zwraca element i: th.F #, jak daleko można się posunąć przy sprawdzaniu poprawnych argumentów?

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

Sama funkcja wydaje się działać, ale ostrzega mnie o niekompletnym wzorze. Nie jestem pewien, co do powrotu, kiedy pasuje do pustej listy w tej sprawie, ponieważ jeśli na przykład wykonaj następujące czynności:

| ([], _) ->() 

Cała funkcja jest traktowany jak funkcja, która bierze jednostkę jako argument. Chcę, aby traktowano ją jako funkcję polimorficzną.

Będąc przy tym, mogę równie dobrze zapytać, jak daleko można się posunąć, aby sprawdzić prawidłowe argumenty podczas projektowania funkcji podczas poważnego rozwoju. Czy powinienem sprawdzić wszystko, aby zapobiec niewłaściwemu użyciu funkcji? W powyższym przykładzie mógłbym na przykład określić funkcję, aby spróbować uzyskać dostęp do elementu na liście, który jest większy niż jego rozmiar. Mam nadzieję, że moje pytanie nie jest zbyt mylące :)

Odpowiedz

2

Przypuszczam, że jeśli zrobienie pustej listy jest nieważne, najlepiej jest rzucić wyjątek?

Generalnie zasady określające, jak defensywne powinieneś być, nie zmieniają się z języka na język - zawsze podążam za wytyczną, że jeśli publicznie paranoję na temat sprawdzania danych wejściowych, ale jeśli jest to prywatny kod, możesz być mniej surowy . (W rzeczywistości, jeśli jest to duży projekt i jest to prywatny kod, bądź trochę surowy ... w zasadzie ścisłość jest proporcjonalna do liczby programistów, którzy mogą wywoływać twój kod.)

+0

"Ścisłość jest proporcjonalna do liczby programistów, którzy mogą nazwać twój kod" Niesamowite podsumowanie. – TechNeilogy

+0

To może brzmieć odrobinę banalnie, ale nigdy nie można się pomylić z "skalnym" kodem. W końcu nie zawsze można zagwarantować, w jaki sposób Twój kod będzie używany lub przez kogo w przyszłości. – Daniel

+0

@Daniel, To prawda, ale wiąże się to z kosztami związanymi z tą decyzją, zarówno w czasie projektowania, jak i konserwacji. Gdyby każda metoda miała być tą paranoją, skończyłaby się podstawą kodu, która jest pełna standardowego zestawu, a także niezbyt inspirującymi programistami. –

5

Jeśli chcesz, aby funkcja zwracała znaczący wynik i mieć ten sam typ, jaki ma teraz, wtedy nie masz innego wyjścia, jak wyrzucić wyjątek w pozostałej sprawie. Awaria dopasowanie rzuci wyjątek, więc nie potrzeba go zmienić, ale może okazać się korzystne wyjątek z bardziej istotnych informacji:

| _ -> failwith "Invalid list index" 

Jeśli oczekujesz nieprawidłowe wskaźniki wykazie, który zostanie rzadkie, to prawdopodobnie jest wystarczająco dobre. Jednak inną alternatywą byłoby zmienić swoją funkcję tak, że zwraca 'a option:

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

Stawia to dodatkowe obciążenie na rozmówcy, który musi teraz wyraźnie do czynienia z możliwością porażki.

9

Możesz dowiedzieć się wiele o "zwykłym" projekcie biblioteki, patrząc na standardowe biblioteki F #. Jest to funkcja, która już robi to, co chcesz nazywa List.nth, ale nawet jeśli wdrożenie tego jako ćwiczenie, można sprawdzić, jak zachowuje się funkcja:

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

Funkcja rzuca System.ArgumentException z dodatkowymi informacjami o wyjątek, aby użytkownicy mogli łatwo dowiedzieć się, co poszło nie tak. Aby wdrożyć tę samą funkcjonalność, można użyć funkcji: invalidArg

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

jest to prawdopodobnie lepiej niż tylko za pomocą failwith który generuje bardziej ogólny wyjątek. Podczas korzystania z invalidArg użytkownicy mogą sprawdzić określony typ wyjątków.

Jak zauważyłem w Kvb, inną opcją jest zwrócenie option 'a. Wiele standardowych funkcji bibliotecznych udostępnia zarówno wersję, która zwraca option, jak i wersję, która zgłasza wyjątek. Na przykład List.pick i List.tryPick. Być może dobrym pomysłem w twoim przypadku będzie posiadanie dwóch funkcji - nth i tryNth.

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

Funkcja ta rzeczywiście ma (niepożądanego) podpis Państwo wymienić:

val nth : unit list * int -> unit 

Dlaczego? Spójrz na prawą stronę tych trzech reguł. Jeśli nie było to dla (), nie można powiedzieć, jaki konkretny rodzaj wartości zwraca twoja funkcja. Ale jak tylko dodasz ostatnią regułę, F # widzi wyrażenie () (które ma typ unit) i od tego może wywnioskować typ zwrotu twojej funkcji; który nie jest już ogólny. Ponieważ jakakolwiek funkcja może mieć tylko jeden stały typ zwracany, wówczas zakłada, że ​​x, xs również zawiera typ unit, co powoduje podpis powyżej.

Jak już zauważyłem w kvb, chcesz czasami zwracać wartość, a na obsadzie pustej listy wejściowej nie chcesz zwracać niczego ... co oznacza, że ​​twoja wartość zwracana powinna być 'a option (może być również . zapisać jako 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 

teraz zgłaszane podpis wygląda poprawne:

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

dotyczące danej se pytanie warunkowe, Przyznaję, że nie mogę w pełni odpowiedzieć na to pytanie, ponieważ sam nadal jestem fanką. Jedna wskazówka: jeśli weźmiesz powyższą funkcję w poprawnej formie (ogólna wersja zwróci numer 'a option), nie możesz pomóc, ale sprawdź wszystkie możliwe wartości zwracane:

Dlaczego? Bo jeśli chcesz dostać się do rzeczywistej wartości powrotnej (o nazwie x w kodzie tylko pokazanego), trzeba „wyciąg” go za pomocą match bloku:

let result = nth (someList, someIndex) 

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

I od reguł powinny zawsze być wyczerpująca (bo kompilator narzeka), automatycznie musisz dodać regułę, która sprawdza, czy istnieje możliwość None.

Kompilator będzie w rzeczywistości wymuszać, aby również rozważyć, co powinno się zdarzyć w sytuacji "błędu"; nie możesz tego zapomnieć. Masz tylko wybór, jak sobie z tym poradzić!

(Oczywiście, jak tylko zrobić programowania .NET i mieć do czynienia z typami, które mogą być null, wszystko może wyglądać nieco inaczej, ponieważ null nie jest rodowitym pojęcie F #).


Dalsze sugestia dla poprawy: Jeśli zmienić sposób, że funkcja nth akceptuje swoje argumenty, pojawi się możliwość częściowego zastosowania go, co oznacza, że ​​npże można go używać z operatorem rurociągów różnych:

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

Teraz można to zrobić:

someList 
|> nth someIndex 

albo to:

let third = nth 2 

someList |> third 

Jeśli, z drugiej strony, to jedyna funkcja akceptuje krotkę, która nie zadziała. Zastanówmy się więc, czy naprawdę potrzebny jest parametr krotki: w tym przypadku faktycznie ogranicza on elastyczność, a co więcej, "znaczenie"/treść dwóch parametrów nie sugeruje, że powinny one zawsze być przechowywane i wyświetlane razem. Dlatego odradzam używanie w tym przypadku krotki.

Powiązane problemy