Jak przechowywać „nieznane” i „brakujące” wartości w zmiennej, zachowując różnicę między „nieznanymi” a „brakującymi”?

57

Rozważ to pytanie „akademickie”. Zastanawiałem się od czasu do czasu, aby uniknąć NULL-ów i jest to przykład, w którym nie mogę znaleźć zadowalającego rozwiązania.


Załóżmy, że przechowuję pomiary tam, gdzie czasami wiadomo, że pomiar jest niemożliwy (lub jego brak). Chciałbym przechowywać tę „pustą” wartość w zmiennej, unikając NULL. Innym razem wartość może być nieznana. Tak więc, mając pomiary dla określonego przedziału czasowego, zapytanie o pomiar w tym okresie mogłoby zwrócić 3 rodzaje odpowiedzi:

  • Rzeczywisty pomiar w tym czasie (na przykład dowolna wartość liczbowa, w tym 0)
  • „Brakująca” / „pusta” wartość (tzn. Dokonano pomiaru i wiadomo, że w tym momencie wartość jest pusta).
  • Nieznana wartość (tzn. W tym momencie nie wykonano żadnego pomiaru. Może być pusta, ale może to być dowolna inna wartość).

Ważne wyjaśnienie:

Zakładając, że masz funkcję get_measurement()zwracającą jedną z „pustych”, „nieznanych” i wartość typu „liczba całkowita”. Posiadanie wartości liczbowej oznacza, że ​​pewne operacje można wykonać na wartości zwracanej (mnożenie, dzielenie, ...), ale użycie takich operacji na wartości NULL spowoduje awarię aplikacji, jeśli nie zostanie złapana.

Chciałbym móc pisać kod, unikając kontroli NULL, na przykład (pseudokod):

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

Zauważ, że żadna z printinstrukcji nie spowodowała wyjątków (ponieważ nie użyto żadnych wartości NULL). Tak więc puste i nieznane wartości byłyby propagowane w razie potrzeby, a sprawdzenie, czy wartość jest w rzeczywistości „nieznana” czy „pusta”, może być opóźnione do momentu, gdy jest to naprawdę konieczne (jak przechowywanie / szeregowanie wartości gdzieś).


Uwaga dodatkowa: Powodem, dla którego chciałbym unikać wartości NULL, jest przede wszystkim łamigłówka. Jeśli chcę załatwić sprawę, nie jestem przeciwny używaniu wartości NULL, ale stwierdziłem, że unikanie ich może w niektórych przypadkach uczynić kod o wiele bardziej niezawodnym.

ekshuma
źródło
19
Dlaczego chcesz odróżnić „pomiar wykonany, ale pusta wartość” od „bez pomiaru”? Co właściwie oznacza „pomiar wykonany, ale pusta wartość”? Czy czujnik nie wygenerował prawidłowej wartości? W takim razie czym różni się to od „nieznanego”? Nie będziesz w stanie cofnąć się w czasie i uzyskać prawidłowej wartości.
DaveG
3
@DaveG Załóżmy, że pobrano liczbę procesorów na serwerze. Jeśli serwer jest wyłączony lub został złomowany, ta wartość po prostu nie istnieje. Będzie to pomiar, który nie ma sensu (być może „brak” / „pusty” nie są najlepszymi terminami). Ale wartość ta jest „znana” jako nonsensowna. Jeśli serwer istnieje, ale proces pobierania wartości ulega awarii, pomiar jest prawidłowy, ale kończy się niepowodzeniem, co powoduje „nieznaną” wartość.
ekshuma
2
@ exhuma W takim razie opisałbym to jako „nie dotyczy”.
Vincent
6
Z ciekawości, jaki pomiar wykonujesz, gdy „pusty” nie jest po prostu równy zeru dowolnej skali? „Nieznany” / „brakujący” Widzę, że jest przydatny, np. Jeśli czujnik nie jest podłączony lub jeśli nieprzetworzone dane wyjściowe czujnika są śmieciami z tego czy innego powodu, ale „pusty” w każdym przypadku, o którym myślę, może być bardziej konsekwentny reprezentowane przez 0, []lub {}(odpowiednio skalar 0, pusta lista i pusta mapa). Ponadto ta „brakująca” / „nieznana” wartość jest w zasadzie dokładnie tym, do czego nullsłuży - oznacza, że może tam być obiekt, ale nie ma go.
Nic Hartley,
7
Niezależnie od tego, jakiego rozwiązania użyjesz, zadaj sobie pytanie, czy występują problemy podobne do tych, które sprawiły, że chcesz przede wszystkim wyeliminować NULL.
Ray

Odpowiedzi:

85

Częstym sposobem na to, przynajmniej w językach funkcjonalnych, jest stosowanie dyskryminowanego związku. Jest to zatem wartość należąca do poprawnej wartości int, wartość oznaczająca „brak” lub wartość oznaczająca „nieznany”. W języku F # może to wyglądać mniej więcej tak:

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

