Comment dois-je stocker les valeurs «inconnues» et «manquantes» dans une variable, tout en conservant la différence entre «inconnu» et «manquant»?

57

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

exhuma
la source
19
Pourquoi voulez-vous distinguer "mesure effectuée mais valeur vide" et "pas de mesure"? En fait, que signifie "mesure effectuée mais valeur vide"? Le capteur a-t-il échoué à produire une valeur valide? Dans ce cas, en quoi est-ce différent de "inconnu"? Vous ne pourrez pas revenir en arrière et obtenir la valeur correcte.
DaveG
3
@DaveG Supposons récupérer le nombre de processeurs sur un serveur. Si le serveur est éteint ou mis au rebut, cette valeur n'existe tout simplement pas. Ce sera une mesure qui n’a aucun sens (peut-être que "manquant" / "vide" ne sont pas les meilleurs termes). Mais la valeur est "connue" pour être absurde. Si le serveur existe, mais que le processus d'extraction de la valeur se bloque, sa mesure est valide, mais échoue, ce qui entraîne une valeur "inconnue".
Exhuma
2
@ Exhuma Je le décrirais comme "non applicable", alors.
Vincent
6
Par curiosité, quel type de mesure prenez-vous lorsque "vide" n'est pas simplement égal au zéro de l'échelle? "Inconnu" / "manquant" Je pense que cela peut être utile, par exemple si un capteur n'est pas branché ou si la sortie brute du capteur est perturbée pour une raison ou une autre, mais "vide" dans tous les cas auquel je peux penser peut être plus cohérent représenté par 0, []ou {}(le scalaire 0, la liste vide et la carte vide, respectivement). De plus, cette valeur "manquant" / "inconnue" est fondamentalement exactement ce à quoi elle nullsert - cela signifie qu'il pourrait y avoir un objet là-bas, mais il n'y en a pas.
Nic Hartley
7
Quelle que soit la solution que vous utilisez pour cela, assurez-vous de vous demander si elle présente des problèmes similaires à ceux qui vous ont incité à vouloir éliminer NULL.
Ray

Réponses:

85

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:

type Measurement =
    | Reading of value : int
    | Missing
    | Unknown of value : RawData

