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 print
instrukcji 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.
źródło
0
,[]
lub{}
(odpowiednio skalar 0, pusta lista i pusta mapa). Ponadto ta „brakująca” / „nieznana” wartość jest w zasadzie dokładnie tym, do czegonull
służy - oznacza, że może tam być obiekt, ale nie ma go.Odpowiedzi:
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:
Measurement
Wartość będzie wtedyReading
, o wartości int albo AMissing
, alboUnknown
z surowych danych, jakvalue
(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.
źródło
std::variant
(i jego duchowych poprzedników).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 # iOptional<T>
w innych językach.Załóżmy, że masz abstrakcyjny typ reprezentujący monadę:
a następnie trzy podklasy:
Potrzebujemy wdrożenia Bind:
Z tego możesz napisać tę uproszczoną wersję Binda:
A teraz gotowe. Masz
Measurement<int>
pod ręką. Chcesz go podwoić:I podążaj za logiką; jeśli
m
jest,Empty<int>
toasString
jestEmpty<String>
doskonałe.Podobnie, jeśli mamy
i
następnie możemy połączyć dwa pomiary:
i znowu, jeśli
First()
jest,Empty<int>
tod
jestEmpty<double>
i tak dalej.Kluczowym krokiem jest poprawne wykonanie operacji wiązania . Zastanów się nad tym.
źródło
Null
zNullable
+ jakiś standardowy kod? :)Measurement<T>
jest to typ monadyczny.Myślę, że w tym przypadku przydatna byłaby odmiana wzorca zerowego obiektu:
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ównull
s. Nie jestem osobą Java, ale rozumiem, że JavaOptionalInt
jest podobna, a inne języki prawdopodobnie mają własne udogodnienia do reprezentowaniaOptional
typu.źródło
Value
gettera, co absolutnie powinno zawieść, ponieważ nie można przekonwertować go zUnknown
powrotem na plikint
. 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).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
Ważne wyjaśnienie:
Możesz spełnić wymagania matematyczne, przeciążając operatory dla klasy
źródło
Option<Option<Int>>
type Measurement = Option<Int>
dla wyniku, który był liczbą całkowitą lub pustym odczytem, jest to w porządku, podobnie jakOption<Measurement>
dla pomiaru, który mógł zostać wykonany lub nie .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żeniaa == b
zawsze 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).źródło
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:
druk
( Pełna realizacja jako sedno ).
Język lub biblioteka FP zapewnia inne narzędzia, takie jak
Try
(akaMaybe
) (obiekt zawierający wartość lub błąd) iEither
(obiekt zawierający wartość sukcesu lub wartość błędu), które również mogą być tutaj użyte.źródło
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:
null
źródło
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 ...)
źródło
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
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:
źródło
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 matematycznemath.nan
i tak pozostaniemath.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:
__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ść.Unknown
lub -1 / -2) i instrukcją ifźródło
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
Missing
może być reprezentowany jakomath.nan
Unknown
moż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ąUnknown
lubMissing
, 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
Missing
może podbićMissingException
iUnknown
może podbić,UnknownException
gdy są na nim wykonywane operacje.Zaletą tego podejścia jest to, że właściwości
Missing
iUnknown
są 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.
sigmoid
można jeszcze zadzwonićsin
, chociaż nie oczekujeMissing
OrUnknown
, ponieważsigmoid
„s dekoratora złapie wyjątek.źródło
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 odpowiednioDataSourceUnavailableException
lubSpectacularFailureToGetDataException
). 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) iget_measurement()
musi zwrócić tylkoint
w przypadku, gdy może pomyślnie pobrać dane z danych źródło - i wiesz, żeint
jest 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.źródło
Podane odpowiedzi są w porządku, ale nadal nie odzwierciedlają hierarchicznej relacji między wartością, pustą i nieznaną.
Brzydki (z powodu jego nieudanej abstrakcji), ale w pełni operacyjny byłby (w Javie):
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.
źródło
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.
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.
źródło
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
.Następnie możesz zobaczyć, jakich wartości brakuje;
I wtedy wynik będzie;
Jak widać
""
nie brakuje. Możesz zagrażać""
jako nieznany. INA
zaginął.źródło
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.źródło