MeasurementWartość będzie wtedy Reading, o wartości int albo A Missing, albo Unknownz surowych danych, jak value(w razie potrzeby).

Jeśli jednak nie używasz języka, który obsługuje dyskryminowane związki lub ich odpowiedniki, ten wzór prawdopodobnie nie będzie dla ciebie zbyt użyteczny. Można więc na przykład użyć klasy z polem wyliczającym, które wskazuje, która z tych trzech zawiera prawidłowe dane.

David Arno
źródło
7
możesz robić typy sum w językach OO, ale jest sporo płyty kotła, aby działały stackoverflow.com/questions/3151702/…
jk.
11
„[W językach języków niefunkcjonalnych] ten wzór prawdopodobnie nie przyda ci się zbytnio” - jest to dość powszechny wzór w OOP. GOF ma odmianę tego wzorca, a języki takie jak C ++ oferują natywne konstrukcje do jego kodowania.
Konrad Rudolph
14
@jk. Tak, nie liczą się (tak myślę, że tak; są po prostu bardzo źli w tym scenariuszu z powodu braku bezpieczeństwa). Miałem na myśli std::variant(i jego duchowych poprzedników).
Konrad Rudolph,
2
@Ewan Nie, mówi „Pomiar to typ danych, który jest albo… albo…”.
Konrad Rudolph
2
@DavidArno Cóż, nawet bez DU istnieje w kanale OOP rozwiązanie „kanoniczne”, które ma mieć nadklasę wartości z podklasami dla prawidłowych i nieprawidłowych wartości. Ale to prawdopodobnie posuwa się za daleko (iw praktyce wydaje się, że większość baz kodu unika polimorfizmu podklasy na rzecz flagi w tym celu, jak pokazano w innych odpowiedziach).
Konrad Rudolph
58

Jeśli jeszcze nie wiesz, co to jest monada, dzisiejszy dzień byłby świetnym dniem do nauki. Mam tutaj delikatne wprowadzenie dla programistów OO:

https://ericlippert.com/2013/02/21/monads-part-one/

Twój scenariusz jest małym rozszerzeniem „może monady”, znanej również jako Nullable<T>C # i Optional<T>w innych językach.

Załóżmy, że masz abstrakcyjny typ reprezentujący monadę:

abstract class Measurement<T> { ... }

a następnie trzy podklasy:

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

Potrzebujemy wdrożenia 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 ...
  }

Z tego możesz napisać tę uproszczoną wersję Binda:

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

A teraz gotowe. Masz Measurement<int>pod ręką. Chcesz go podwoić:

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

I podążaj za logiką; jeśli mjest, Empty<int>to asStringjest Empty<String>doskonałe.

Podobnie, jeśli mamy

Measurement<int> First()

i

Measurement<double> Second(int i);

następnie możemy połączyć dwa pomiary:

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

i znowu, jeśli First()jest, Empty<int>to djest Empty<double>i tak dalej.

Kluczowym krokiem jest poprawne wykonanie operacji wiązania . Zastanów się nad tym.

Eric Lippert
źródło
4
Monady (na szczęście) są znacznie łatwiejsze w użyciu niż zrozumienie. :)
Guran
11
@leftaroundabout: Właśnie dlatego, że nie chciałem rozróżniać włosów od siebie; jak zauważa oryginalny plakat, wielu ludziom brakuje pewności, jeśli chodzi o radzenie sobie z monadami. Teoria kategorii oparta na żargonie, charakteryzująca proste operacje, działa przeciwko rozwijaniu poczucia pewności siebie i zrozumienia.
Eric Lippert,
2
Więc rada jest, aby wymienić Nullz Nullable+ jakiś standardowy kod? :)
Eric Duminil
3
@ Claude: Powinieneś przeczytać mój samouczek. Monada jest typem ogólnym, który podlega pewnym regułom i zapewnia możliwość powiązania ze sobą łańcucha operacji, więc w tym przypadku Measurement<T>jest to typ monadyczny.
Eric Lippert,
5
@daboross: Chociaż zgadzam się, że stanowe monady są dobrym sposobem na wprowadzenie monad, nie uważam, aby utrzymywanie stanu było tym, co charakteryzuje monadę. Myślę, że fakt, że można połączyć ze sobą sekwencję funkcji, jest przekonujący; stan jest tylko szczegółem implementacji.
Eric Lippert,
18

Myślę, że w tym przypadku przydatna byłaby odmiana wzorca zerowego obiektu:

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
    };
}

Możesz przekształcić go w struct, przesłonić Equals / GetHashCode / ToString, dodać niejawne konwersje z lub do int, a jeśli chcesz zachowanie podobne do NaN, możesz również zaimplementować własne operatory arytmetyczne, aby np. Measurement.Unknown * 2 == Measurement.Unknown.

