Considérez ceci comme une question "académique". Je me demandais de temps en temps d'éviter les NULL et c'est un exemple où je ne peux pas trouver de solution satisfaisante.
Supposons que je stocke les mesures là où, à l'occasion, on sait que la mesure est impossible (ou manquante). Je voudrais stocker cette valeur "vide" dans une variable tout en évitant NULL. D'autres fois, la valeur peut être inconnue. Ainsi, ayant les mesures pour une certaine période, une requête sur une mesure dans cette période pourrait renvoyer 3 types de réponses:
- La mesure réelle à ce moment (par exemple, toute valeur numérique, y compris
0
) - Une valeur "manquant" / "vide" (c'est-à-dire qu'une mesure a été effectuée et que la valeur est connue pour être vide à ce point).
- Une valeur inconnue (c'est-à-dire qu'aucune mesure n'a été effectuée à ce stade. Elle peut être vide, mais il peut également s'agir d'une autre valeur).
Précision importante:
En supposant que vous ayez une fonction get_measurement()
renvoyant un "vide", "inconnu" et une valeur de type "entier". Avoir une valeur numérique implique que certaines opérations peuvent être effectuées sur la valeur de retour (multiplication, division, ...), mais l'utilisation de telles opérations sur des valeurs NULL plantera l'application si elle n'est pas interceptée.
J'aimerais pouvoir écrire du code en évitant les contrôles NULL, par exemple (pseudocode):
>>> value = get_measurement() # returns `2`
>>> print(value * 2)
4
>>> value = get_measurement() # returns `Empty()`
>>> print(value * 2)
Empty()
>>> value = get_measurement() # returns `Unknown()`
>>> print(value * 2)
Unknown()
Notez qu'aucune des print
instructions n'a provoqué d'exception (aucune valeur NULL n'ayant été utilisée). Ainsi, les valeurs vides et inconnues se propagent selon les besoins et le contrôle d'une valeur "inconnue" ou "vide" peut être retardé jusqu'à ce qu'il soit réellement nécessaire (comme stocker / sérialiser la valeur quelque part).
Note latérale: La raison pour laquelle j'aimerais éviter les NULL, c'est avant tout un casse-tête. Si je veux obtenir des résultats, je ne suis pas opposé à l'utilisation de valeurs NULL, mais j'ai constaté que les éviter peut rendre le code beaucoup plus robuste dans certains cas.
0
,[]
ou{}
(le scalaire 0, la liste vide et la carte vide, respectivement). De plus, cette valeur "manquant" / "inconnue" est fondamentalement exactement ce à quoi ellenull
sert - cela signifie qu'il pourrait y avoir un objet là-bas, mais il n'y en a pas.Réponses:
La façon habituelle de faire cela, du moins avec les langages fonctionnels, consiste à utiliser un syndicat discriminé. Il s'agit alors d'une valeur appartenant à un int valide, une valeur indiquant "manquant" ou une valeur indiquant "inconnu". En F #, cela pourrait ressembler à quelque chose comme:
Une
Measurement
valeur sera alors unReading
, avec une valeur int, ou unMissing
ouUnknown
avec les données brutes commevalue
(si nécessaire).Toutefois, si vous n'utilisez pas un langage prenant en charge les syndicats discriminés, ou leur équivalent, ce modèle ne vous sera probablement pas d'une grande utilité. Ainsi, vous pouvez par exemple utiliser une classe avec un champ enum qui indique laquelle des trois contient les données correctes.
la source
std::variant
(et ses prédécesseurs spirituels).Si vous ne savez pas déjà ce qu'est une monade, ce serait un bon jour pour apprendre. J'ai une introduction douce pour les programmeurs OO ici:
https://ericlippert.com/2013/02/21/monads-part-one/
Votre scénario est une petite extension de la "monade peut-être", également connue sous le nom
Nullable<T>
de C # etOptional<T>
d'autres langues.Supposons que vous ayez un type abstrait pour représenter la monade:
puis trois sous-classes:
Nous avons besoin d'une implémentation de Bind:
À partir de là, vous pouvez écrire cette version simplifiée de Bind:
Et maintenant tu as fini. Vous avez un
Measurement<int>
en main. Vous voulez le doubler:Et suivez la logique; si
m
estEmpty<int>
alorsasString
estEmpty<String>
, excellent.De même, si nous avons
et
alors nous pouvons combiner deux mesures:
et encore, si
First()
estEmpty<int>
alorsd
estEmpty<double>
et ainsi de suite.L'étape clé consiste à obtenir l'opération de liaison correcte . Pensez-y bien.
la source
Null
parNullable
+ du code standard? :)Measurement<T>
s'agit du type monadique.Je pense que dans ce cas, une variation sur un modèle d'objet nul serait utile:
Vous pouvez le transformer en une structure, remplacer Equals / GetHashCode / ToString, ajouter des conversions implicites de ou vers
int
, et si vous souhaitez un comportement de type NaN, vous pouvez également implémenter vos propres opérateurs arithmétiques de sorte.Measurement.Unknown * 2 == Measurement.Unknown
.Cela dit, C #
Nullable<int>
implémente tout cela, le seul inconvénient étant qu'il est impossible de faire la différence entre différents types denull
s. Je ne suis pas une personne de Java, mais je crois comprendre que celui-ciOptionalInt
est similaire et que d'autres langages ont probablement leurs propres installations pour représenter unOptional
type.la source
Value
getter, qui doit absolument échouer car vous ne pouvez pas convertir unUnknown
back en unint
. Si la mesure avait uneSaveToDatabase()
méthode , disons, alors une bonne mise en œuvre n'effectuerait probablement pas de transaction si l'objet actuel est un objet null (soit par comparaison avec un singleton, soit par un remplacement de méthode).Si vous DEVEZ utiliser un nombre entier, il n’ya qu’une solution possible. Utilisez certaines des valeurs possibles comme «nombres magiques» qui signifient «manquant» et «inconnu»
par exemple 2 147 483 647 et 2 147 483 646
Si vous avez simplement besoin de l'int pour les mesures "réelles", créez une structure de données plus complexe.
Précision importante:
Vous pouvez répondre aux exigences mathématiques en surchargeant les opérateurs de la classe.
la source
Option<Option<Int>>
type Measurement = Option<Int>
résultat pour un résultat qui est un entier ou une lecture vide convient, de mêmeOption<Measurement>
qu'une mesure pour une mesure qui aurait été prise ou non. .Si vos variables sont des nombres à virgule flottante, IEEE754 (le nombre à virgule flottante standard qui est pris en charge par la plupart des processeurs modernes et les langues) a le dos: il est une option peu connue, mais les définit standards non pas un, mais toute une famille de Les valeurs NaN (pas un nombre), qui peuvent être utilisées pour des significations arbitraires définies par l'application. Dans les flottants à simple précision, par exemple, vous disposez de 22 bits libres que vous pouvez utiliser pour distinguer 2 types de valeurs non valides.
Normalement, les interfaces de programmation n’exposent que l’un d’eux (par exemple, Numpy's
nan
); Je ne sais pas s'il existe un moyen intégré de générer d'autres méthodes que la manipulation de bits explicite, mais il suffit d'écrire quelques routines de bas niveau. (Vous aurez également besoin de quelqu'un pour les différencier, car, par conception,a == b
renvoie toujours faux lorsque l'un d'eux est un NaN.)Leur utilisation vaut mieux que réinventer votre propre "nombre magique" pour signaler des données non valides, car elles se propagent correctement et signalent une invalidité: par exemple, vous ne risquez pas de vous tirer une balle dans le pied si vous utilisez une
average()
fonction et oubliez de vérifier vos valeurs spéciales.Le seul risque est que les bibliothèques ne les prennent pas en charge correctement, car elles constituent une fonctionnalité assez obscure: par exemple, une bibliothèque de sérialisation peut les «aplatir» pour les rendre identiques
nan
(ce qui lui semble équivalent dans la plupart des cas).la source
Après la réponse de David Arno , vous pouvez faire quelque chose comme une union discriminée en POO, et dans un style fonctionnel comme celui fourni par Scala, par les types fonctionnels Java 8 ou dans une bibliothèque Java FP telle que Vavr ou Fugue, cela semble assez naturel d'écrire quelque chose comme:
impression
( Mise en œuvre complète en tant qu'essentiel .)
Un langage ou une bibliothèque FP fournit d'autres outils tels que
Try
(akaMaybe
) (un objet contenant une valeur ou une erreur) etEither
(un objet contenant une valeur de réussite ou une valeur d'échec), qui pourraient également être utilisés ici.la source
La solution idéale à votre problème dépend des raisons pour lesquelles vous vous souciez de la différence entre une défaillance connue et une mesure non fiable connue et des processus en aval que vous souhaitez prendre en charge. Notez que, dans ce cas, les «processus en aval» n'excluent pas les opérateurs humains ou les développeurs.
Le simple fait de proposer une "seconde variante" de null ne donne pas à l'ensemble de processus en aval suffisamment d'informations pour dériver un ensemble raisonnable de comportements.
Si vous vous basez plutôt sur des hypothèses contextuelles sur la source des mauvais comportements engendrés par le code en aval, nous appellerions cette mauvaise architecture.
Si vous en savez suffisamment pour faire la distinction entre un motif d'échec et un échec sans motif connu, et que cette information va éclairer les comportements futurs, vous devriez communiquer ces connaissances en aval ou les gérer en ligne.
Quelques modèles pour gérer ceci:
null
la source
Si je voulais "faire quelque chose" plutôt qu'une solution élégante, le truc rapide et sale consisterait simplement à utiliser les chaînes "inconnu", "manquant" et "représentation sous forme de chaîne de ma valeur numérique", qui serait alors converti à partir d'une chaîne et utilisé selon les besoins. Mis en œuvre plus rapidement que d'écrire ceci et, du moins dans certaines circonstances, tout à fait adéquat. (Je forme maintenant un pool de paris sur le nombre de votes négatifs ...)
la source
Si la question semble être la suivante: "Comment renvoyer deux informations sans relation d'une méthode qui renvoie un seul int? Je ne veux jamais vérifier mes valeurs de retour et les valeurs NULL sont incorrectes, ne les utilisez pas."
Regardons ce que vous voulez transmettre. Vous transmettez soit un int, soit un non- raisonnement pour lequel vous ne pouvez pas donner l'int. La question affirme qu'il n'y aura que deux raisons, mais quiconque a déjà fait une énumération sait que la liste s'allongera. La possibilité de spécifier d’autres raisons est tout à fait logique.
Au départ, cela pourrait donc être un bon argument pour lancer une exception.
Lorsque vous souhaitez indiquer à l'appelant quelque chose de spécial qui ne figure pas dans le type de retour, les exceptions constituent souvent le système approprié: les exceptions ne concernent pas uniquement les états d'erreur et vous permettent de renvoyer une grande quantité de contexte et de raisons pour expliquer pourquoi vous pouvez simplement le faire. t int aujourd'hui.
Et c’est le SEUL système qui vous permet de renvoyer des ints garantis-valides et garantit que chaque opérateur int et chaque méthode prenant ints peut accepter la valeur de retour de cette méthode sans avoir à rechercher des valeurs non valides telles que null ou des valeurs magiques.
Mais les exceptions ne sont réellement qu'une solution valable si, comme son nom l'indique, il s'agit d'un cas exceptionnel et non du cours normal des affaires.
Et essayer / attraper et manipuler équivaut tout autant à un passe-partout qu'un contrôle nul, ce à quoi on s'est objecté au départ.
Et si l'appelant ne contient pas try / catch, l'appelant doit le faire, et ainsi de suite.
Un second passage naïf consiste à dire "C'est une mesure. Les mesures de distance négatives sont peu probables." Donc, pour certaines mesures Y, vous pouvez simplement avoir des consts pour
C’est comme cela que l’on fait dans beaucoup d’anciens systèmes C, et même dans les systèmes modernes où il existe une contrainte réelle pour int, et que vous ne pouvez pas l’envelopper dans une structure ou une monade quelconque.
Si les mesures peuvent être négatives, vous devez simplement augmenter la taille de votre type de données (par exemple, long int) et faire en sorte que les valeurs magiques soient supérieures à la plage de int, et idéalement, commencez par une valeur qui apparaîtra clairement dans un débogueur.
Il y a de bonnes raisons de les avoir comme variable distincte, plutôt que d'avoir simplement des nombres magiques, cependant. Par exemple, dactylographie stricte, maintenabilité et conformité aux attentes.
Dans notre troisième tentative, nous examinons donc les cas où il est normal que des valeurs non int soient utilisées. Par exemple, si une collection de ces valeurs peut contenir plusieurs entrées non entières. Cela signifie qu'un gestionnaire d'exceptions peut être une mauvaise approche.
Dans ce cas, il semble judicieux de disposer d’une structure qui passe l’int, et de la logique. Encore une fois, cette justification peut simplement être une constante comme ci-dessus, mais au lieu de conserver les deux dans le même int, vous les stockez en tant que parties distinctes d'une structure. Au départ, nous avons la règle que si la justification est définie, l'int ne sera pas défini. Mais nous ne sommes plus liés à cette règle; nous pouvons également fournir des justifications pour les nombres valides, le cas échéant.
Quoi qu'il en soit, chaque fois que vous l'appelez, vous avez toujours besoin d'un passe-partout pour tester la justification de voir si l'int est valide, puis retirez-vous et utilisez la partie int si la justification nous le permet.
C'est là que vous devez étudier votre raisonnement derrière "ne pas utiliser null".
Comme les exceptions, null signifie un état exceptionnel.
Si un appelant appelle cette méthode et ignore complètement la partie "justification" de la structure, attend un numéro sans traitement d'erreur et obtient un zéro, il le traitera comme un nombre et se trompera. Si cela donne un nombre magique, il le traitera comme un nombre et se trompera. Mais si elle devient nulle, elle tombera , comme il se doit.
Donc, chaque fois que vous appelez cette méthode, vous devez vérifier si sa valeur de retour est vérifiée. Cependant, vous manipulez les valeurs non valides, qu'elles soient dans la bande ou hors bande, try / catch, en recherchant un composant "raisonnement" dans la structure, pour un nombre magique, ou en vérifiant un int pour un null ...
L'alternative, pour gérer la multiplication d'une sortie pouvant contenir un int invalide et une logique telle que "Mon chien a mangé cette mesure", consiste à surcharger l'opérateur de multiplication de cette structure.
... et surchargez ensuite tous les autres opérateurs de votre application susceptibles d'être appliqués à ces données.
... Et ensuite surcharger toutes les méthodes qui pourraient prendre ints.
... Et toutes ces surcharges devront toujours contenir des vérifications pour les entrées non valides, afin que vous puissiez traiter le type de retour de cette méthode comme s'il s'agissait toujours d'un entier valide au moment où vous l'appelez.
La prémisse originale est donc fausse de différentes manières:
la source
Je ne comprends pas la prémisse de votre question, mais voici la réponse en valeur faciale. Pour manquant ou vide, vous pouvez faire
math.nan
(pas un nombre). Vous pouvez effectuer toutes les opérations mathématiquesmath.nan
et cela resteramath.nan
.Vous pouvez utiliser
None
(null Python) pour une valeur inconnue. De toute façon, vous ne devriez pas manipuler une valeur inconnue, et certaines langues (Python n’en fait pas partie) ont des opérateurs null spéciaux afin que l’opération ne soit exécutée que si la valeur est non nulle, sinon la valeur reste nulle.D'autres langues ont des clauses de garde (comme Swift ou Ruby), et Ruby a un retour anticipé conditionnel.
J'ai vu cela résolu en Python de différentes manières:
__mult__
manière à ce qu'aucune exception ne soit déclenchée lorsque vos valeurs inconnues ou manquantes apparaissent. Numpy et les pandas pourraient avoir une telle capacité en eux.Unknown
ou -1 / -2) et une instruction ifla source
La manière dont la valeur est stockée en mémoire dépend de la langue et des détails d'implémentation. Je pense que ce que vous voulez dire, c'est comment l'objet doit se comporter pour le programmeur. (C'est comme ça que j'ai lu la question, dites-moi si je me trompe.)
Vous avez déjà proposé une réponse à cette question dans votre question: utilisez votre propre classe qui accepte toute opération mathématique et se retourne elle-même sans déclencher une exception. Vous dites que vous voulez cela parce que vous voulez éviter les contrôles nuls.
Solution 1: ne pas éviter les contrôles nuls
Missing
peut être représenté commemath.nan
Unknown
peut être représenté commeNone
Si vous avez plusieurs valeurs, vous pouvez
filter()
uniquement appliquer l'opération à des valeurs qui ne sont pasUnknown
ouMissing
, ou à toutes les valeurs que vous souhaitez ignorer pour la fonction.Je ne peux pas imaginer un scénario dans lequel vous avez besoin d'une vérification nulle sur une fonction qui agit sur un seul scalaire. Dans ce cas, il est bon de forcer les contrôles nuls.
Solution 2: utilisez un décorateur qui capte les exceptions
Dans ce cas,
Missing
pourrait augmenterMissingException
etUnknown
pourrait augmenterUnknownException
lorsque des opérations sont effectuées sur celui-ci.L'avantage de cette approche est que les propriétés de
Missing
etUnknown
ne sont supprimées que lorsque vous leur demandez explicitement de les supprimer. Un autre avantage est que cette approche est auto-documentée: chaque fonction indique si elle attend ou non un inconnu ou un manquant et comment la fonction.Lorsque vous appelez une fonction ne vous attendez pas à ce qu'un manquant obtienne un manquant, la fonction se lèvera immédiatement, vous montrant exactement où l'erreur s'est produite au lieu d'échouer en silence et de propager un manquant dans la chaîne d'appels. La même chose vaut pour Unknown.
sigmoid
peut toujours appelersin
, même s'il ne s'attend pas à unMissing
ouUnknown
, puisquesigmoid
le décorateur attrapera l'exception.la source
Ces deux conditions ressemblant à des erreurs, j'estime donc que la meilleure option consiste simplement à les
get_measurement()
lancer immédiatement comme des exceptions (telles queDataSourceUnavailableException
ouSpectacularFailureToGetDataException
respectivement). Ensuite, si l'un de ces problèmes se produit, le code de collecte de données peut y réagir immédiatement (par exemple, en essayant à nouveau dans ce dernier cas) etget_measurement()
ne doit renvoyer unint
message que s'il parvient à récupérer les données à partir des données. source - et vous savez que leint
est valide.Si votre situation ne prend pas en charge les exceptions ou ne les utilise pas beaucoup, une bonne solution consiste à utiliser des codes d'erreur, éventuellement renvoyés via une sortie distincte
get_measurement()
. Il s'agit du modèle idiomatique en C, où la sortie réelle est stockée dans un pointeur d'entrée et un code d'erreur est renvoyé en tant que valeur de retour.la source
Les réponses données sont correctes, mais ne reflètent toujours pas la relation hiérarchique entre valeur, vide et inconnu.
Ugly (pour son abstraction défaillante), mais pleinement opérationnel serait (en Java):
Ici, les langages fonctionnels avec un bon système de caractères sont meilleurs.
En fait: Les vides / manquantes et * inconnues non-valeurs semblent plutôt partie d'un état du processus, une pipeline de production. Comme les feuilles de calcul Excel avec des formules faisant référence à d'autres cellules. Là, on pourrait penser à peut-être stocker des lambdas contextuels. Changer une cellule réévaluerait toutes les cellules dépendantes de manière récursive.
Dans ce cas, une valeur int serait obtenue par un fournisseur int. Une valeur vide donnerait à un fournisseur int lançant une exception vide ou évaluant le vidage (de manière récursive vers le haut). Votre formule principale relierait toutes les valeurs et renverrait éventuellement un vide (valeur / exception). Une valeur inconnue désactiverait l'évaluation en lançant une exception.
Les valeurs seraient probablement observables, comme une propriété java liée, notifiant les auditeurs du changement.
En bref: le modèle récurrent de besoin de valeurs avec des états supplémentaires vide et inconnu semble indiquer qu'un modèle de données plus répandu, similaire aux propriétés liées, pourrait être préférable.
la source
Oui, le concept de multiples types de NA différents existe dans certaines langues. plus encore dans les statistiques, où il est plus significatif (à savoir la différence énorme entre Missing-At-Random, Missing-Complètement-At-Random, Missing-Not-At-Random ).
si nous ne mesurons que la longueur des widgets, il n'est pas crucial de faire la distinction entre "défaillance du capteur" et "coupure de courant" ou "défaillance du réseau" (bien que le "dépassement numérique" transmette des informations)
mais, par exemple, dans l'exploration de données ou dans une enquête, demandant par exemple aux personnes interrogées leur revenu ou leur statut VIH, le résultat de "inconnu" est différent de "refuser de répondre", et vous pouvez voir que nos hypothèses antérieures sur la manière de les imputer auront tendance être différent de l'ancien. Ainsi, des langages tels que SAS prennent en charge plusieurs types de NA différents; le langage R ne l’est pas, mais les utilisateurs doivent très souvent y réfléchir; Les NA à différents points d'un pipeline peuvent être utilisés pour désigner des choses très différentes.
En ce qui concerne la façon dont vous représentez différents types de NA dans des langages généraux qui ne les prennent pas en charge, les gens piratent des objets tels que NaN à virgule flottante (nécessite la conversion d’entiers), d’énums ou de sentinelles (par exemple, 999 ou -1000). valeurs catégoriques. D'habitude, il n'y a pas de réponse très propre, désolé.
la source
R possède une prise en charge intégrée des valeurs manquantes. https://medium.com/coinmonks/dealing-with-missing-data-using-r-3ae428da2d17
Edit: parce que j'ai été voté, je vais expliquer un peu.
Si vous envisagez de traiter des statistiques, je vous recommande d'utiliser un langage de statistiques tel que R, car R est écrit par des statisticiens pour des statisticiens. Les valeurs manquantes sont un sujet tellement important qu'ils vous apprennent un semestre entier. Et il existe de gros livres uniquement sur les valeurs manquantes.
Vous pouvez cependant vouloir marquer vos données manquantes, comme un point ou "manquant" ou autre chose. En R, vous pouvez définir ce que vous voulez dire par manquant. Vous n'avez pas besoin de les convertir.
La manière normale de définir la valeur manquante consiste à les marquer comme
NA
.Ensuite, vous pouvez voir quelles valeurs manquent.
Et alors le résultat sera;
Comme vous pouvez le voir, il
""
ne manque pas. Vous pouvez menacer""
comme inconnu. EtNA
manque.la source
Y a-t-il une raison pour laquelle la fonctionnalité de l'
*
opérateur ne peut pas être modifiée à la place?La plupart des réponses impliquent une valeur de recherche, mais il serait peut-être plus simple de modifier l'opérateur mathématique dans ce cas.
Vous seriez alors en mesure d’avoir des fonctionnalités
empty()
/ similairesunknown()
sur l’ensemble de votre projet.la source