52 votes

Existe-t-il un moyen intéressant de rendre les signatures de fonctions plus informatives en Haskell?

Je me rends compte que cela pourrait être considéré comme un subjective ou peut-être une question hors-sujet, j'espère donc que, plutôt que de l'avoir fermé, il serait transféré, peut-être pour les Programmeurs.

Je commence à apprendre Haskell, surtout pour ma propre édification, et j'aime beaucoup les idées et les principes de la sauvegarde de la langue. Je suis devenu fasciné avec les langages fonctionnels après la prise d'un langage de la théorie de la classe où nous avons joué autour avec Lisp, et j'avais entendu beaucoup de bonnes choses sur la façon productive Haskell pourrait être, alors j'ai pensé étudier moi-même. Jusqu'à présent, j'aime la langue, sauf pour une chose que je ne pouvez pas simplement obtenir loin de: Ceux de la mère effing signatures de fonction.

Mon parcours professionnel est principalement de faire OO, surtout en Java. La plupart des endroits que j'ai travaillé pour avoir martelé dans un lot de la norme moderne dogmes; Agile, Code Propre, TDD, etc. Après quelques années de travail de cette façon, Elle est certainement devenue ma zone de confort; en particulier l'idée que le "bon" code doit être auto-documentation. J'ai pris l'habitude de travailler dans un environnement de développement, où de long et détaillé des noms de méthode avec de très descriptif, les signatures sont un non-problème avec les intelligent de saisie semi-automatique et un large éventail d'outils analytiques pour la navigation dans des packages et des symboles; si je puis appuyez sur Ctrl+Espace dans Eclipse, puis en déduire qu'une méthode est de faire de la recherche à son nom et l'localement étendue des variables associées à ses arguments, au lieu de tirer vers le haut la Javadoc, je suis heureux comme un cochon dans la merde.

C'est, décidément, ne fait pas partie de la communauté de meilleures pratiques en Haskell. J'ai lu beaucoup d'avis différents sur la question, et je comprends que le Haskell communauté considère sa concision d'être un "pro". Je suis passé par Comment Lire Haskell, et je comprends la logique derrière un grand nombre de décisions, mais cela ne veut pas dire que je l'aime; une lettre de noms de variables, etc. ne sont-ce pas amusant pour moi. Je reconnais que je vais devoir m'habituer à tout ça si je veux garder le piratage avec la langue.

Mais je ne peux pas obtenir plus de la fonction de signatures. Prenez cet exemple, tiré d' Apprendre que vous avez un Haskell[...]'la section sur la syntaxe de la fonction:

bmiTell :: (RealFloat a) => a -> a -> String  
bmiTell weight height  
    | weight / height ^ 2 <= 18.5 = "You're underweight, you emo, you!"  
    | weight / height ^ 2 <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"  
    | weight / height ^ 2 <= 30.0 = "You're fat! Lose some weight, fatty!"  
    | otherwise                   = "You're a whale, congratulations!"

Je me rends compte que c'est un exemple stupide qui a été créé uniquement dans le but d'expliquer les gardes et les contraintes de classe, mais si vous examinez juste la signature de cette fonction, vous n'avez aucune idée de qui de ses arguments était destiné à être le poids ou la hauteur. Même si vous étiez à utiliser Float ou Double au lieu de tout type, il serait encore de ne pas être immédiatement visibles.

Au début, je pensais que je serais mignon et intelligent et brillant et d'essayer d'usurper l'aide de plus les noms de variables avec de multiples contraintes de classe:

bmiTell :: (RealFloat weight, RealFloat height) => weight -> height -> String

Cette recraché une erreur (en aparté, si quelqu'un peut expliquer l'erreur pour moi, je lui en serais reconnaissant):

Could not deduce (height ~ weight)
    from the context (RealFloat weight, RealFloat height)
      bound by the type signature for
                 bmiTell :: (RealFloat weight, RealFloat height) =>
                            weight -> height -> String
      at example.hs:(25,1)-(27,27)
      `height' is a rigid type variable bound by
               the type signature for
                 bmiTell :: (RealFloat weight, RealFloat height) =>
                            weight -> height -> String
               at example.hs:25:1
      `weight' is a rigid type variable bound by
               the type signature for
                 bmiTell :: (RealFloat weight, RealFloat height) =>
                            weight -> height -> String
               at example.hs:25:1
    In the first argument of `(^)', namely `height'
    In the second argument of `(/)', namely `height ^ 2'
    In the first argument of `(<=)', namely `weight / height ^ 2'

