web-dev-qa-db-fra.com

Pourquoi la tête de Haskell plante-t-elle sur une liste vide (ou pourquoi * ne * retourne-t-elle pas une liste vide)? (Philosophie du langage)

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.

73
Jack Henahan

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.

96
Russell O'Connor

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 ).

23
Tsuyoshi Ito

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).

8
yatima2975

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.)

4
Lambdageek

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).

2
Landei

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
1
Johan Kotlinski