2010-11-12 29 views
6

J'essaie d'apprendre un peu de l'état d'esprit de la programmation fonctionnelle en F #, donc tous les conseils sont appréciés. En ce moment je fais une fonction récursive simple qui prend une liste et retourne l'élément i: th. La fonction elle-même semble fonctionner, mais elle m'avertit d'un motif incomplet. Je ne sais pas quoi revenir quand je correspondre à la liste vide dans ce cas, car si je par exemple procédez comme suit:F #, jusqu'où est-il raisonnable d'aller en vérifiant les arguments valides?

| ([], _) ->() 

La fonction entière est traitée comme une fonction qui prend une unité comme argument. Je veux qu'il traite comme une fonction polymorphe.

Pendant que j'y suis, je peux aussi bien me demander dans quelle mesure il est raisonnable d'aller vérifier les arguments valides lors de la conception d'une fonction lors d'un développement sérieux. Dois-je vérifier pour tout, donc "abuser" de la fonction est empêché? Dans l'exemple ci-dessus, je pourrais par exemple spécifier la fonction pour essayer d'accéder à un élément de la liste qui est plus grand que sa taille. J'espère que ma question n'est pas trop confuse :)

Répondre

2

Vraisemblablement, si prendre une liste vide n'est pas valide, il vaut mieux juste lancer une exception? En règle générale, les règles sur la façon dont vous devriez être défensif ne changent pas vraiment de langue en langue - je vais toujours par la ligne directrice que si le public est paranoïaque sur la validation des entrées, mais si c'est du code privé, vous pouvez être moins strict . (En fait, si c'est un grand projet, et un code privé, soyez un peu strict ... la rigueur est proportionnelle au nombre de développeurs qui pourraient appeler votre code.)

+0

"La rigueur est proportionnelle au nombre de développeurs qui pourraient appeler votre code" Résumé génial. – TechNeilogy

+0

Cela peut sembler un peu banal, mais vous ne pouvez jamais vous tromper avec un code "solide comme le roc". Après tout, ce n'est pas souvent que vous pouvez garantir la façon dont votre code sera utilisé, ou par qui, dans le futur. – Daniel

+0

@Daniel, Vrai, mais il y a un coût associé à cette décision, tant pour le temps de développement que pour la maintenance. Si chaque méthode devait être aussi paranoïaque, vous obtiendriez une base de code pleine de rebondissements, ainsi que des développeurs non inspirés. –

5

Si vous voulez que votre fonction renvoie un résultat significatif et pour avoir le même type que maintenant, alors vous n'avez pas d'autre choix que de lancer une exception dans le cas restant. Un échec de mise en correspondance lancer une exception, donc vous ne besoin pour le changer, mais vous pouvez le trouver préférable de lancer une exception avec des informations plus pertinentes:

| _ -> failwith "Invalid list index" 

Si vous prévoyez des indices liste non valides pour être rare, alors c'est probablement assez bon. Cependant, une autre alternative serait de changer votre fonction pour qu'elle retourne un 'a option:

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

Cela place un fardeau supplémentaire sur l'appelant, qui doit maintenant traiter explicitement la possibilité d'un échec.

9

Vous pouvez en apprendre beaucoup sur la conception de bibliothèque «habituelle» en regardant les bibliothèques F # standard. Il existe déjà une fonction qui fait ce que vous voulez appelé List.nth, mais même si vous implémentez cela comme un exercice, vous pouvez vérifier la fonction se comporte:

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

La fonction System.ArgumentException lancers francs avec des informations supplémentaires sur la exception, de sorte que les utilisateurs peuvent facilement trouver ce qui s'est mal passé. Pour mettre en œuvre les mêmes fonctionnalités, vous pouvez utiliser la fonction invalidArg:

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

Ceci est probablement mieux que d'utiliser simplement failwith qui jette une exception plus générale. Lorsque vous utilisez invalidArg, les utilisateurs peuvent rechercher un type spécifique d'exceptions.Comme kvb l'a noté, une autre option est de retourner option 'a

De nombreuses fonctions de bibliothèque standard fournissent à la fois une version qui renvoie option et une version qui lève une exception. Par exemple List.pick et List.tryPick. Donc, peut-être un bon design dans votre cas serait d'avoir deux fonctions - nth et tryNth.

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

Cette fonction aura en effet la (indésirable) la signature que vous avez mentionné:

val nth : unit list * int -> unit 

Pourquoi? Regardez le côté droit de ces trois règles. Si ce n'était pas le (), vous ne pourriez pas dire quel type de valeur concrète votre fonction renvoie. Mais dès que vous ajoutez la dernière règle, F # voit l'expression () (qui a le type unit) et de cela peut déduire le type de retour de votre fonction; qui n'est plus générique maintenant. Comme toute fonction ne peut avoir qu'un type de retour fixe, elle en déduit que x, xs impliquent également le type unit, ce qui donne la signature ci-dessus. Comme déjà noté par kvb, vous voulez parfois retourner une valeur, et dans la distribution d'une liste d'entrée vide, vous ne voulez rien renvoyer ... ce qui signifie que votre valeur de retour doit être 'a option (peut également être . écrit 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 

maintenant la signature rapporté semble correct:

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

concernant votre se Question de cond, j'avoue que je ne peux pas y répondre complètement parce que je suis toujours un débutant F # moi-même. Un conseil, cependant: Si vous prenez la fonction ci-dessus dans sa forme correcte (la version générique renvoyant un 'a option), vous ne pouvez pas vous empêcher de vérifier toutes les valeurs possibles:

Pourquoi? Parce que si vous voulez arriver à la valeur de rendement réel (du nom x dans le code juste montré), vous devez « extraire » à l'aide d'un bloc match:

let result = nth (someList, someIndex) 

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

Et puisque vos règles devrait toujours être exhaustive (de peur le compilateur se plaindre), vous devrez automatiquement ajouter une règle qui vérifie la possibilité de None.

Le compilateur va effectivement forcer à considérer également ce qui devrait arriver dans une situation "d'erreur"; vous n'êtes pas autorisé à l'oublier. Vous avez seulement le choix de comment faire face à cela!

(Bien sûr, dès que vous faire de la programmation .NET et doivent faire face à des types qui peuvent être null, les choses peut sembler un peu différent, puisque null n'est pas un concept natif de F #.)


Autre suggestion d'amélioration: Si vous changez la façon dont votre fonction nth accepte ses arguments, vous avez la possibilité de l'appliquer partiellement, ce qui signifie par exempleque vous pouvez l'utiliser avec l'opérateur pipelining:

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

Maintenant, vous pouvez faire ceci:

someList 
|> nth someIndex 

Ou ceci:

let third = nth 2 

someList |> third 

Si, d'autre part, votre fonction ne accepte un tuple, cela ne fonctionnera pas. Donc, considérez si vous avez vraiment besoin d'un paramètre de tuple: Dans ce cas, il restreint réellement la flexibilité, et plus encore, le "sens"/contenu des deux paramètres ne suggère pas qu'ils doivent toujours être conservés et apparaître ensemble. Je déconseille donc d'utiliser un tuple dans ce cas.