Ne pas comprendre complètement pourquoi cela ne fonctionne pas, j'ai commencé à Googler autour, et j'ai même trouvé ce petit post qui suggère que les paramètres nommés, plus précisément, l'usurpation des paramètres nommés par newtype, mais qui semble un peu beaucoup.

N'est-il pas une manière acceptable pour l'artisanat instructif signatures de fonction? Est "Le Haskell Façon" simplement pour l'Aiglefin la merde hors de tout?

81voto

Ben Points 22160

Une signature de type n'est pas un Java-style de signature. Java est le style de signature vais vous dire quel est le paramètre qui est le poids et qui est la hauteur seulement parce qu'il se mêle les noms de paramètre avec le paramètre type. Haskell ne pouvez pas faire cela comme une règle générale, car les fonctions sont définies à l'aide de la correspondance de modèle et de plusieurs équations, comme dans:

map :: (a -> b) -> [a] -> [b]
map f (x:xs) = f x : map f xs
map _ [] = []

Ici, le premier paramètre est nommé f dans la première équation et en _ (ce qui signifie en gros "sans nom") dans le second. Le deuxième paramètre de ne pas avoir un nom dans l'équation; dans les premières parties de celle-ci ont des noms (et le programmeur sera probablement penser à lui comme "le xs liste"), tandis que dans le second, il est tout à fait littéral de l'expression.

Et puis il n'y a point sans de telles définitions:

concat :: [[a]] -> [a]
concat = foldr (++) []

La signature d'un type nous dit qu'il prend un paramètre de type [[a]], mais pas de nom pour ce paramètre apparaît n'importe où dans le système.

En dehors d'un individu équation d'une fonction, les noms qu'il utilise pour se référer à ses arguments ne sont pas pertinents, de toute façon , sauf que de la documentation. Depuis que l'idée d'un nom canonique d'une fonction du paramètre n'est pas bien définie dans Haskell, la place de l'information "le premier paramètre de l' bmiTell représente le poids tandis que le deuxième correspond à la hauteur" est dans la documentation, non pas dans le type de signature.