Une Measurementvaleur sera alors un Reading, avec une valeur int, ou un Missingou Unknownavec les données brutes comme value(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.

David Arno
la source
7
vous pouvez faire des types de somme dans les langues OO mais il y a un peu de plaque de chaudière pour les faire fonctionner stackoverflow.com/questions/3151702/…
jk.
11
“[Dans les langages de langues non fonctionnelles] ce motif ne vous sera probablement pas d'une grande utilité” - C'est un motif assez courant en POO. GOF propose une variante de ce modèle et des langages tels que C ++ proposent des constructions natives pour le coder.
Konrad Rudolph
14
@jk. Oui, ils ne comptent pas (eh bien je suppose qu'ils le font; ils sont simplement très mauvais dans ce scénario en raison du manque de sécurité). Je voulais dire std::variant(et ses prédécesseurs spirituels).
Konrad Rudolph
2
@Ewan Non, cela dit «La mesure est un type de données qui est soit… ou…».
Konrad Rudolph
2
@DavidArno Eh bien, même sans UD, il existe une solution «canonique» dans OOP, qui consiste à avoir une super-classe de valeurs avec des sous-classes pour les valeurs valides et non valides. Mais c'est probablement aller trop loin (et dans la pratique, il semble que la plupart des bases de code évitent le polymorphisme des sous-classes en faveur d'un drapeau pour cela, comme indiqué dans d'autres réponses).
Konrad Rudolph
58

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 # et Optional<T>d'autres langues.

Supposons que vous ayez un type abstrait pour représenter la monade:

abstract class Measurement<T> { ... }

puis trois sous-classes:

final class Unknown<T> : Measurement<T> { ... a singleton ...}
final class Empty<T> : Measurement<T> { ... a singleton ... }
final class Actual<T> : Measurement<T> { ... a wrapper around a T ...}

Nous avons besoin d'une implémentation de Bind:

abstract class Measurement<T>
{ 
    public Measurement<R> Bind(Func<T, Measurement<R>> f)
  {
    if (this is Unknown<T>) return Unknown<R>.Singleton;
    if (this is Empty<T>) return Empty<R>.Singleton;
    if (this is Actual<T>) return f(((Actual<T>)this).Value);
    throw ...
  }

À partir de là, vous pouvez écrire cette version simplifiée de Bind:

public Measurement<R> Bind(Func<A, R> f) 
{
  return this.Bind(a => new Actual<R>(f(a));
}

Et maintenant tu as fini. Vous avez un Measurement<int>en main. Vous voulez le doubler:

Measurement<int> m = whatever;
Measurement<int> doubled = m.Bind(a => a * 2);
Measurement<string> asString = m.Bind(a => a.ToString());

Et suivez la logique; si mest Empty<int>alors asStringest Empty<String>, excellent.

De même, si nous avons

Measurement<int> First()

et

Measurement<double> Second(int i);

alors nous pouvons combiner deux mesures:

Measurement<double> d = First().Bind(Second);

et encore, si First()est Empty<int>alors dest Empty<double>et ainsi de suite.

L'étape clé consiste à obtenir l'opération de liaison correcte . Pensez-y bien.

Eric Lippert
la source
4
Les monades (heureusement) sont beaucoup plus faciles à utiliser que à comprendre. :)
Guran
11
@leftaroundabout: Justement parce que je ne voulais pas me lancer dans cette distinction: couper les cheveux en quatre; comme le note l’affiche originale, de nombreuses personnes manquent de confiance en elles face aux monades. Les caractérisations de la théorie des catégories chargées d'opérations simples dans le jargon vont à l'encontre du développement d'un sentiment de confiance et de compréhension.
Eric Lippert
2
Votre conseil est donc de remplacer Nullpar Nullable+ du code standard? :)
Eric Duminil
3
@ Claude: Vous devriez lire mon tutoriel. Une monade est un type générique qui suit certaines règles et permet de lier une chaîne d'opérations. Dans ce cas, il Measurement<T>s'agit du type monadique.
Eric Lippert
5
@daboross: Bien que je sois d'accord sur le fait que les monades à états sont un bon moyen d'introduire des monades, je ne pense pas que le fait de porter l'état comme étant la chose qui caractérise une monade. Je pense au fait que vous pouvez relier une séquence de fonctions est une chose convaincante; l'état d'esprit n'est qu'un détail de la mise en œuvre.
Eric Lippert
18

Je pense que dans ce cas, une variation sur un modèle d'objet nul serait utile:

public class Measurement
{
    private int value;
    private bool isUnknown = false;
    private bool isMissing = false;

    private Measurement() { }
    public Measurement(int value) { this.value = value; }

    public int Value {
        get {
            if (!isUnknown && !isMissing)
            {
                return this.value;
            }
            throw new SomeException("...");
        }                   
    }

    public static readonly Measurement Unknown = new Measurement
    {
        isUnknown = true
    };

    public static readonly Measurement Missing = new Measurement
    {
        isMissing = true
    };
}

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 de nulls. Je ne suis pas une personne de Java, mais je crois comprendre que celui-ci OptionalIntest similaire et que d'autres langages ont probablement leurs propres installations pour représenter un Optionaltype.

Maciej Stachowski
la source
6
L’implémentation la plus courante de ce modèle concerne l’héritage. Il pourrait y avoir un cas pour deux sous-classes: MissingMeasurement et UnknownMeasurement. Ils pourraient implémenter ou remplacer des méthodes dans la classe de mesure parent. +1
Greg Burghardt
2
N'est-ce pas le motif du modèle d'objet nul que vous n'échouez pas sur des valeurs non valides, mais que vous ne faites rien?
Chris Wohlert
2
@ChrisWohlert dans ce cas, l’objet n’a pas vraiment de méthode à part le Valuegetter, qui doit absolument échouer car vous ne pouvez pas convertir un Unknownback en un int. Si la mesure avait une SaveToDatabase()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).
Maciej Stachowski
3
@ MaciejStachowski Ouais, je ne dis pas que cela ne devrait rien faire, je dis que le modèle d'objet nul n'est pas un bon choix. Votre solution pourrait être bonne, mais je n’appellerais pas cela le modèle d’objet nul .
Chris Wohlert
14

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.

class Measurement {
    public bool IsEmpty;
    public bool IsKnown;
    public int Value {
        get {
            if(!IsEmpty && IsKnown) return _value;
            throw new Exception("NaN");
            }
        }
}

Précision importante:

Vous pouvez répondre aux exigences mathématiques en surchargeant les opérateurs de la classe.

public static Measurement operator+ (Measurement a, Measurement b) {
    if(a.IsEmpty) { return b; }
    ...etc
}
Ewan
la source
10
@KakturusOption<Option<Int>>
Bergi
5
@Bergi Vous ne pouvez pas penser que ce soit même acceptable à distance ..
BlueRaja - Danny Pflughoeft Le
8
@ BlueRaja-DannyPflughoeft En fait, il correspond assez bien à la description des PO, qui possède également une structure imbriquée. Pour devenir acceptable, nous introduirions bien sûr un alias de type approprié (ou "newtype") - mais un type Measurement = Option<Int>résultat pour un résultat qui est un entier ou une lecture vide convient, de même Option<Measurement>qu'une mesure pour une mesure qui aurait été prise ou non. .
Bergi
7
@arp "Entiers près de NaN"? Pourriez-vous expliquer ce que vous entendez par là? Il semble quelque peu contre-intuitif de dire qu'un nombre est "proche" du concept même de quelque chose qui n'est pas un nombre.
Nic Hartley
3
@Nic Hartley Dans notre système, un groupe de ce qui aurait "naturellement" été le plus petit nombre entier négatif possible était réservé en tant que NaN. Nous avons utilisé cet espace pour coder diverses raisons pour lesquelles ces octets représentaient autre chose que des données légitimes. (C'était il y a des décennies et j'ai peut-être fuzzé certains détails, mais il y avait certainement un ensemble de bits que vous pouviez mettre dans une valeur entière pour le faire jeter NaN si vous essayiez de faire des calculs avec cela.
arp
11

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

Federico Poloni
la source
6

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:

var value = Measurement.of(2);
out.println(value.map(x -> x * 2));

var empty = Measurement.empty();
out.println(empty.map(x -> x * 2));

var unknown = Measurement.unknown();
out.println(unknown.map(x -> x * 2));

impression

Value(4)
Empty()
Unknown()

( Mise en œuvre complète en tant qu'essentiel .)

Un langage ou une bibliothèque FP fournit d'autres outils tels que Try(aka Maybe) (un objet contenant une valeur ou une erreur) et Either(un objet contenant une valeur de réussite ou une valeur d'échec), qui pourraient également être utilisés ici.

David Moles
la source
2

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:

  • Types de somme
  • Syndicats discriminés
  • Objets ou structures contenant une énumération représentant le résultat de l'opération et un champ pour le résultat
  • Chaînes magiques ou nombres magiques impossibles à obtenir avec un fonctionnement normal
  • Exceptions, dans les langues dans lesquelles cette utilisation est idiomatique
  • Se rendre compte qu’il n’ya aucune valeur à différencier ces deux scénarios et à utiliser null
Gremlin de fer
la source
2

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

mickeyf_supports_Monica
la source
Upvote pour mentionner "faire quelque chose."
barbecue
4
Certaines personnes remarqueront peut-être que cela pose les mêmes problèmes que NULL, à savoir qu’il faut passer des contrôles NULL aux contrôles "inconnu" et "manquant", tout en conservant le plantage au moment de l’exécution pour la corruption de données silencieuse et chanceuse. le malheur en tant que seul indicateur que vous avez oublié un chèque. Même les contrôles NULL manquants ont l’avantage que les linters peuvent les attraper, mais cela les perd. Il ajoute une distinction entre "inconnu" et "manquant", donc, il bat NULL là-bas ...
8bittree
2

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

  • -1 = inconnu,
  • -2 = impossible à mesurer,
  • -3 = a refusé de répondre,
  • -4 = connu mais confidentiel,
  • -5 = varie en fonction de la phase de la lune, voir tableau 5a,
  • -6 = quatre dimensions, mesures indiquées dans le titre,
  • -7 = erreur de lecture du système de fichiers,
  • -8 = réservé pour une utilisation future,
  • -9 = carré / cube si Y est identique à X,
  • -10 = est un écran de contrôle et n'utilise donc pas les mesures X, Y: utilisez X comme diagonale de l'écran,
  • -11 = a écrit les mesures au verso d'un reçu et il a été blanchi pour le rendre illisible mais je pense que c'était 5 ou 17
  • -12 = ... vous avez l'idée.

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:

  1. Si vous avez des valeurs non valides, vous ne pouvez pas éviter de vérifier ces valeurs à tout moment dans le code où vous gérez les valeurs.
  2. Si vous retournez autre chose qu'un int, vous ne retournez pas un int, vous ne pouvez donc pas le traiter comme un int. La surcharge d'opérateur vous permet de faire semblant , mais c'est juste faire semblant.
  3. Un int avec des nombres magiques (y compris NULL, NAN, Inf ...) n'est plus vraiment un int, c'est une structure de pauvre.
  4. Éviter les valeurs nulles ne rendra pas le code plus robuste, il masquera simplement les problèmes liés aux ints ou les déplacera dans une structure complexe de gestion des exceptions.
Dewi Morgan
la source
1

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ématiques math.nanet cela restera math.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:

  • avec une structure de données wrapper, étant donné que les informations numériques concernent généralement une entité et ont un temps de mesure. Le wrapper peut redéfinir les méthodes magiques de __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.
  • avec une valeur sentinelle (comme votre Unknownou -1 / -2) et une instruction if
  • avec un drapeau booléen séparé
  • avec une structure de données paresseuse: votre fonction effectue une opération sur la structure, puis renvoie la fonction la plus externe qui a besoin du résultat réel, puis évalue la structure de données paresseuse.
  • avec un pipeline d'opérations paresseux - similaire au précédent, mais celui-ci peut être utilisé sur un ensemble de données ou une base de données
noɥʇʎԀʎzɐɹƆ
la source
1

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

Missingpeut être représenté comme math.nan
Unknownpeut être représenté commeNone

Si vous avez plusieurs valeurs, vous pouvez filter()uniquement appliquer l'opération à des valeurs qui ne sont pas Unknownou Missing, 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, Missingpourrait augmenter MissingExceptionet Unknownpourrait augmenter UnknownExceptionlorsque des opérations sont effectuées sur celui-ci.

@suppressUnknown(value=Unknown) # if an UnknownException is raised, return this value instead
@suppressMissing(value=Missing)
def sigmoid(value):
    ...

L'avantage de cette approche est que les propriétés de Missinget Unknownne 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.

sigmoidpeut toujours appeler sin, même s'il ne s'attend pas à un Missingou Unknown, puisque sigmoidle décorateur attrapera l'exception.

noɥʇʎԀʎzɐɹƆ
la source
1
Je me demande quel est l’intérêt d’afficher deux réponses à la même question (c’est votre réponse précédente , vous n’avez rien qui cloche?)
Gnat
@gnat Cette réponse explique pourquoi cela ne devrait pas être fait comme le montre l'auteur. Je ne voulais pas m'embrouiller à intégrer deux réponses avec des idées différentes. Il est simplement plus facile d'écrire deux réponses pouvant être lues indépendamment. . Je ne comprends pas pourquoi vous vous souciez tellement du raisonnement sans danger de quelqu'un d'autre.
nozɐɹƆ
0

Supposons récupérer le nombre de processeurs sur un serveur. Si le serveur est éteint ou mis au rebut, cette valeur n'existe tout simplement pas. Ce sera une mesure qui n’a aucun sens (peut-être que "manquant" / "vide" ne sont pas les meilleurs termes). Mais la valeur est "connue" pour être absurde. Si le serveur existe, mais que le processus d'extraction de la valeur se bloque, sa mesure est valide, mais échoue, ce qui entraîne une valeur "inconnue".

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 que DataSourceUnavailableExceptionou SpectacularFailureToGetDataExceptionrespectivement). 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) et get_measurement()ne doit renvoyer un intmessage que s'il parvient à récupérer les données à partir des données. source - et vous savez que le intest 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.

