Czy sensowne jest posiadanie zarówno koncepcji „zerowej”, jak i „być może”?

11

Podczas tworzenia klienta interfejsu API sieci Web w języku C # napotkałem problem dotyczący nullwartości, która reprezentowałaby dwie różne rzeczy:

  • nic , np. foomoże mieć lub może nie miećbar
  • nieznany : domyślnie odpowiedź API zawiera tylko podzbiór właściwości, musisz wskazać, które dodatkowe właściwości chcesz. Tak nieznany oznacza, że ​​właściwość nie była wymagana od API.

Po kilku poszukiwaniach dowiedziałem się o typie Może (lub Opcji), w jaki sposób jest on używany w językach funkcjonalnych i jak „rozwiązuje” problemy zerowania dereferencji, zmuszając użytkownika do myślenia o możliwym braku wartości. Jednak wszystkie zasoby natknąłem mówił o wymianie NULL z Może . Znalazłem kilka wzmianek o trójwartościowej logice , ale nie do końca ją rozumiem i przez większość czasu wspomniano o niej w kontekście „to zła rzecz”.

Zastanawiam się teraz, czy sensownie jest mieć zarówno wartość zerową, jak i Być może odpowiednio reprezentować nieznane i nic . Czy to logika trójwartościowa, o której czytam, czy też ma inną nazwę? A może zamierzony sposób na zagnieżdżenie „Może w być”?

Stijn
źródło
9
Nie ma sensu mieć null. Jest to całkowicie zepsuty pomysł.
Andrej Bauer,
7
Nie rób może-może-foo, który ma inną semantykę niż może-foo. Może jest monadą , a jedno z jej praw jest takie M M xi M xpowinno mieć tę samą semantykę.
Eric Lippert,
2
Można rozważyć przyjrzenie się projektowi wczesnych wersji Visual Basic, które nie zawierały niczego (odniesienie do żadnego obiektu), zerowego (semantyka zerowa bazy danych), pustego (zmienna nie została zainicjowana) i brakującego (parametr opcjonalny nie został przekazany). Ten projekt był skomplikowany i pod wieloma względami niespójny, ale istnieje powód, dla którego koncepcje te nie zostały ze sobą powiązane.
Eric Lippert,
3
Tak, nie użyłbym może być może. Ale w rzeczywistości, tak jak Maybe ajest taka sama jak , semantycznie, jest taka sama jak , a izomorficzne do pisania z @Andej odpowiedź. Możesz również zdefiniować własną instancję monady dla tego typu, a zatem użyć różnych kombinatorów monad. a + 1 + 1a+1Maybe Maybe aa+1+1UserInput a
Euge
12
@EricLippert: nieprawdą jest, że „ M (M x)i M xpowinien mieć tę samą semantykę”. Weźmy M = Listna przykład: listy list nie są tym samym, co listy. Kiedy Mjest monada, jest transformacja (czyli mnożenie monada) od M (M x)do M xco wyjaśnia relacje między nimi, ale oni nie mają „te same semantyki”.
Andrej Bauer,

Odpowiedzi:

14

nullWartość jako domyślną obecny wszędzie jest po prostu naprawdę złamane pomysł , więc zapomnij o tym.

Zawsze powinieneś mieć dokładnie taką koncepcję, która najlepiej opisuje twoje rzeczywiste dane. Jeśli potrzebujesz typu, który wskazuje „nieznane”, „nic” i „wartość”, powinieneś mieć właśnie to. Ale jeśli nie odpowiada twoim rzeczywistym potrzebom, nie powinieneś tego robić. Dobrym pomysłem jest sprawdzenie, czego używają inni ludzie i co zaproponowali, ale nie musisz ich ślepo podążać.

Ludzie, którzy projektują standardowe biblioteki, próbują odgadnąć typowe wzorce użytkowania i zwykle robią to dobrze, ale jeśli potrzebujesz konkretnej rzeczy, powinieneś ją zdefiniować sam. Na przykład w Haskell można zdefiniować:

data UnknownNothing a =
     Unknown
   | Nothing
   | Value a

Może być nawet tak, że powinieneś używać czegoś bardziej opisowego, łatwiejszego do zapamiętania:

data UserInput a =
     Unknown
   | NotGiven
   | Answer a

Możesz zdefiniować 10 takich typów, które będą używane w różnych scenariuszach. Wadą jest to, że nie będziesz mieć dostępnych wcześniej istniejących funkcji bibliotecznych (takich jak te Maybe), ale zwykle okazuje się to szczegółem. Dodanie własnej funkcjonalności nie jest trudne.

Andrej Bauer
źródło
Ma to sens, kiedy się tak pisze. Najbardziej interesują mnie pomysły, istniejące wsparcie biblioteki to tylko bonus :)
Stijn
Gdyby tylko bariera tworzenia takich typów była mniejsza w wielu językach. : /
Raphael
Gdyby tylko ludzie przestali używać języków z lat 60. (lub ich reinkarnacji z lat 80.).
Andrej Bauer
@AndrejBauer: co? ale wtedy teoretycy nie mieliby na co narzekać!
Yttrill
Nie narzekamy, jesteśmy w górze.
Andrej Bauer,
4

O ile mi wiadomo, wartość nullw C # jest możliwą wartością dla niektórych zmiennych, w zależności od jej typu (mam rację?). Na przykład wystąpienia niektórych klas. Dla pozostałych typów (jak int, boolitp) można dodać tę wartość wyjątku przez deklarowania zmiennych z int?lub bool?zamiast (to jest dokładnie to, co Mayberobi konstruktor, jak opiszę następne).

Konstruktor Maybetypów z programowania funkcjonalnego dodaje tę nową wartość wyjątku dla danego typu danych. Więc jeśli Intjest typem liczb całkowitych lub Gametypem stanu gry, to Maybe Intma wszystkie liczby całkowite plus wartość zerową (czasami nazywaną niczym ). To samo dotyczy Maybe Game. Tutaj nie ma typów, które pochodzą z nullwartością. Dodajesz go, kiedy go potrzebujesz.

IMO, to ostatnie podejście jest lepsze dla każdego języka programowania.

Ogromny
źródło
Tak, twoje rozumienie C # jest prawidłowe. Czy mogę więc wywnioskować z twojej odpowiedzi, że sugerujesz zagnieżdżenie Maybei nullcałkowite upuszczenie ?
Stijn
2
Moje zdanie dotyczy tego, jaki powinien być język. Naprawdę nie znam najlepszych praktyk programowania w języku C #. W twoim przypadku najlepszą opcją byłoby zdefiniowanie typu danych zgodnie z opisem @Andrej Bauer, ale myślę, że nie możesz tego zrobić w języku C #.
Euge
@Euge: Typy sum algebraicznych można (z grubsza) aproksymować za pomocą klasycznego podtypu OO i polimorficznej wysyłki podtypu. Tak dzieje się na przykład w Scali, z dodatkowym zwrotem pozwalającym na zamknięcie typów. Typ staje się abstrakcyjną nadklasą, konstruktory typów stają się konkretnymi podklasami, dopasowanie wzorca nad konstruktorami można aproksymować, przenosząc przypadki na przeciążone metody w konkretnych podklasach lub isinstancetesty. Scala ma również „właściwe” dopasowanie do wzorca, a C♯ faktycznie niedawno zyskał również proste wzorce.
Jörg W Mittag,
„Dodatkowy zwrot”, o którym wspomniałem dla Scali, polega na tym, że oprócz „standardowych” modyfikatorów „wirtualnych” (dla klasy, z której można odziedziczyć) i final(dla klasy, której nie można odziedziczyć), ma ona również sealeddla klasa, którą można rozszerzyć tylko w ramach tej samej jednostki kompilacji . Oznacza to, że kompilator może statycznie znać wszystkie możliwe podklasy (pod warunkiem, że wszystkie z nich są albo same, sealedalbo final), a tym samym sprawdzać kompletność dopasowań wzorców, co nie jest możliwe statycznie dla języka z ładowaniem kodu środowiska wykonawczego i nieograniczonym dziedziczeniem.
Jörg W Mittag,
Oczywiście możesz projektować typy z tą semantyką w C #. Zauważ, że C # nie pozwala na zagnieżdżanie wbudowanego typu (zwanego Nullable w C #). int??jest nielegalne w C #.
Eric Lippert,
3