Je suis d'accord absolument que ce qu'une fonction ne doit être limpide de la "public" de l'information disponible à ce sujet. En Java, c'est le nom de la fonction, et les types de paramètres et de noms. Si (comme c'est souvent), l'utilisateur aura besoin de plus d'informations que cela, vous l'ajoutez dans la documentation. En Haskell du public des renseignements sur une fonction est le nom de la fonction et les types de paramètres. Si l'utilisateur a besoin de plus d'informations que cela, vous l'ajoutez dans la documentation. Note IDEs pour Haskell, comme Leksah pourrez facilement vous montrer Haddock commentaires.


Notez que le préféré chose à faire dans une langue avec un solide système de type expressif comme Haskell est souvent pour essayer de faire autant d'erreurs détectables que les erreurs de type. Ainsi, une fonction comme bmiTell immédiatement définit les panneaux d'avertissement pour moi, pour les raisons suivantes:

  1. Elle prend deux paramètres de même type représentant les différentes choses
  2. Il va faire la mauvaise chose si les paramètres passés dans le mauvais ordre
  3. Les deux types n'ont pas une position naturelle (comme les deux [a] arguments au ++ le font)

Une chose qui est souvent fait pour augmenter la sécurité de type est de rendre les newtypes, comme dans le lien que vous avez trouvé. Je ne pense pas vraiment avoir de ce fait beaucoup de choses à faire avec le paramètre nommé en passant, que c'est un type de données qui représente explicitement la hauteur, plutôt que de toute autre quantité que vous pouvez vouloir mesurer avec un certain nombre. Donc, je ne voudrais pas avoir le newtype valeurs apparaissant seulement à l'appel; je serais en utilisant le newtype de la valeur partout où j'ai obtenu les données de hauteur de ainsi, et de la passer autour de la taille de données plutôt que comme un certain nombre, en sorte que je reçois le type de sécurité (et la documentation) bénéficient partout. Je tiens seulement à déballer la valeur en un nombre brut quand j'ai besoin de passer à quelque chose qui fonctionne sur des chiffres et non sur la hauteur (comme les opérations de calcul à l'intérieur d' bmiTell).

Notez que cela n'a pas de gestion d'exécution; newtypes sont représentés de façon identique pour les données de "l'intérieur" de la newtype wrapper, afin de l'enrouler/dérouler les opérations sont non-ops sur la représentation sous-jacente et sont tout simplement supprimé lors de la compilation. Il ajoute uniquement les caractères supplémentaires dans le code source, mais ces personnages sont exactement la documentation que vous êtes à la recherche d', avec l'avantage supplémentaire d'être appliquées par le compilateur; Java-style signatures de vous dire quel est le paramètre qui est du poids et de la hauteur, mais le compilateur ne serez pas en mesure de dire si vous avez accidentellement transmis à l'envers!

37voto

C. A. McCann Points 56834

Il y a d'autres options, en fonction de la façon dont idiot et/ou pédant vous souhaitez obtenir votre types.

Par exemple, vous pourriez faire...

type Meaning a b = a

bmiTell :: (RealFloat a) => a `Meaning` weight -> a `Meaning` height -> String  
bmiTell weight height = -- etc.

...mais c'est incroyablement stupide, déroutant, et n'aide pas dans la plupart des cas. Il en va de même pour ce, qui, de plus, nécessite l'aide d'extensions de langage:

bmiTell :: (RealFloat weight, RealFloat height, weight ~ height) 
        => weight -> height -> String  
bmiTell weight height = -- etc.

Légèrement plus raisonnable serait celui-ci:

type Weight a = a
type Height a = a

bmiTell :: (RealFloat a) => Weight a -> Height a -> String  
bmiTell weight height = -- etc.

...mais c'est encore un peu maladroit et a tendance à se perdre quand GHC étend type de synonymes.

Le vrai problème ici, c'est que vous êtes fixation supplémentaire sémantique de contenu pour différentes valeurs du même type polymorphe, ce qui va à l'encontre du grain de la langue elle-même et, comme telles, généralement pas idiomatique.

Une option, bien sûr, est de simplement traiter avec peu de variables de type. Mais ce n'est pas très satisfaisant s'il y a une importante distinction entre deux choses de même type qui n'est pas évident de l'ordre qu'ils sont donnés dans.

Ce que je vous recommande d'essayer, au contraire, à l'aide de newtype des wrappers pour spécifier la sémantique:

newtype Weight a = Weight { getWeight :: a }
newtype Height a = Height { getHeight :: a }

bmiTell :: (RealFloat a) => Weight a -> Height a -> String  
bmiTell (Weight weight) (Height height)

Cela est nulle part près d'aussi commun que mérite d'être, je pense. C'est un peu de saisie de texte supplémentaire (ha, ha), mais pas seulement de rendre votre type de signatures de plus instructif, même avec le type de synonymes élargi, il permet au vérificateur de types attraper si par erreur vous utilisez un poids en tant que hauteur de, ou de de ces. Avec l' GeneralizedNewtypeDeriving extension vous pouvez même obtenir automatique des instances de même pour les classes de type qui ne peuvent pas normalement être dérivée.

27voto

singpolyma Points 5586

Haddocks et/ou à la recherche également la fonction de l'équation (les noms que vous avez lié les choses) sont les moyens que je raconte ce qui se passe. Vous pouvez Haddock différents paramètres, comme ceci,

bmiTell :: (RealFloat a) => a      -- ^ your weight
                         -> a      -- ^ your height
                         -> String -- ^ what I'd think about that

il n'est donc pas juste une goutte de texte qui explique tous les trucs.

La raison de votre mignon variables de type n'a pas fonctionné, c'est que votre fonction est:

(RealFloat a) => a -> a -> String

Mais votre tentative de modification:

(RealFloat weight, RealFloat height) => weight -> height -> String

est équivalent à ceci:

(RealFloat a, RealFloat b) => a -> b -> String

Donc, dans ce type de signature que vous avez dit que les deux premiers arguments sont de différents types, mais le GHC a déterminé que (selon votre utilisation) ils doivent avoir le même type. Donc, il se plaint qu'il ne peut pas déterminer qu' weight et height sont du même type, même si elles doivent être (qui est, votre proposition de signature de type n'est pas assez stricte et permettrait aux invalides utilise de la fonction).

14voto

AndrewC Points 21273

weight doit être du même type que height parce que vous êtes en les divisant (pas de conversions implicites. weight ~ height signifie qu'ils sont du même type. ghc a tourné un peu en expliquant comment il est venu à la conclusion qu' weight ~ height était nécessaire, désolé. Vous êtes autorisé à dire ce qu'il/vous avez voulu à l'aide d'une syntaxe du type de familles extension:

{-# LANGUAGE TypeFamilies #-}
bmiTell :: (RealFloat weight, RealFloat height,weight~height) => weight -> height -> String
bmiTell weight height  
  | weight / height ^ 2 <= 18.5 = "You're underweight, you emo, you!"  
  | weight / height ^ 2 <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"  
  | weight / height ^ 2 <= 30.0 = "You're fat! Lose some weight, fatty!"  
  | otherwise                   = "You're a whale, congratulations!"

Cependant, ce n'est pas l'idéal non plus. Vous devez garder à l'esprit que Haskell utilise un très paradigme différent, en effet, et vous devez être prudent de ne pas tourner en supposant que ce qui est important dans une autre langue est importante ici. Vous êtes d'apprentissage plus quand vous êtes en dehors de votre zone de confort. C'est comme quelqu'un de Londres tournage à Toronto et à se plaindre de la ville est source de confusion car toutes les rues sont les mêmes, alors que quelqu'un de Toronto pourrait prétendre, à Londres, est source de confusion car il n'y a pas de régularité dans les rues. Ce que vous appelez l'obfuscation est appelé clarté par Haskellers.

Si vous souhaitez revenir à plus orienté objet clarté de l'objectif, puis de faire l'bmiTell travailler seulement sur les personnes, de sorte

data Person = Person {name :: String, weight :: Float, height :: Float}
bmiOffence :: Person -> String
bmiOffence p
  | weight p / height p ^ 2 <= 18.5 = "You're underweight, you emo, you!"  
  | weight p / height p ^ 2 <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"  
  | weight p / height p ^ 2 <= 30.0 = "You're fat! Lose some weight, fatty!"  
  | otherwise                   = "You're a whale, congratulations!"

C'est, je crois, est le tri de la façon dont vous le faire comprendre, en programmation orientée objet. Je ne crois vraiment pas que vous utilisez le type de votre programmation orientée objet, les arguments de méthode pour obtenir ces informations, vous devez être secrètement en utilisant les noms de paramètre pour la clarté plutôt que les types, et il n'est guère raisonnable de s'attendre à haskell pour vous dire les noms de paramètre lorsque vous exclure de la lecture des noms de paramètre dans votre question.[voir * ci-dessous] Le type de système en Haskell est remarquablement souple et très puissant, s'il vous plaît ne pas abandonner juste parce que c'est d'abord de s'aliéner pour vous.

Si vous voulez vraiment les types de vous le dire, nous pouvons le faire pour vous:

type Weight = Float -- a type synonym - Float and Weight are exactly the same type, but human-readably different
type Height = Float

bmiClear :: Weight -> Height -> String
....

C'est l'approche utilisée avec des Chaînes de caractères qui représentent les noms de fichiers, c'est pourquoi nous définissons

type FilePath = String
writeFile :: FilePath -> String -> IO ()  -- take the path, the contents, and make an IO operation

ce qui donne de la clarté vous avez été au bout. Cependant, il est estimé que

type FilePath = String

manque de type de sécurité, et que

newtype FilePath = FilePath String

ou quelque chose d'encore plus intelligent serait une bien meilleure idée. Voir Ben répondre pour point très important sur le type de sécurité.

[*] OK, vous pouvez faire :t dans ghci et d'obtenir la signature d'un type sans le nom du paramètre, mais ghci est interactif de développement du code source. Votre bibliothèque ou d'un module ne devrait pas rester sans papiers et hacky, vous devez utiliser le incroyablement léger syntaxe haddock système de documentation et d'installer l'aiglefin localement. La légitimité de la version de votre plainte serait qu'il n'y a pas une :v commande qui affiche le code source de votre fonction bmiTell. Les métriques suggèrent que votre code Haskell pour le même problème sera réduit par un facteur (j'ai trouver sur 10 dans mon cas par rapport à l'équivalent OO ou non oo code impératif), afin de montrer la définition à l'intérieur de gchi est souvent sensible. On doit soumettre une demande de fonctionnalité.

13voto

Gabriel Gonzalez Points 23530

Essaye ça:

 type Height a = a
type Weight a = a

bmiTell :: (RealFloat a) => Weight a -> Height a -> String
 

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X