Dlaczego znak minus „-” zasadniczo nie jest przeciążony w taki sam sposób, jak znak plus?

64

Znak plus +służy do dodawania i konkatenacji łańcuchów, ale jego towarzysz: znak minus -, na ogół nie jest widoczny do przycinania łańcuchów lub w innych przypadkach innych niż odejmowanie. Jaki może być tego powód lub ograniczenia?

Rozważ następujący przykład w JavaScript:

var a = "abcdefg";
var b = "efg";

a-b == NaN
// but
a+b == "abcdefgefg"
Digvijay Yadav
źródło
35
które „yy” należy usunąć?
gashach
12
Jeśli pójdę z zachowaniem znaku „+”, to prawo najbardziej ma sens.
Digvijay Yadav
46
Wystarczająco źle jest, że +operator binarny jest przeciążony dwoma całkowicie niepowiązanymi znaczeniami „dodawanie liczbowe” i „konkatenacja ciągów”. Na szczęście niektóre języki zapewniają osobny operator konkatenacji, taki jak .(Perl5, PHP), ~(Perl6), &(VB), ++(Haskell),…
amon
6
@MasonWheeler Używają ->(myśl, że dereferencjonowanie dostępu członków w C, ponieważ wirtualne wywołania metod muszą koniecznie obejmować pośrednie wskaźniki). Nie ma prawa projektowania języka, które wymaga wywołań metod / dostępu członków do korzystania z .operatora, chociaż jest to coraz bardziej powszechna konwencja. Czy wiesz, że Smalltalk nie ma operatora wywołań metod? object methodWystarczy proste zestawienie .
amon
20
Python robi minus przeciążeniem, dla zadanej odejmowanie (a to może być przeciążony typów zdefiniowanych przez użytkownika, jak również). Zestawy Python przeciążają także większość operatorów bitowych dla przecięcia / unii / etc.
Kevin

Odpowiedzi:

116

Krótko mówiąc, nie ma żadnych szczególnie użytecznych operacji odejmujących na łańcuchach, z którymi ludzie chcieliby pisać algorytmy.

+Operator zwykle oznacza operację addytywnej monoid , czyli asocjacyjna współpracy z elementem tożsamości:

  • A + (B + C) = (A + B) + C
  • A + 0 = 0 + A = A

Sensowne jest używanie tego operatora do takich rzeczy, jak dodawanie liczb całkowitych, łączenie łańcuchów i ustawianie unii, ponieważ wszystkie one mają tę samą strukturę algebraiczną:

1 + (2 + 3) == (1 + 2) + 3
1 + 0 == 0 + 1 == 1

"a" + ("b" + "c") == ("a" + "b") + "c"
"a" + "" == "" + "a" == "a"

I możemy go użyć do napisania przydatnych algorytmów, takich jak concatfunkcja, która działa na sekwencji dowolnych „ możliwych do połączenia” rzeczy, np .:

def concat(sequence):
    return sequence.reduce(+, 0)

Kiedy w grę -wchodzi odejmowanie , zwykle mówisz o strukturze grupy , która dodaje odwrotne −A dla każdego elementu A, tak że:

  • A + −A = −A + A = 0

I chociaż ma to sens w przypadku odejmowania liczb całkowitych i zmiennoprzecinkowych, a nawet ustawiania różnicy, nie ma to większego sensu dla ciągów i list. Jaka jest odwrotność "foo"?

Istnieje struktura zwana monoidem eliminującym , która nie ma odwrotności, ale ma właściwość anulowania , dzięki czemu:

  • A - A = 0
  • A - 0 = A
  • (A + B) - B = A

Jest to struktura, którą opisujesz, gdzie "ab" - "b" == "a", ale "ab" - "c"nie jest zdefiniowana. Po prostu nie mamy wielu przydatnych algorytmów, które wykorzystują tę strukturę. Myślę, że jeśli traktujesz konkatenację jako serializację, to można zastosować odejmowanie do pewnego rodzaju analizy.

Jon Purdy
źródło
2
W przypadku zbiorów (i wielu zestawów) odejmowanie ma sens, ponieważ w przeciwieństwie do sekwencji kolejność elementu nie ma znaczenia.
CodesInChaos
@CodesInChaos: Dodałem wzmiankę o nich, ale nie czułem się komfortowo, umieszczając zestawy jako przykład grupy - nie sądzę, aby tworzyły jeden zestaw, ponieważ generalnie nie można zbudować odwrotności zestawu.
Jon Purdy
12
W rzeczywistości +operacja jest również przemienna dla liczb A+B == B+A, co oznacza, że ​​jest złym kandydatem do łączenia ciągów znaków. To, plus mylące pierwszeństwo operatorów sprawia, że ​​użycie +do konkatenacji ciągów jest historycznym błędem. Prawdą jest jednak, że użycie -dowolnej operacji łańcuchowej znacznie pogorszyło sytuację…
Holger,
2
@Darkhogg: Racja! PHP pożyczony .od Perla; jest ~w Perl6, prawdopodobnie inne.
Jon Purdy
1
@MartinBeckett, ale widać, że zachowanie może być mylące z .text.gz.text...
Boris the Spider
38

