Remarque à l'intention d'autres contributeurs potentiels: n'hésitez pas à utiliser des notations abstraites ou mathématiques pour faire valoir votre point de vue. Si je trouve que votre réponse n'est pas claire, je demanderai des explications, mais sinon, n'hésitez pas à vous exprimer de manière confortable.
Pour être clair: je suis pas à la recherche d'un "sûr" head
, et le choix de head
en particulier n'est pas particulièrement significatif. La chair de la question suit la discussion de head
et head'
, qui servent de contexte.
Je pirate Haskell depuis quelques mois maintenant (au point qu'il est devenu ma langue principale), mais je ne suis certes pas bien informé sur certains des concepts les plus avancés ni sur les détails de la philosophie de la langue (bien que Je suis plus que disposé à apprendre). Ma question n'est donc pas tant technique (à moins que ce ne soit et je ne m'en rends pas compte), mais plutôt philosophique.
Pour cet exemple, je parle de head
.
Comme j'imagine que vous le savez,
Prelude> head []
*** Exception: Prelude.head: empty list
Cela découle de head :: [a] -> a
. C'est suffisant. Évidemment, on ne peut pas retourner un élément de type (en agitant la main) sans type. Mais en même temps, il est simple (sinon trivial) de définir
head' :: [a] -> Maybe a
head' [] = Nothing
head' (x:xs) = Just x
J'ai vu une petite discussion à ce sujet ici dans la section commentaire de certaines déclarations. Notamment, un Alex Stangl dit
"Il y a de bonnes raisons de ne pas tout" sécuriser "et de lever les exceptions en cas de violation des conditions préalables."
Je ne remets pas nécessairement en cause cette affirmation, mais je suis curieux de savoir quelles sont ces "bonnes raisons".
De plus, un Paul Johnson dit:
'Par exemple, vous pouvez définir "safeHead :: [a] -> Peut-être un", mais maintenant au lieu de gérer une liste vide ou de prouver que cela ne peut pas se produire, vous devez gérer "Rien" ou prouver que cela ne peut pas se produire . '
Le ton que j'ai lu à partir de ce commentaire suggère qu'il s'agit d'une augmentation notable de la difficulté/complexité/quelque chose, mais je ne suis pas sûr de comprendre ce qu'il met là-bas.
Un Steven Pruzina dit (en 2011, pas moins),
"Il y a une raison plus profonde pour laquelle par exemple 'head' ne peut pas être à l'épreuve des plantages. Pour être polymorphe tout en gérant une liste vide, 'head' doit toujours renvoyer une variable du type qui est absente d'une liste vide particulière. Ce serait Delphic si Haskell pouvait faire ça ... ".
Le polymorphisme est-il perdu en autorisant la gestion des listes vides? Si oui, comment et pourquoi? Y a-t-il des cas particuliers qui rendraient cela évident? Cette section a répondu amplement à @Russell O'Connor. Toute autre réflexion est, bien sûr, appréciée.
Je vais modifier cela selon la clarté et la suggestion. Toutes les pensées, documents, etc. que vous pouvez fournir seront très appréciés.
Le polymorphisme est-il perdu en autorisant la gestion des listes vides? Si oui, comment et pourquoi? Y a-t-il des cas particuliers qui rendraient cela évident?
Le théorème libre pour head
déclare que
f . head = head . $map f
Appliquer ce théorème à []
implique que
f (head []) = head (map f []) = head []
Ce théorème doit être valable pour chaque f
, donc en particulier il doit être valable pour const True
et const False
. Cela implique
True = const True (head []) = head [] = const False (head []) = False
Ainsi, si head
est correctement polymorphe et head []
était une valeur totale, alors True
serait égal à False
.
PS. J'ai d'autres commentaires sur l'arrière-plan de votre question: si vous avez une condition préalable à ce que votre liste ne soit pas vide, vous devez l'appliquer en utilisant un type de liste non vide dans votre signature de fonction au lieu d'utiliser une liste.
Pourquoi utilise-t-on head :: [a] -> a
au lieu de la correspondance de motifs? L'une des raisons est que vous savez que l'argument ne peut pas être vide et que vous ne voulez pas écrire le code pour gérer le cas où l'argument est vide.
Bien sûr, votre head'
de type [a] -> Maybe a
est défini dans la bibliothèque standard comme Data.Maybe.listToMaybe
. Mais si vous remplacez une utilisation de head
par listToMaybe
, vous devez écrire le code pour gérer le cas vide, ce qui va à l'encontre de cet objectif d'utiliser head
.
Je ne dis pas que l'utilisation de head
est un bon style. Il cache le fait qu'il peut en résulter une exception, et en ce sens ce n'est pas bon. Mais c'est parfois pratique. Le fait est que head
remplit certaines fonctions qui ne peuvent être remplies par listToMaybe
.
La dernière citation de la question (sur le polymorphisme) signifie simplement qu'il est impossible de définir une fonction de type [a] -> a
qui renvoie une valeur sur la liste vide (comme Russell O'Connor l'a expliqué dans sa réponse ).
Il est tout à fait naturel de s'attendre à ce que les éléments suivants tiennent: xs === head xs : tail xs
- une liste est identique à son premier élément, suivie du reste. Cela semble logique, non?
Maintenant, comptons le nombre de conses (applications de :
), Sans tenir compte des éléments réels, lors de l'application de la prétendue "loi" à []
: []
Devrait être identique à foo : bar
, Mais le premier a 0 contre, tandis que le second en a (au moins) un. Euh oh, quelque chose ne va pas ici!
Le système de types de Haskell, pour toutes ses forces, n'est pas en mesure d'exprimer le fait que vous ne devriez appeler head
que sur une liste non vide (et que la `` loi '' n'est valable que pour les listes non vides). L'utilisation de head
transfère la charge de la preuve au programmeur, qui doit s'assurer qu'il n'est pas utilisé sur des listes vides. Je crois que les langages typés de manière dépendante comme Agda peuvent aider ici.
Enfin, une description un peu plus opérationnelle et philosophique: comment mettre en œuvre head ([] :: [a]) :: a
? Conjurer une valeur de type a
à partir de rien est impossible (pensez à des types inhabités tels que data Falsum
), Et reviendrait à prouver n'importe quoi (via l'isomorphisme de Curry-Howard).
Il existe plusieurs façons de penser à ce sujet. Je vais donc plaider pour et contre head'
:
Contre head'
:
Il n'est pas nécessaire d'avoir head'
: Les listes étant un type de données concret, tout ce que vous pouvez faire avec head'
vous pouvez le faire par correspondance de motifs.
De plus, avec head'
vous échangez simplement un foncteur contre un autre. À un moment donné, vous voulez passer aux punaises et effectuer un travail sur l'élément de liste sous-jacent.
Pour la défense de head'
:
Mais la correspondance des motifs obscurcit ce qui se passe. Dans Haskell, nous nous intéressons au calcul des fonctions, ce qui est mieux accompli en les écrivant dans un style sans point à l'aide de compositions et de combinateurs.
De plus, en pensant au []
et Maybe
foncteurs, head'
vous permet de vous déplacer d'avant en arrière (en particulier l'instance Applicative
de []
avec pure = replicate
.)
Si dans votre cas d'utilisation, une liste vide n'a aucun sens, vous pouvez toujours choisir d'utiliser NonEmpty
à la place, où neHead
est sûr à utiliser. Si vous le voyez sous cet angle, ce n'est pas la fonction head
qui n'est pas sûre, c'est toute la structure de données de la liste (encore une fois, pour ce cas d'utilisation).
Je pense que c'est une question de simplicité et de beauté. Ce qui est bien sûr dans l'œil du spectateur.
Si vous venez d'un arrière-plan LISP, vous savez peut-être que les listes sont construites de contre-cellules, chaque cellule ayant un élément de données et un pointeur vers la cellule suivante. La liste vide n'est pas une liste en soi, mais un symbole spécial. Et Haskell va avec ce raisonnement.
À mon avis, c'est à la fois plus propre, plus simple à raisonner et plus traditionnel, si la liste vide et la liste sont deux choses différentes.
... Je peux ajouter - si vous craignez que la tête ne soit pas sûre - ne l'utilisez pas, utilisez plutôt la correspondance de motifs:
sum [] = 0
sum (x:xs) = x + sum xs