To powiedziawszy, C # Nullable<int>implementuje to wszystko, z jedynym zastrzeżeniem, że nie można rozróżniać różnych typów nulls. Nie jestem osobą Java, ale rozumiem, że Java OptionalIntjest podobna, a inne języki prawdopodobnie mają własne udogodnienia do reprezentowania Optionaltypu.

Maciej Stachowski
źródło
6
Najczęstszą implementacją tego wzorca, jaką widziałem, jest dziedziczenie. Mogą istnieć przypadki dwóch podklas: MissingMeasurement i UnknownMeasurement. Mogą implementować lub zastępować metody w nadrzędnej klasie Measurement. +1
Greg Burghardt
2
Czy nie ma sensu wzorca zerowego obiektu , że nie zawiodłeś na niepoprawnych wartościach, a raczej nic nie robiłeś?
Chris Wohlert
2
@ChrisWohlert w tym przypadku obiekt tak naprawdę nie ma żadnych metod oprócz Valuegettera, co absolutnie powinno zawieść, ponieważ nie można przekonwertować go z Unknownpowrotem na plik int. Jeśli pomiar miałby, powiedzmy, SaveToDatabase()metodę, to dobra implementacja prawdopodobnie nie wykonałaby transakcji, jeśli bieżący obiekt jest obiektem zerowym (albo przez porównanie z singletonem, albo zastąpienie metody).
Maciej Stachowski,
3
@MaciejStachowski Tak, nie mówię, że nie powinien nic robić, mówię, że Null Object Pattern nie jest dobrym dopasowaniem. Twoje rozwiązanie może być w porządku, ale nie nazwałbym tego Wzorzecem zerowym obiektu .
Chris Wohlert,
14

Jeśli dosłownie MUSISZ użyć liczby całkowitej, istnieje tylko jedno możliwe rozwiązanie. Użyj niektórych możliwych wartości jako „magicznych liczb”, które oznaczają „brak” i „nieznany”

np. 2 147 483 647 i 2 147 483 646

Jeśli potrzebujesz tylko int dla „rzeczywistych” pomiarów, stwórz bardziej skomplikowaną strukturę danych

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

Ważne wyjaśnienie:

Możesz spełnić wymagania matematyczne, przeciążając operatory dla klasy