Ponieważ łączenie dowolnych dwóch poprawnych ciągów jest zawsze prawidłową operacją, ale odwrotność nie jest prawdą.

var a = "Hello";
var b = "World";

Co powinno a - btu być? Naprawdę nie ma dobrego sposobu na udzielenie odpowiedzi na to pytanie, ponieważ samo pytanie jest nieprawidłowe.

Mason Wheeler
źródło
31
@DigvijayYadav, jeśli usuniesz 5 mango z 5 jabłek, czy musi tam być licznik -5 mango? Czy to nic nie robi? Czy potrafisz zdefiniować to wystarczająco dobrze, aby można je było ogólnie zaakceptować i umieścić we wszystkich kompilatorach i tłumaczach języków, aby używać tego operatora w tej formie? To jest wielkie wyzwanie tutaj.
JB King
28
@DigvijayYadav: Więc właśnie opisałeś dwa możliwe sposoby realizacji tego, i istnieje dobry argument, aby uważać każdy z nich za ważny, więc już robimy bałagan z pomysłem określenia tej operacji. : P
Mason Wheeler,
13
@smci Wydaje mi się, że 5 + Falsepowinien to być błąd , ponieważ liczba nie jest liczbą logiczną, a wartość logiczna nie jest liczbą.
Mason Wheeler
6
@JanDvorak: Nie ma w tym nic szczególnie „Haskelly”; to podstawowe mocne pisanie.
Mason Wheeler
5
@DigvijayYadav Więc (a+b)-b = a(mam nadzieję!), Ale (a-b)+bczasami a, czasem a+bzależy od tego, czy bjest to podciąg, aczy nie? Co to za szaleństwo?
28

Ponieważ -operator manipulacji ciągami nie ma wystarczającej „spójności semantycznej”. Operatory powinny być przeciążane tylko wtedy, gdy jest absolutnie jasne, co przeciążenie robi z operandami, a odejmowanie łańcucha nie spełnia tego paska.

W związku z tym preferowane są wywołania metod:

public string Remove(string source, string toRemove)
public string Replace(string source, string oldValue, string newValue)

W języku C # używamy +do konkatenacji ciągów, ponieważ forma

var result = string1 + string2 + string3;

zamiast

var result = string.Concat(string1, string2, string3);

jest wygodny i prawdopodobnie łatwiejszy do odczytania, mimo że wywołanie funkcji jest prawdopodobnie bardziej „poprawne” z semantycznego punktu widzenia.

W +tym kontekście operator może naprawdę oznaczać tylko jedną rzecz. To nie jest prawdziwe dla -, ponieważ pojęcie odjęcia strun jest niejednoznaczna (wywołanie funkcji Replace(source, oldValue, newValue)z ""jako newValueparametr usuwa wszelkie wątpliwości, a funkcja może być używana do zmiany podciągi, a nie tylko je usunąć).

Problem polega oczywiście na tym, że przeciążenie operatora zależy od typów przekazywanych do operatora, a jeśli przekażesz ciąg znaków w miejscu, w którym powinna być liczba, możesz uzyskać wynik, którego się nie spodziewałeś. Ponadto w przypadku wielu konkatenacji (tj. W pętli) StringBuilderpreferowany jest obiekt, ponieważ każde użycie +tworzy nowy ciąg, co może pogorszyć wydajność. Więc +operator nie jest nawet odpowiedni we wszystkich kontekstach.

Istnieją przeciążenia operatora, które mają lepszą spójność semantyczną niż +operator do konkatenacji łańcucha. Oto jedna, która dodaje dwie liczby zespolone:

public static Complex operator +(Complex c1, Complex c2) 
{
    return new Complex(c1.real + c2.real, c1.imaginary + c2.imaginary);
}
Robert Harvey
źródło
8
+1 Biorąc pod uwagę dwa ciągi A i B, mogę myśleć o AB jako „usuń końcowe B z końca A”, „usuń instancję B z gdzieś w A”, „usuń wszystkie instancje B z gdzieś w A , ”lub nawet„ usuń wszystkie znaki znajdujące się w B z A. ”
Cort Ammon
8

Język Groovy pozwala -:

println('ABC'-'B')

zwroty:

AC

I:

println( 'Hello' - 'World' )

zwroty:

Hello

I:

println('ABABABABAB' - 'B')

zwroty:

AABABABAB
Wim Deblauwe
źródło
11
Ciekawe - więc postanawia usunąć pierwsze wystąpienie? Dobry przykład całkowicie sprzecznego z intuicją zachowania.
Hulk
9
Stąd mamy, że ('ABABABABA' + 'B') - 'B'to nie jest prawie tak samo jak wartość początkowa 'ABABABABA'.
CVn
3
@ MichaelKjörling OTOH, (A + B) - A == Bdla każdego A i B. Czy mogę to nazwać odjęciem od lewej?
John Dvorak
2
Haskell ma ++na konkatenację. Działa na dowolnej liście, a ciąg znaków to tylko lista znaków. Ma również \\, która usuwa pierwsze wystąpienie każdego elementu w prawym argumencie z lewego argumentu.
John Dvorak
3
Wydaje mi się, że te przykłady są dokładnie powodem, dla którego nie powinno być operatora minus dla ciągów. Jest to niespójne i nie intuicyjne zachowanie. Kiedy myślę o „-”, na pewno nie myślę, „usuń pierwszą instancję pasującego łańcucha, jeśli wystąpi, w przeciwnym razie po prostu nic nie rób”.
enderland
6

Znak plus prawdopodobnie kontekstowo ma sens w większej liczbie przypadków, ale kontrprzykładem (być może wyjątkiem potwierdzającym regułę) w Pythonie jest obiekt set, który zapewnia, -ale nie +:

>>> set('abc') - set('bcd')
set(['a'])
>>> set('abc') + set('bcd')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'set' and 'set'

Używanie +znaku nie ma sensu, ponieważ intencja może być niejednoznaczna - czy oznacza to ustawienie skrzyżowania czy zjednoczenia? Zamiast tego używa |do zjednoczenia i &skrzyżowania:

>>> set('abc') | set('bcd')
set(['a', 'c', 'b', 'd'])
>>> set('abc') & set('bcd')
set(['c', 'b'])
Aaron Hall
źródło
2
Jest to bardziej prawdopodobne, ponieważ odejmowanie zestawu jest zdefiniowane w matematyce, ale dodawanie zestawu nie jest.
Mehrdad
Użycie „-” wydaje się niejasne; tak naprawdę potrzebny jest operator „ale nie”, który byłby również przydatny podczas wykonywania bitowej arytmetyki z liczbami całkowitymi. Gdyby 30 ~ i 7 miały 24, to użycie ~ & z zestawami pasowałoby ładnie z & i | nawet jeśli w zestawach brakuje operatora ~.
supercat
1
set('abc') ^ set('bcd')zwraca set(['a', 'd']), jeśli pytasz o różnicę symetryczną.
Aaron Hall
3

-” jest używany w niektórych słowach złożonych (na przykład „na miejscu”) do łączenia różnych części w to samo słowo. Dlaczego nie używamy „ -” do łączenia różnych ciągów znaków w językach programowania? Myślę, że to miałoby sens! Do diabła z tymi +bzdurami!

Spróbujmy jednak spojrzeć na to z nieco bardziej abstrakcyjnego punktu widzenia.

Jak zdefiniowałbyś algebrę łańcuchów? Jakie miałbyś operacje i jakie prawa by na nich obowiązywały? Jakie byłyby ich relacje?

Pamiętaj, że może nie być absolutnie żadnych dwuznaczności! Każdy możliwy przypadek musi być dobrze zdefiniowany, nawet jeśli oznacza to, że nie można tego zrobić! Im mniejsza jest algebra, tym łatwiej to zrobić.

Na przykład, co tak naprawdę oznacza dodawanie lub odejmowanie dwóch ciągów?

Jeśli dodasz dwa ciągi (na przykład let a = "aa"i b = "bb"), czy uzyskasz aabbwynik w wyniku a + b?

Jak o b + a? To by było bbaa? Dlaczego nie aabb? Co się stanie, jeśli odejmiesz aaod wyniku dodania? Czy twój łańcuch miałby pojęcie ujemnej ilości aa?

Teraz wróć do początku tej odpowiedzi i zamień spaceshuttlezamiast łańcucha. Uogólniając, dlaczego jakakolwiek operacja jest zdefiniowana lub nie zdefiniowana dla żadnego typu?

Chodzi mi o to, że nic nie stoi na przeszkodzie, aby stworzyć algebrę do czegokolwiek. Znalezienie znaczących operacji lub nawet przydatnych operacji może być trudne.

W przypadku łańcuchów łączenie jest właściwie jedynym sensownym, z jakim się zetknąłem. Nie ma znaczenia, jakiego symbolu użyto do przedstawienia operacji.

Zavior
źródło
1
„W przypadku strun łączenie jest właściwie jedynym sensownym, z jakim się zetknąłem” . Czy zatem nie zgadzasz się z Pythonem 'xy' * 3 == 'xyxyxy'?
smci
3
@smci, to z pewnością mnożenie jako powtórzenie ?
jonrsharpe
jaki jest właściwy operator do łączenia promów kosmicznych?
Mr.Mindor
4
@ Mr.Mindor backspace ... aby usunąć przestrzeń między promami kosmicznymi.
YoungJohn