TheHinsinator
la source
0

Les réponses données sont correctes, mais ne reflètent toujours pas la relation hiérarchique entre valeur, vide et inconnu.

  • Le plus haut vient inconnu .
  • Ensuite, avant d'utiliser une valeur vide, il faut d'abord clarifier.
  • Enfin vient la valeur à calculer avec.

Ugly (pour son abstraction défaillante), mais pleinement opérationnel serait (en Java):

Optional<Optional<Integer>> unknowableValue;

unknowableValue.ifPresent(emptiableValue -> ...);
Optional<Integer> emptiableValue = unknowableValue.orElse(Optional.empty());

emptiableValue.ifPresent(value -> ...);
int value = emptiableValue.orElse(0);

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.

Joop Eggen
la source
0

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.

  • il y a aussi le cas où nous avons plusieurs variables NA pour une seule entrée ("imputation multiple"). Exemple: si je ne connais pas l'âge, le code postal, le niveau d'éducation ou le revenu d'une personne, il est plus difficile d'imputer son revenu.

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

smci
la source
0

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.

x <- c(1, 2, NA, 4, "")

Ensuite, vous pouvez voir quelles valeurs manquent.

is.na(x)

Et alors le résultat sera;

FALSE FALSE  TRUE FALSE FALSE

Comme vous pouvez le voir, il ""ne manque pas. Vous pouvez menacer ""comme inconnu. Et NAmanque.

ilhan
la source
@Hulk, quels autres langages fonctionnels prennent en charge les valeurs manquantes? Même s'ils supportent les valeurs manquantes, je suis sûr que vous ne pouvez pas les utiliser avec des méthodes statistiques dans une seule ligne de code.
ilhan
-1

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()/ similaires unknown()sur l’ensemble de votre projet.

Edward
la source
4
Cela signifie que vous devrez surcharger tous les opérateurs
pipe