2013-08-27 10 views
11

Ostatnio uczyłem się F # i pochodzę z imperatywu (C++/C#). Jako ćwiczenie pracowałem nad funkcjami, które mogą robić rzeczy z macierzami, takimi jak dodawanie, mnożenie, uzyskiwanie wyznaczników itp. Wszystko idzie dobrze w tym względzie, ale uważam, że być może nie podejmuję najlepszych decyzji, jeśli chodzi o obsługę Nieprawidłowe wejścia, na przykład:F #: Some, None lub Exception?

// I want to multiply two matrices 
let mult m1 m2 = 
    let sizeOK = validateDims m1 m2 

    // Here is where I am running to conceptual trouble: 
    // In a C# world, I would throw an exception. 
    if !sizeOK then 
    raise (InvalidOperationException("bad dimensions!") 
    else 
    doWork m1 m2 

Więc choć to technicznie działa, czy jest to właściwe dla języka funkcjonalnego? Czy jest to w duchu programowania funkcjonalnego? Czy miałoby to więcej sensu przepisywać ją jako:

let mult m1 m2 = 
    let sizeOK = validateDims m1 m2 

    if !sizeOK then 
    None 
    else 
    Some doWork m1 m2 

W tym przypadku Wracam opcję, która dodaje dodatkową warstwę wokół matrycy, ale mogę też korzystać z wyników czynności, nawet w braku przypadki (brak) z dopasowaniem wzorców itp. w pewnym późniejszym punkcie programu. Czy istnieje najlepsza praktyka dla tego typu scenariuszy? Co zrobiłby funkcjonalny programista?

+0

Uważam, że ten ostatni jest bardziej "w duchu" PR. Możesz zajrzeć do monady Maybe, a także do jednego wspólnego podejścia do obsługi złych danych wejściowych. –

+0

Jak powiedział Pad w swojej odpowiedzi, często implementuję dwie funkcje, jedną, która rzuca na błędy i identyczną funkcję-try, która zwraca opcję/wybór zamiast rzucania. następnie używam tego, który sprawia, że ​​mój ostatni kod wygląda najładniej :) dla łatwiejszej obsługi opcji/wyboru, możesz przeczytać "programowanie zorientowane na kolej": http://fsharpforfunandprofit.com/posts/recipe-part2/ – stmax

Odpowiedz

8

staram się unikać wyjątków dla następujących powodów:

  • .NET exceptions are slow
  • wyjątkami zmiany przepływów kontroli programów w nieoczekiwany sposób, który sprawia, że ​​znacznie trudniej rozumować o
  • Wyjątki często pojawiają się w sytuacje krytyczne, podczas gdy możesz awaryjnie korzystać z opcji.

W twoim przypadku, będę przestrzegać konwencji biblioteki F # rdzeniowe (np List.tryFind i List.find itp) i utworzyć obie wersje:

let tryMult m1 m2 = 
    let sizeOK = validateDims m1 m2 

    if not sizeOK then 
    None 
    else 
    Some <| doWork m1 m2 

let mult m1 m2 = 
    let sizeOK = validateDims m1 m2 

    if not sizeOK then 
    raise <| InvalidOperationException("bad dimensions!") 
    else 
    doWork m1 m2 

Ten przykład nie jest wyjątkowy wystarczy użyć wyjątki . Funkcja mult jest dołączona do kompatybilności z C#. Ktoś, kto używa biblioteki w języku C#, nie ma dopasowywania wzorca, aby łatwo rozłożyć opcje.

Jedną wadą z opcjami jest to, że nie podają przyczyny, dla której funkcja nie wytworzyła wartości. To przesada tutaj; ogólnie Choice (albo z monada Haskell terminu) jest bardziej odpowiednie dla error handling:

let tryMult m1 m2 = 
    // Assume that you need to validate input 
    if not (validateInput m1) || not (validateInput m2) then 
    Choice2Of2 <| ArgumentException("bad argument!") 
    elif not <| validateDims m1 m2 then 
    Choice2Of2 <| InvalidOperationException("bad dimensions!") 
    else 
    Choice1Of2 <| doWork m1 m2 

To szkoda że F # rdzeń pozbawiony funkcji wysokiego celu manipulowania Wybór. Te funkcje można znaleźć w bibliotece FSharpX lub ExtCore.

3

staram się iść z następującymi wytycznymi:

Zastosowanie wyjątek w funkcji, która ma zawsze mają wartości zwracanych, gdy coś pójdzie nie tak niespodziewanie. Może to np. być, jeśli argumenty nie są zgodne z umową dla funkcji. Ma to tę zaletę, że kod klienta staje się prostszy.

Użyj opcji, jeśli funkcja czasami ma wartość zwrotną dla prawidłowego wejścia. Może to np. dostać się na mapę, gdzie ważny klucz może nie istnieć. W ten sposób zmuszasz użytkownika do sprawdzenia, czy funkcja ma wartość zwracaną. Może to zredukować błędy, ale zawsze powoduje zawikłanie kodu klienta.

Twoja sprawa jest nieco pomiędzy. Jeśli spodziewasz się, że przede wszystkim będzie używany w miejscach, w których wymiary są prawidłowe, rzuciłbym wyjątek. Jeśli spodziewasz się, że kod klienta często wywoła go z nieprawidłowym wymiarem, zwrócę opcję. Prawdopodobnie będę iść z pierwszych, jak to jest czystsze (patrz poniżej), ale nie wiem, context:

// With exception 
let mult3 a b c = 
    mult (mult a b) c; 

// With option 
let mult3 a b c= 
    let option = mult a b 
    match option with 
    | Some(x) -> mult x b 
    | None -> None 

Zastrzeżenie: nie mam doświadczenia zawodowego w programowaniu funkcjonalnym, ale jestem TA w programowaniu F # na poziomie magisterskim.

2

Podobają mi się powyższe odpowiedzi, ale chciałem dodać kolejną opcję. To naprawdę zależy od tego, jak nieoczekiwany jest wynik i czy ma sens kontynuowanie. Jeśli jest to rzadkie zdarzenie, a osoba dzwoniąca prawdopodobnie nie planowała porażki, wyjątek jest całkowicie respektowany. Kod do wychwycenia wyjątku może być na wielu poziomach powyżej, a dzwoniący prawdopodobnie nie planował porażki. Jeśli to naprawdę rutynowy wynik niepowodzenia operacji, Some/None jest w porządku, ale daje tylko dwie opcje i nie ma możliwości podania wyniku. Inną opcją jest urozmaicenie możliwości. Zmusza to dzwoniącego do dopasowania się do różnych wyników, jest rozszerzalne i nie wymusza na tobie, aby każdy wynik był tego samego typu.

np.

type MultOutcome = 
    | RESULT of Matrix 
    | DIMERROR 
    | FOOERROR of string 


let mult a b = 
    if dimensionsWrong then 
     DIMERROR 
    elif somethingElseIDoNotLike then 
     FOOERROR("specific message") 
    else 
     DIMRESULT(a*b) 


match mult x y with 
    | DIMERROR -> printfn "I guess I screwed up my matricies" 
    | FOOERROR(s) -> printfn "Operation failed with message %s" s 
    | DIMRESULT(r) -> 
     // Proceed with result r