Jeśli można zdefiniować związki typu, jak X or Yi jeśli masz typ o nazwie Null, która reprezentuje tylko nullwartość (i nic więcej), a następnie dla każdego typu T, T or Nullw rzeczywistości nie jest tak różne od Maybe T. Jedynym problemem nulljest to, że język traktuje typ Tjako T or Nullniejawnie, co czyni nullprawidłową wartość dla każdego typu (z wyjątkiem typów pierwotnych w językach, które je mają). Tak dzieje się na przykład w Javie, gdzie funkcja, która przyjmuje Stringrównież, akceptuje null.

Jeśli traktujesz typy jako zbiór wartości i orjako sumę zbiorów, wówczas (T or Null) or Nullreprezentuje zestaw wartości T ∪ {null} ∪ {null}, który jest taki sam jak T or Null. Istnieją kompilatory, które wykonują tego rodzaju analizy (SBCL). Jak powiedziano w komentarzach, nie można łatwo odróżnić typów od domen Maybe Ti Maybe Maybe Tkiedy wyświetlasz je (z drugiej strony widok ten jest całkiem przydatny w przypadku programów z dynamicznym pisaniem). Ale możesz także zachować typy jako wyrażenia algebraiczne, gdzie (T or Null) or Nullreprezentują wiele poziomów Maybesa.

rdzeń rdzeniowy
źródło
2
W rzeczywistości ważne Maybe (Maybe X)jest, że to nie to samo, co Maybe X. Nothingto nie to samo co Just Nothing. Połączenie Maybe (Maybe X)z Maybe Xtym uniemożliwia leczenie Maybe _polimorficzne.
Gilles „SO- przestań być zły”
1

Musisz dowiedzieć się, co chcesz wyrazić, a następnie znaleźć sposób, jak to wyrazić.

Po pierwsze, masz wartości w domenie problemowej (takie jak liczby, ciągi znaków, rekordy klientów, booleany itp.). Po drugie, masz dodatkowe ogólne wartości nad wartościami domen problemowych: Na przykład „nic” (znany brak wartości), „nieznany” (brak wiedzy o obecności lub braku wartości), nie wspomniano „ coś ”(zdecydowanie jest pewna wartość, ale nie wiemy która). Może możesz pomyśleć o innych sytuacjach.

Jeśli chcesz być w stanie przedstawić wszystkie wartości plus te trzy dodatkowe wartości, utwórz enum z przypadkami „nic”, „nieznane”, „coś” i „wartość” i stamtąd.

W niektórych językach niektóre przypadki są prostsze. Na przykład w Swift masz dla każdego typu T typ „opcjonalny T”, który ma jako możliwe wartości zero plus wszystkie wartości T. To pozwala łatwo poradzić sobie z wieloma sytuacjami i jest idealny, jeśli masz „nic” i „ wartość". Możesz go użyć, jeśli masz „nieznane” i „wartość”. Można nie używać go jeśli trzeba obsłużyć „nic”, „nieznane” i „wartość”. Inne języki mogą używać „null” lub „może”, ale to tylko inne słowo.

W JSON masz słowniki z parami klucz / wartość. Tutaj klucz może po prostu brakować w słowniku lub może mieć wartość null. Dokładnie opisując sposób przechowywania rzeczy, możesz przedstawić wartości ogólne plus dwie dodatkowe wartości.

gnasher729
źródło