Considérez ceci comme une question "académique". Je me demandais comment éviter les NULL de temps en temps et c'est un exemple où je ne peux pas trouver de solution satisfaisante.
Supposons que je stocke les mesures là où la mesure est parfois 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 un certain laps de temps, une requête sur une mesure dans cette période de temps pourrait renvoyer 3 types de réponses:
0
)Précision importante:
En supposant que vous disposiez d'une fonction get_measurement()
renvoyant l'une des valeurs "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 instructions print
n'a provoqué d'exceptions (car aucun NULL n'a été utilisé). Ainsi, les valeurs vides et inconnues se propageraient si nécessaire et la vérification si une valeur est réellement "inconnue" ou "vide" pourrait être retardée jusqu'à ce qu'elle soit vraiment nécessaire (comme stocker/sérialiser la valeur quelque part).
Side-Note: La raison pour laquelle j'aimerais éviter les NULLs, c'est principalement un casse-tête. Si je veux faire avancer les choses, je ne suis pas opposé à l'utilisation de NULL, mais j'ai trouvé que les éviter peut rendre le code beaucoup plus robuste dans certains cas.
La façon courante de le faire, au moins avec les langages fonctionnels, est d'utiliser une union discriminée. Il s'agit alors d'une valeur faisant partie d'un entier valide, d'une valeur indiquant "manquant" ou d'une valeur indiquant "inconnu". En F #, cela pourrait ressembler à quelque chose comme:
type Measurement =
| Reading of value : int
| Missing
| Unknown of value : RawData
Une valeur Measurement
sera alors un Reading
, avec une valeur int, ou un Missing
, ou un Unknown
avec les données brutes comme value
(si nécessaire).
Cependant, si vous n'utilisez pas un langage qui prend en charge les syndicats discriminés, ou leur équivalent, ce modèle ne vous sera probablement pas très utile. Donc là, vous pouvez par exemple utiliser une classe avec un champ enum qui indique lequel des trois contient les données correctes.
Si vous ne savez pas déjà ce qu'est une monade, aujourd'hui serait une excellente journée 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 "peut-être monade", également appelée Nullable<T>
En C # et Optional<T>
Dans 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 cela, 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 vous avez terminé. 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 m
est Empty<int>
alors asString
est Empty<String>
, excellent.
De même, si nous avons
Measurement<int> First()
et
Measurement<double> Second(int i);
on peut alors combiner deux mesures:
Measurement<double> d = First().Bind(Second);
et encore, si First()
est Empty<int>
alors d
est Empty<double>
et ainsi de suite.
L'étape clé consiste à obtenir l'opération de liaison correcte . Réfléchissez bien.
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 depuis ou vers int
, et si vous voulez un comportement de type NaN, vous pouvez également implémenter vos propres opérateurs arithmétiques de sorte que par exemple. Measurement.Unknown * 2 == Measurement.Unknown
.
Cela dit, C # est Nullable<int>
implémente tout cela, la seule mise en garde étant que vous ne pouvez pas différencier les différents types de null
. Je ne suis pas une personne Java, mais je crois savoir que OptionalInt
de Java est similaire, et d'autres langages ont probablement leurs propres installations pour représenter un type Optional
.
Si vous DEVEZ littéralement utiliser un entier, il n'y a qu'une seule 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 juste besoin de l'int pour des mesures "réelles", créez une structure de données plus compliquée
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 atteindre l'exigence mathématique en surchargeant les opérateurs pour la classe
public static Measurement operator+ (Measurement a, Measurement b) {
if(a.IsEmpty) { return b; }
...etc
}
Si vos variables sont des nombres à virgule flottante, IEEE754 (la norme de nombre à virgule flottante qui est prise en charge par la plupart des processeurs et des langages modernes) a votre dos: c'est une fonctionnalité peu connue, mais la norme n'en définit pas un, mais ne famille entière de NaN (pas un nombre) valeurs, qui peuvent être utilisées pour des significations arbitraires définies par l'application. Dans les flottants simple précision, par exemple, vous avez 22 bits libres que vous pouvez utiliser pour distinguer entre 2 ^ {22} types de valeurs non valides.
Normalement, les interfaces de programmation n'exposent qu'une seule d'entre elles (par exemple, nan
) de Numpy; Je ne sais pas s'il existe un moyen intégré de générer les autres autres que la manipulation explicite de bits, mais il s'agit simplement d'écrire quelques routines de bas niveau. (Vous en aurez également besoin pour les différencier, car, par conception, a == b
Renvoie toujours false lorsque l'un d'eux est un NaN.)
Il est préférable de les utiliser que de réinventer votre propre "numéro magique" pour signaler des données invalides, car elles se propagent correctement et signalent l'invalidité: par exemple, vous ne risquez pas de vous tirer une balle dans le pied si vous utilisez un average()
fonction et oubliez de vérifier vos valeurs spéciales.
Le seul risque est que les bibliothèques ne les prennent pas correctement en charge, car elles sont une fonctionnalité assez obscure: par exemple, une bibliothèque de sérialisation peut les `` aplatir '' toutes au même nan
(ce qui lui semble équivalent dans la plupart des cas).
Après réponse de David Arno , vous pouvez faire quelque chose comme une union discriminée dans la POO, et dans un style fonctionnel objet tel que celui offert par Scala, par Java 8 types fonctionnels, ou une Java FP bibliothèque telle que Vavr ou Fugue il semble assez naturel de é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()
( Implémentation complète sous forme de Gist .)
Un FP langage ou bibliothèque fournit d'autres outils comme Try
(aka Maybe
) (un objet qui contient soit une valeur, soit une erreur) et Either
(un objet qui contient soit une valeur de réussite soit une valeur d'échec) qui pourrait également être utilisé ici.
La solution idéale à votre problème dépendra de la raison pour laquelle 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 les "processus en aval" dans ce cas n'excluent pas les opérateurs humains ou les autres développeurs.
Le simple fait de proposer une "deuxième version" 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 fiez plutôt à des hypothèses contextuelles sur la source des mauvais comportements du code en aval, j'appellerais cette mauvaise architecture.
Si vous en savez suffisamment pour faire la distinction entre un motif d'échec et un échec sans raison connue, et que ces informations vont éclairer les comportements futurs, vous devez communiquer ces connaissances en aval ou les gérer en ligne.
Quelques modèles pour gérer cela:
null
Si je voulais "faire quelque chose" plutôt qu'une solution élégante, le hack rapide et sale serait simplement d'utiliser les chaînes "inconnu", "manquant" et "représentation 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 dans au moins certaines circonstances, tout à fait adéquat. (Je forme maintenant un pool de paris sur le nombre de downvotes ...)
The Gist si la question semble être "Comment puis-je retourner deux informations non liées à partir d'une méthode qui retourne un seul int? Je ne veux jamais vérifier mes valeurs de retour, et les valeurs nulles sont mauvaises, ne les utilisez pas."
Voyons ce que vous voulez réussir. Vous passez soit un int, soit un non-int justification pour laquelle 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 toute liste s'allongera. La possibilité de spécifier d'autres justifications est tout à fait logique.
Au départ, cela semble donc être un bon argument pour lever une exception.
Lorsque vous voulez dire à l'appelant quelque chose de spécial qui n'est pas dans le type de retour, les exceptions sont souvent le système approprié: les exceptions ne sont pas seulement pour les états d'erreur, et vous permettent de renvoyer beaucoup de contexte et de justification pour expliquer pourquoi vous pouvez simplement 't int aujourd'hui.
Et c'est le SEUL système qui vous permet de renvoyer des entiers valides garantis et de garantir que chaque opérateur int et chaque méthode qui accepte des ints peuvent accepter la valeur de retour de cette méthode sans jamais avoir besoin de vérifier les valeurs invalides comme null ou les valeurs magiques.
Mais les exceptions ne sont vraiment une solution valable que si, comme son nom l'indique, il s'agit d'un cas exceptionnel , et non du cours normal des affaires.
Et un try/catch et un gestionnaire est tout autant un passe-partout qu'une vérification nulle, ce à quoi on s'est opposé en premier lieu.
Et si l'appelant ne contient pas le try/catch, alors l'appelant de l'appelant doit, et ainsi de suite.
Une deuxième passe naïve consiste à dire "C'est une mesure. Des mesures de distance négatives sont peu probables." Donc, pour une mesure Y, vous pouvez simplement avoir des constantes pour
C'est ainsi que cela se fait dans de nombreux anciens systèmes C, et même dans les systèmes modernes où il existe une véritable contrainte à int, et vous ne pouvez pas l'encapsuler dans une structure ou une monade d'un certain type.
Si les mesures peuvent être négatives, vous agrandissez simplement votre type de données (par exemple, long int) et les valeurs magiques sont supérieures à la plage de l'int, et commencez idéalement 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. Par exemple, typage strict, maintenabilité et conformité aux attentes.
Dans notre troisième tentative, nous examinons donc les cas dans lesquels le cours normal des affaires a des valeurs non int. 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 la mauvaise approche.
Dans ce cas, cela semble être un bon cas pour une structure qui passe l'int, et la justification. Encore une fois, cette justification peut simplement être une constante comme celle ci-dessus, mais au lieu de les deux dans le même int, vous les stockez en tant que parties distinctes d'une structure. Initialement, 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 pour voir si l'int est valide, puis retirez et utilisez la partie int si la justification nous le permet.
C'est là que vous devez enquêter sur votre raisonnement derrière "ne pas utiliser null".
Comme les exceptions, null est censé signifier un état exceptionnel.
Si un appelant appelle cette méthode et ignore complètement la partie "justification" de la structure, s'attendant à un nombre sans aucune gestion d'erreur, et il obtient un zéro, alors il traitera le zéro comme un nombre et se trompera. S'il obtient un nombre magique, il le traitera comme un nombre et se trompera. Mais s'il obtient une valeur nulle, il tombera , comme il devrait le faire.
Donc, chaque fois que vous appelez cette méthode, vous devez vérifier sa valeur de retour, mais vous gérez les valeurs non valides, qu'elles soient dans la bande ou hors bande, essayez/rattrapez, vérifiez la structure pour un composant "justification", vérifiez l'int pour un nombre magique, ou vérifier un int pour un nul ...
L'alternative, pour gérer la multiplication d'une sortie qui pourrait contenir un int invalide et une justification comme "Mon chien a mangé cette mesure", est de surcharger l'opérateur de multiplication pour cette structure.
... Et puis surchargez tous les autres opérateurs de votre application susceptibles d'être appliqués à ces données.
... Et puis surcharger toutes les méthodes qui pourraient prendre des pouces.
... Et tous de ces surcharges devront toujours contenir des vérifications pour les entiers invalides, juste pour que vous puissiez traiter le renvoie le type de cette méthode comme s'il s'agissait toujours d'un entier valide au moment où vous l'appelez.
La prémisse d'origine est donc fausse de diverses manières:
Je ne comprends pas la prémisse de votre question, mais voici la réponse nominale. Pour manquant ou vide, vous pouvez faire math.nan
(Pas un nombre). Vous pouvez effectuer toutes les opérations mathématiques sur math.nan
et il restera math.nan
.
Vous pouvez utiliser None
(null de Python) pour une valeur inconnue. De toute façon, vous ne devriez pas manipuler une valeur inconnue, et certains langages (Python n'en fait pas partie) ont des opérateurs null spéciaux afin que l'opération ne soit effectuée que si la valeur est non nulle, sinon la valeur reste nulle.
D'autres langages ont des clauses de garde (comme Swift ou Ruby), et Ruby a un retour anticipé conditionnel).
J'ai vu cela résolu dans Python de différentes manières:
__mult__
afin qu'aucune exception ne soit levée lorsque vos valeurs inconnues ou manquantes apparaissent. Numpy et pandas pourraient avoir une telle capacité en eux.Unknown
ou -1/-2) et une instruction ifLa façon dont la valeur est stockée en mémoire dépend du langage et des détails d'implémentation. Je pense que vous voulez dire comment l'objet doit se comporter avec le programmeur. (Voici comment je lis la question, dites-moi si je me trompe.)
Vous avez déjà proposé une réponse à cela dans votre question: utilisez votre propre classe qui accepte toute opération mathématique et se retourne sans lever d'exception. Vous dites que vous le voulez parce que vous voulez éviter les contrôles nuls.
Solution 1: n'évitez pas les contrôles nuls
Missing
peut être représenté par math.nan
Unknown
peut être représenté par None
Si vous avez plusieurs valeurs, vous pouvez filter()
pour n'appliquer l'opération qu'aux valeurs qui ne sont pas Unknown
ou Missing
, ou toutes les valeurs que vous souhaitez ignorer pour la fonction.
Je ne peux pas imaginer un scénario où vous avez besoin d'un contrôle nul sur une fonction qui agit sur un seul scalaire. Dans ce cas, il est bon de forcer les vérifications nulles.
Solution 2: tilisez un décorateur qui détecte les exceptions
Dans ce cas, Missing
peut augmenter MissingException
et Unknown
peut augmenter UnknownException
lorsque 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 Missing
et Unknown
ne sont supprimées que lorsque vous demandez explicitement qu'elles soient supprimées. Un autre avantage est que cette approche est auto-documentée: chaque fonction montre si elle attend ou non une inconnue ou une manquante et comment la fonction.
Lorsque vous appelez une fonction ne s'attend pas à ce qu'un manquant obtienne un manquant, la fonction augmentera immédiatement, vous montrant exactement où l'erreur s'est produite au lieu d'échouer et de propager silencieusement un manquant dans la chaîne d'appels. Il en va de même pour Unknown.
sigmoid
peut toujours appeler sin
, même s'il n'attend pas de Missing
ou Unknown
, puisque le décorateur de sigmoid
interceptera l'exception .
Supposons que l'on récupère le nombre de CPU d'un serveur. Si le serveur est éteint ou a été 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 de récupération de la valeur se bloque, sa mesure est valide, mais échoue, entraînant une valeur "inconnue".
Ces deux conditions ressemblent à des erreurs, donc je dirais que la meilleure option ici est simplement d'avoir get_measurement()
lever immédiatement ces deux exceptions (telles que DataSourceUnavailableException
ou SpectacularFailureToGetDataException
, 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 réessayant dans ce dernier cas), et get_measurement()
n'a qu'à retourner un int
dans le cas où il peut obtenir avec succès les données de la source de données - et vous savez que le int
est valide.
Si votre situation ne prend pas en charge les exceptions ou ne peut pas en faire grand cas, alors une bonne alternative consiste à utiliser des codes d'erreur, peut-être 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é comme valeur de retour.
R a un support de valeur manquante intégré. https://medium.com/coinmonks/dealing-with-missing-data-using-r-3ae428da2d17
Edit: parce que j'ai été sous-voté, je vais expliquer un peu.
Si vous allez traiter des statistiques, je vous recommande d'utiliser un langage statistique tel que R car R est écrit par des statisticiens pour des statisticiens. Les valeurs manquantes sont un sujet tellement important qu'elles vous enseignent tout un semestre. Et il n'y a de gros livres que sur les valeurs manquantes.
Vous pouvez cependant vous souhaitez marquer vos données manquantes, comme un point ou "manquant" ou autre chose. Dans R, vous pouvez définir ce que vous entendez par manquer. Vous n'avez pas besoin de les convertir.
La manière normale de définir la valeur manquante est de les marquer comme NA
.
x <- c(1, 2, NA, 4, "")
Ensuite, vous pouvez voir quelles valeurs manquent;
is.na(x)
Et puis le résultat sera;
FALSE FALSE TRUE FALSE FALSE
Comme vous pouvez le voir ""
n'est pas manquant. Vous pouvez menacer ""
comme inconnu. Et NA
est manquant.
Les réponses données sont bonnes, mais ne reflètent toujours pas la relation hiérarchique entre la valeur, vide et inconnue.
Moche (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 système de type Nice sont meilleurs.
En fait: Le vide/manquant et inconnu * non -les valeurs semblent plutôt faire partie d'un état de processus, d'un pipeline de production. Comme les cellules de feuille de calcul Excel avec des formules référençant d'autres cellules. Là, on penserait peut-être à stocker des lambdas contextuelles. Changer une cellule réévaluerait toutes les cellules dépendantes récursivement.
Dans ce cas, une valeur int serait obtenue par un fournisseur int. Une valeur vide donnerait un fournisseur int levant une exception vide, ou évaluant à vide (récursivement vers le haut). Votre formule principale relierait toutes les valeurs et renverrait éventuellement une valeur vide (valeur/exception). Une valeur inconnue désactiverait l'évaluation en lançant une exception.
Les valeurs seraient probablement observables, comme une propriété liée Java, informant les auditeurs du changement.
En bref: Le schéma récurrent de besoin de valeurs avec des états supplémentaires vides et inconnus semble indiquer qu'une feuille de calcul plus semblable à un modèle de données de propriétés liées pourrait être meilleure.
Oui, le concept de plusieurs types de NA différents existe dans certaines langues; plus encore dans les statistiques, où il est plus significatif (à savoir l'énorme distinction entre Missing-At-Random, Missing-Completely-At-Random, Missing-Not-At- Aléatoire ).
si nous mesurons uniquement les longueurs des widgets, il n'est pas crucial de faire la distinction entre "défaillance du capteur" ou "coupure de courant" ou "défaillance du réseau" (bien que le "débordement numérique" transmette des informations)
mais par exemple l'exploration de données ou une enquête, demandant par exemple aux répondants leur revenu ou leur statut sérologique, un résultat de "Inconnu" est distinct de "Refuser de répondre", et vous pouvez voir que nos hypothèses antérieures sur la façon d'imputer ces derniers auront tendance à être différentes des premières. Ainsi, des langages comme SAS prennent en charge plusieurs types de NA différents; le langage R ne le fait pas, mais les utilisateurs doivent très souvent le contourner; 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 à usage général qui ne les prennent pas en charge, les gens piratent généralement des éléments tels que NaN à virgule flottante (nécessite la conversion d'entiers), des énumérations ou des sentinelles (par exemple 999 ou -1000) pour les nombres entiers ou valeurs catégoriques. Habituellement, il n'y a pas de réponse très claire, désolé.