public static Measurement operator+ (Measurement a, Measurement b) {
    if(a.IsEmpty) { return b; }
    ...etc
}
Ewan
źródło
10
@KakturusOption<Option<Int>>
Bergi
5
@Bergi Nie możesz chyba myśleć, że jest to nawet do przyjęcia.
BlueRaja - Danny Pflughoeft
8
@ BlueRaja-DannyPflughoeft Właściwie to całkiem pasuje do opisu PO, który ma również strukturę zagnieżdżoną. Aby uzyskać akceptację, wprowadzilibyśmy oczywiście odpowiedni alias typu (lub „nowy typ”) - ale type Measurement = Option<Int>dla wyniku, który był liczbą całkowitą lub pustym odczytem, ​​jest to w porządku, podobnie jak Option<Measurement>dla pomiaru, który mógł zostać wykonany lub nie .
Bergi,
7
@arp „Integers near NaN”? Czy możesz wyjaśnić, co przez to rozumiesz? Mówienie, że liczba jest „bliska” samej koncepcji czegoś, co nie jest liczbą, wydaje się nieco sprzeczne z intuicją.
Nic Hartley,
3
@Nic Hartley W naszym systemie grupa wartości, które „naturalnie” byłyby najniższymi możliwymi ujemnymi liczbami całkowitymi, była zarezerwowana jako NaN. Wykorzystaliśmy tę przestrzeń do kodowania różnych powodów, dla których bajty te reprezentowały coś innego niż uzasadnione dane. (to było kilkadziesiąt lat temu i mogłem sfałszować niektóre szczegóły, ale na pewno był zestaw bitów, które można było wprowadzić w liczbach całkowitych, aby rzucić NaN, jeśli spróbujesz z nim zrobić matematykę.
arp
11

Jeśli twoje zmienne są numery-zmiennoprzecinkowych, IEEE754 (pływający punkt standardowy numer, który jest obsługiwany przez większość nowoczesnych procesorów i języków) ma pleców: to mało znana funkcja, ale standard definiuje nie jeden, ale całą rodzinę z Wartości NaN (nie-liczba), które można wykorzystać do dowolnych znaczeń zdefiniowanych przez aplikację. Na przykład w pływakach o pojedynczej precyzji masz 22 wolne bity, których możesz użyć do rozróżnienia 2 ^ {22} typów niepoprawnych wartości.

Zwykle interfejsy programistyczne ujawniają tylko jeden z nich (np. Numpy nan); Nie wiem, czy istnieje wbudowany sposób generowania innych niż jawna manipulacja bitami, ale to tylko kwestia napisania kilku procedur niskiego poziomu. (Będziesz także potrzebował jednego, aby je rozróżnić, ponieważ z założenia a == bzawsze zwraca false, gdy jeden z nich jest NaN.)

Używanie ich jest lepsze niż wymyślanie własnej „magicznej liczby” w celu sygnalizowania nieprawidłowych danych, ponieważ prawidłowo się propagują i sygnalizują nieważność: na przykład nie ryzykujesz trafienia w stopę, jeśli używasz average()funkcji i zapominasz sprawdzić twoje specjalne wartości.

Jedynym ryzykiem jest to, że biblioteki nie obsługują ich poprawnie, ponieważ są dość niejasną cechą: na przykład biblioteka serializacji może „spłaszczyć” je wszystkie w ten sam sposób nan(co w większości przypadków wygląda na równoważne).

Federico Poloni
źródło
6

Postępując zgodnie z odpowiedzią Davida Arno , możesz zrobić coś w rodzaju dyskryminowanego związku w OOP, w stylu obiektowo-funkcjonalnym, takim jak Scala, typy funkcjonalne Java 8 lub biblioteka Java FP, taka jak Vavr lub Fugue , wydaje się dość naturalne napisać coś takiego:

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

druk

Value(4)
Empty()
Unknown()

( Pełna realizacja jako sedno ).

Język lub biblioteka FP zapewnia inne narzędzia, takie jak Try(aka Maybe) (obiekt zawierający wartość lub błąd) i Either(obiekt zawierający wartość sukcesu lub wartość błędu), które również mogą być tutaj użyte.

David Moles
źródło
2

Idealne rozwiązanie Twojego problemu zależy od tego, dlaczego zależy Ci na różnicy między znaną awarią a znanym niewiarygodnym pomiarem oraz na tym, jakie dalsze procesy chcesz wspierać. Uwaga: „procesy niższego szczebla” w tym przypadku nie wykluczają ludzkich operatorów ani innych programistów.

Samo wymyślenie „drugiego smaku” wartości null nie daje późniejszemu zestawowi procesów wystarczających informacji do uzyskania rozsądnego zestawu zachowań.

Jeśli zamiast tego polegasz na kontekstowych założeniach o źródle złych zachowań popełnianych przez kod źródłowy, nazwałbym tę złą architekturę.

Jeśli znasz wystarczająco dużo, aby odróżnić przyczynę niepowodzenia od awarii bez znanej przyczyny, a ta informacja będzie miała wpływ na przyszłe zachowania, powinieneś przekazać tę wiedzę w dalszej części procesu lub postępować zgodnie z nią.

Niektóre wzorce do obsługi tego:

  • Rodzaje sum
  • Dyskryminowane związki
  • Obiekty lub struktury zawierające wyliczenie reprezentujące wynik operacji i pole dla wyniku
  • Magiczne ciągi lub magiczne liczby, których nie da się osiągnąć przy normalnym działaniu
  • Wyjątki, w językach, w których takie użycie jest idiomatyczne
  • Uświadomienie sobie, że tak naprawdę nie ma żadnej wartości w rozróżnieniu między tymi dwoma scenariuszami i po prostu ich użyciu null
Żelazny Gremlin
źródło
2

Gdybym martwił się „zrobieniem czegoś”, a nie eleganckim rozwiązaniem, szybki i brudny hack polegałby na użyciu ciągów „nieznane”, „brakujące” i „ciąg reprezentujący moją wartość liczbową”, które wówczas byłyby konwertowane z ciągu i używane w razie potrzeby. Wdrożone szybciej niż napisanie tego, a przynajmniej w niektórych okolicznościach, całkowicie wystarczające. (Teraz tworzę pulę zakładów na liczbę głosów negatywnych ...)

mickeyf
źródło
Pozytywnie za wzmiankę o „zrobieniu czegoś”.
Do widzenia Pani Chipps,
4
Niektóre osoby mogą zauważyć, że cierpi to na większość problemów związanych z używaniem NULL, a mianowicie, że po prostu przełącza się z konieczności czeków NULL na potrzeby czeków „nieznanych” i „brakujących”, ale utrzymuje awarię w czasie wykonywania dla szczęśliwego, cichego uszkodzenia danych dla nieszczęście jako jedyne oznaki, że zapomniałeś czeku. Nawet brakujące kontrole NULL mają tę zaletę, że kłaczki mogą je złapać, ale to traci. Dodaje jednak rozróżnienie między „nieznanym” a „zaginionym”, więc bije tam NULL ...
8bittree 16.08.18
2

Istota, jeśli pytanie brzmi: „Jak zwrócić dwie niepowiązane informacje z metody, która zwraca jedną liczbę całkowitą? Nigdy nie chcę sprawdzać moich zwracanych wartości, a wartości null są złe, nie używaj ich”.

Spójrzmy na to, co chcesz przekazać. Zdajesz uzasadnienie int lub non-int, dlaczego nie możesz podać int. Pytanie zapewnia, że ​​będą tylko dwa powody, ale każdy, kto kiedykolwiek wyliczył enum, wie, że każda lista będzie rosła. Określenie innych uzasadnień ma sens.

Początkowo wydaje się, że może to być dobry powód do zgłoszenia wyjątku.

Jeśli chcesz powiedzieć dzwoniącemu coś wyjątkowego, co nie występuje w typie zwracanym, wyjątki są często odpowiednim systemem: wyjątki dotyczą nie tylko stanów błędów i pozwalają na zwrócenie wielu kontekstów i uzasadnień wyjaśniających, dlaczego tak po prostu możesz to jest dzisiaj.

I to jest TYLKO system, który pozwala na zwrócenie gwarantowanych poprawnych liczb całkowitych i gwarantuje, że każdy operator int i metoda, która przyjmuje liczby ints, może zaakceptować wartość zwracaną tej metody bez konieczności sprawdzania nieprawidłowych wartości, takich jak null lub magiczne wartości.

Ale wyjątki są tak naprawdę tylko właściwym rozwiązaniem, jeśli, jak sama nazwa wskazuje, jest to wyjątkowy przypadek, a nie normalny sposób prowadzenia działalności.

A try / catch i handler to tak samo płyta kontrolna jak kontrola zerowa, co było przede wszystkim przedmiotem sprzeciwu.

A jeśli dzwoniący nie zawiera try / catch, wówczas dzwoniący musi to zrobić i tak dalej.


Naiwnym drugim przejściem jest powiedzenie „To pomiar. Negatywne pomiary odległości są mało prawdopodobne”. Więc dla niektórych pomiarów Y możesz mieć po prostu stałe dla

  • -1 = nieznany,
  • -2 = niemożliwy do zmierzenia,
  • -3 = odmówił odpowiedzi,
  • -4 = znany, ale poufny,
  • -5 = zmienia się w zależności od fazy księżyca, patrz tabela 5a,
  • -6 = czterowymiarowe, wymiary podane w tytule,
  • -7 = błąd odczytu systemu plików,
  • -8 = zarezerwowane do wykorzystania w przyszłości,
  • -9 = kwadrat / sześcienny, więc Y jest takie samo jak X,
  • -10 = to ekran monitora, więc nie używaj pomiarów X, Y: użyj X jako przekątnej ekranu,
  • -11 = zapisał pomiary na odwrocie paragonu i został sprany do nieczytelności, ale myślę, że był to 5 lub 17,
  • -12 = ... masz pomysł.

Tak dzieje się w wielu starych systemach C, a nawet w nowoczesnych systemach, w których istnieje rzeczywiste ograniczenie int, a nie można go owinąć w strukturę lub monadę jakiegoś typu.

Jeśli pomiary mogą być ujemne, to po prostu powiększasz typ danych (np. Long int) i masz magiczne wartości wyższe niż zakres int, i idealnie zaczynasz od pewnej wartości, która będzie wyraźnie widoczna w debuggerze.

Istnieją jednak dobre powody, aby mieć je jako osobną zmienną, a nie tylko magiczne liczby. Na przykład ścisłe pisanie, łatwość konserwacji i zgodność z oczekiwaniami.


W naszej trzeciej próbie przyglądamy się zatem przypadkom, w których normalnym kierunkiem działalności jest posiadanie wartości innych niż int. Na przykład, jeśli zbiór tych wartości może zawierać wiele pozycji niecałkowitych. Oznacza to, że procedura obsługi wyjątków może być niewłaściwa.

W takim przypadku wygląda to dobrze na strukturę, która przechodzi przez int, i uzasadnienie. Ponownie, to uzasadnienie może być po prostu stałą jak powyżej, ale zamiast trzymać oba w tej samej int, przechowujesz je jako odrębne części struktury. Początkowo mamy zasadę, że jeśli zostanie ustawione uzasadnienie, int nie zostanie ustawione. Ale nie jesteśmy już przywiązani do tej zasady; w razie potrzeby możemy podać uzasadnienie również dla prawidłowych liczb.

Tak czy inaczej, za każdym razem, gdy go wywołujesz, nadal potrzebujesz szablonu, aby przetestować uzasadnienie, aby sprawdzić, czy int jest poprawny, a następnie wyciągnij i użyj części int, jeśli uzasadnienie na to pozwala.

W tym miejscu musisz zbadać swoje uzasadnienie „nie używaj null”.

Podobnie jak wyjątki, null ma oznaczać wyjątkowy stan.

Jeśli osoba dzwoniąca wywołuje tę metodę i całkowicie ignoruje „uzasadnienie” części struktury, oczekując liczby bez obsługi błędów, i otrzymuje zero, wówczas zniesie zero jako liczbę i będzie źle. Jeśli otrzyma magiczną liczbę, potraktuje to jako liczbę i pomyli się. Ale jeśli przyjmie wartość zerową, przewróci się , jak powinno, to cholernie dobrze.

Tak więc za każdym razem, gdy wywołujesz tę metodę, musisz sprawdzać jej wartość zwracaną, jednak obsługujesz niepoprawne wartości, czy to w paśmie, czy poza pasmem, spróbuj / złap, sprawdzając strukturę pod kątem komponentu „racjonalnego”, sprawdzając int dla magicznej liczby lub sprawdzanie int dla zerowej ...

Alternatywą, aby poradzić sobie z mnożeniem wyniku, który może zawierać niepoprawną liczbę całkowitą i uzasadnienie, takie jak „Mój pies zjadł ten pomiar”, jest przeciążenie operatora mnożenia dla tej struktury.

... A następnie przeciąż każdy inny operator aplikacji, który może zostać zastosowany do tych danych.

... A następnie przeciąż wszystkie metody, które mogą wymagać ints.

... I wszystkie te przeciążenia będą musiały nadal zawierać kontrole pod kątem niepoprawnych liczb całkowitych, tak aby można było traktować typ zwracany tej jednej metody tak, jakby zawsze była poprawną liczbą całkowitą w miejscu, w którym ją wywołujesz.

Oryginalna przesłanka jest fałszywa na różne sposoby:

  1. Jeśli masz niepoprawne wartości, nie możesz uniknąć sprawdzania tych niepoprawnych wartości w dowolnym punkcie kodu, w którym przetwarzasz wartości.
  2. Jeśli zwracasz coś innego niż int, nie zwracasz int, więc nie możesz traktować tego jak int. Przeciążenie operatora pozwala udawać , ale to tylko udawanie.
  3. Int z liczbami magicznymi (w tym NULL, NAN, Inf ...) nie jest już tak naprawdę int, jest strukturą biedaka.
  4. Unikanie wartości null nie uczyni kodu bardziej odpornym, po prostu ukryje problemy z intami lub przeniesie je do złożonej struktury obsługi wyjątków.
Dewi Morgan
źródło
1

Nie rozumiem przesłanki twojego pytania, ale oto odpowiedź nominalna. W przypadku braku lub pustej możesz zrobić math.nan(nie liczbę). Możesz wykonywać dowolne operacje matematyczne math.nani tak pozostanie math.nan.

Możesz użyć None(null Pythona) dla nieznanej wartości. I tak nie powinieneś manipulować nieznaną wartością, a niektóre języki (Python nie jest jednym z nich) mają specjalne operatory zerowe, dzięki czemu operacja jest wykonywana tylko wtedy, gdy wartość jest różna od wartości zerowej, w przeciwnym razie wartość pozostanie pusta.

Inne języki mają klauzule ochronne (jak Swift lub Ruby), a Ruby ma warunkowy wcześniejszy zwrot.

Rozwiązałem to w Pythonie na kilka różnych sposobów:

  • ze strukturą danych opakowania, ponieważ informacje liczbowe zwykle dotyczą jednostki i mają czas pomiaru. Opakowanie może zastąpić magiczne metody, __mult__tak aby żadne wyjątki nie były zgłaszane, gdy pojawią się Twoje Nieznane lub Brakujące wartości. Numpy i pandy mogą mieć w sobie taką zdolność.
  • z wartością wartownika (jak twój Unknownlub -1 / -2) i instrukcją if
  • z osobną flagą logiczną
  • z leniwą strukturą danych - twoja funkcja wykonuje pewne operacje na strukturze, a następnie zwraca, najbardziej zewnętrzna funkcja, która potrzebuje rzeczywistego wyniku, ocenia leniwą strukturę danych
  • z leniwym potokiem operacji - podobny do poprzedniego, ale ten można wykorzystać na zestawie danych lub bazie danych
noɥʇʎԀʎzɐɹƆ
źródło
1

Sposób przechowywania wartości w pamięci zależy od języka i szczegółów implementacji. Myślę, że masz na myśli to, jak obiekt powinien zachowywać się dla programisty. (Tak czytam pytanie, powiedz mi, czy się mylę).

Już w swoim pytaniu zaproponowałeś odpowiedź: użyj własnej klasy, która akceptuje dowolne operacje matematyczne i zwraca się bez zgłaszania wyjątku. Mówisz, że tego chcesz, ponieważ chcesz uniknąć zerowych kontroli.

Rozwiązanie 1: Nie unikaj sprawdzania wartości zerowej

Missingmoże być reprezentowany jako math.nan
Unknownmoże być reprezentowany jakoNone

Jeśli masz więcej niż jedną wartość, można filter()jedynie zastosować operację na wartości, które nie są Unknownlub Missing, lub cokolwiek wartości chcesz zignorować dla funkcji.

Nie wyobrażam sobie scenariusza, w którym potrzebujesz zerowego sprawdzenia funkcji, która działa na pojedynczy skalar. W takim przypadku dobrze jest wymusić kontrolę zerową.


Rozwiązanie 2: użyj dekoratora, który wychwytuje wyjątki

W takim przypadku Missingmoże podbić MissingExceptioni Unknownmoże podbić, UnknownExceptiongdy są na nim wykonywane operacje.

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

Zaletą tego podejścia jest to, że właściwości Missingi Unknownsą tłumione tylko wtedy, gdy wyraźnie zażądasz ich zniesienia. Kolejną zaletą jest to, że takie podejście jest samo dokumentujące: każda funkcja pokazuje, czy oczekuje nieznanego lub brakującego oraz w jaki sposób funkcja.

Gdy wywołujesz funkcję, która nie oczekuje, że brakująca otrzyma tęsknotę, funkcja natychmiast się podniesie, pokazując dokładnie, gdzie wystąpił błąd, zamiast po cichu zawieść i propagując brakujący łańcuch połączeń. To samo dotyczy Nieznanego.

sigmoidmożna jeszcze zadzwonić sin, chociaż nie oczekuje MissingOr Unknown, ponieważ sigmoid„s dekoratora złapie wyjątek.

noɥʇʎԀʎzɐɹƆ
źródło
1
zastanawiam się, jaki jest sens zamieszczania dwóch odpowiedzi na to samo pytanie (to jest twoja poprzednia odpowiedź , czy coś jest z nią nie tak?)
komnata
@gnat Ta odpowiedź zawiera uzasadnienie, dlaczego nie należy tego robić w sposób pokazany przez autora, a ja nie chciałem przechodzić przez problem integracji dwóch odpowiedzi z różnymi pomysłami - po prostu łatwiej jest napisać dwie odpowiedzi, które można odczytać niezależnie . Nie rozumiem, dlaczego tak bardzo troszczysz się o nieszkodliwe rozumowanie kogoś innego.
noɥʇʎԀʎzɐɹƆ
0

Załóżmy, że pobrano liczbę procesorów na serwerze. Jeśli serwer jest wyłączony lub został złomowany, ta wartość po prostu nie istnieje. Będzie to pomiar, który nie ma sensu (być może „brak” / „pusty” nie są najlepszymi terminami). Ale wartość ta jest „znana” jako nonsensowna. Jeśli serwer istnieje, ale proces pobierania wartości ulega awarii, pomiar jest prawidłowy, ale kończy się niepowodzeniem, co powoduje „nieznaną” wartość.

Oba brzmią jak warunki błędu, więc sądzę, że najlepszą opcją jest po prostu get_measurement()natychmiastowe wyrzucenie obu z nich jako wyjątków (takich jak odpowiednio DataSourceUnavailableExceptionlub SpectacularFailureToGetDataException). Następnie, jeśli wystąpi którykolwiek z tych problemów, kod gromadzący dane może zareagować na niego natychmiast (na przykład poprzez ponowną próbę w drugim przypadku) i get_measurement()musi zwrócić tylko intw przypadku, gdy może pomyślnie pobrać dane z danych źródło - i wiesz, że intjest poprawny.

Jeśli Twoja sytuacja nie obsługuje wyjątków lub nie możesz z nich wiele skorzystać, dobrym rozwiązaniem jest użycie kodów błędów, być może zwróconych przez osobne wyjście do get_measurement(). Jest to idiomatyczny wzorzec w C, w którym rzeczywiste dane wyjściowe są przechowywane we wskaźniku wejściowym, a kod błędu jest zwracany jako wartość zwracana.

TheHansinator
źródło
0

Podane odpowiedzi są w porządku, ale nadal nie odzwierciedlają hierarchicznej relacji między wartością, pustą i nieznaną.

  • Najwyższy jest nieznany .
  • Następnie przed użyciem wartości należy najpierw wyjaśnić puste .
  • Na końcu pojawia się wartość do obliczenia.

Brzydki (z powodu jego nieudanej abstrakcji), ale w pełni operacyjny byłby (w Javie):

Optional<Optional<Integer>> unknowableValue;

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

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

Tutaj funkcjonalne języki z ładnym systemem pisma są lepsze.

W rzeczywistości: W pustych / brakujące i nieznanych * non-wartości wydają się raczej częścią jakiegoś stanu procesu, pewnego procesu produkcyjnego. Podobnie jak Excel arkusze kalkulacyjne z formułami odnoszącymi się do innych komórek. Można by pomyśleć o przechowywaniu kontekstowych lambd. Zmiana komórki ponownie oceni wszystkie rekurencyjnie zależne komórki.

W takim przypadku wartość int zostałaby uzyskana przez dostawcę int. Pusta wartość dałaby int dostawcy rzucającemu pusty wyjątek lub oceniając go jako pustego (rekurencyjnie w górę). Twoja główna formuła połączyłaby wszystkie wartości i prawdopodobnie zwróciłaby pustą wartość (wartość / wyjątek). Nieznana wartość uniemożliwiłaby ocenę przez zgłoszenie wyjątku.

Wartości prawdopodobnie byłyby obserwowalne, jak własność związana z javą, powiadamiająca słuchaczy o zmianie.

W skrócie: powtarzający się wzorzec potrzebujących wartości z dodatkowymi stanami pustymi i nieznanymi wydaje się wskazywać, że lepszym może być model danych bardziej podobny do arkusza kalkulacyjnego.

Joop Eggen
źródło
0

Tak, w wielu językach istnieje koncepcja wielu różnych typów NA ; tym bardziej w statystycznych, gdzie jest to bardziej znaczące (tj. ogromne rozróżnienie między Missing-At-Random, Missing-Całkowicie-At-Random, Missing-Not-At-Random ).

  • jeśli mierzymy tylko długości widżetów, nie jest konieczne rozróżnienie między „awarią czujnika”, „odcięciem zasilania” lub „awarią sieci” (chociaż „przepełnienie numeryczne” przekazuje informacje)

  • ale np. w przypadku eksploracji danych lub ankiety, pytającej respondentów o np. ich dochód lub status HIV, wynik „Nieznany” różni się od „Odmów odpowiedzi” i widać, że nasze wcześniejsze założenia dotyczące przypisywania tego ostatniego będą miały tendencję być różnym od pierwszego. Tak więc języki takie jak SAS obsługują wiele różnych typów NA; język R nie, ale użytkownicy bardzo często muszą się włamać; NA w różnych punktach rurociągu mogą być używane do oznaczania bardzo różnych rzeczy.

  • istnieje również przypadek, w którym mamy wiele zmiennych NA dla jednego wpisu („wielokrotna imputacja”). Przykład: jeśli nie znam wieku, kodu pocztowego, poziomu wykształcenia ani dochodów osoby, trudniej jest przypisać jej dochód.

Jeśli chodzi o to, jak reprezentujesz różne typy NA w językach ogólnego przeznaczenia, które ich nie obsługują, na ogół ludzie hakują takie rzeczy jak zmiennoprzecinkowe NaN (wymaga konwersji liczb całkowitych), wyliczenia lub wartowników (np. 999 lub -1000) dla liczb całkowitych lub wartości kategoryczne. Zwykle nie ma zbyt czystej odpowiedzi, przepraszam.

smci
źródło
0

R ma wbudowaną obsługę brakujących wartości. https://medium.com/coinmonks/dealing-with-missing-data-using-r-3ae428da2d17

Edytuj: ponieważ zostałem przegłosowany, wyjaśnię trochę.

Jeśli masz zamiar zajmować się statystykami, zalecamy używanie języka statystyk, takiego jak R, ponieważ R jest napisany przez statystyków dla statystyk. Brakujące wartości to tak duży temat, że uczą cię przez cały semestr. I są duże książki tylko o brakujących wartościach.

Możesz jednak oznaczyć brakujące dane, takie jak kropka, „brak” lub cokolwiek innego. W R możesz zdefiniować, co rozumiesz przez brak. Nie musisz ich konwertować.

Normalnym sposobem na zdefiniowanie brakującej wartości jest oznaczenie ich jako NA.

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

Następnie możesz zobaczyć, jakich wartości brakuje;

is.na(x)

I wtedy wynik będzie;

FALSE FALSE  TRUE FALSE FALSE

Jak widać ""nie brakuje. Możesz zagrażać ""jako nieznany. I NAzaginął.

ilhan
źródło
@Hulk, jakie inne języki funkcjonalne obsługują brakujące wartości? Nawet jeśli obsługują brakujące wartości, jestem pewien, że nie można wypełnić ich metodami statystycznymi tylko w jednym wierszu kodu.
ilhan
-1

Czy istnieje powód, dla którego *nie można zmienić funkcji operatora?

Większość odpowiedzi wymaga pewnego rodzaju wyszukiwania, ale w takim przypadku może być po prostu łatwiej zmienić operator matematyczny.

Będziesz wtedy mógł mieć podobny empty()/ unknown()funkcjonalność w obrębie całego projektu.

Edward
źródło
4
Oznacza to, że musiałbyś przeciążać wszystkich operatorów
rura