Равенство и Еквивалентност

Lisp, Scheme и функционалните езици като цяло се отличават със своята опростеност. Програмите, стига да свикнеш със скобите, са елегантни и нямат нужда от документация.
Повечето задачи се свеждат до вече решени и съответно се решават на по един ред.
Всичко се прави точно по един начин, спазва се великият принцип DRY1.
Днес ще се занимаваме с осемдесет и петте начина да се провери дали два обекта в Scheme са равни.
Ето списъкът, който ще ползваме за тестване.

(define a 5)
(define b 7)
(define l (list a b a 5 5 9))
(define m (list 5.5 5.5 5.0))

Операцията =

С = се сравняват две числа.

(= (car l) (caddr l) )
> #t
(= (car l) (cadddr l) )
> #t
(= (car l) (cadr l) )
> #f
(= (car m) (cadr m))
> #t

Просто и ясно. Да беше само целият език така.
Оттук нататък се отварят портите на ада.

eq?

Предикатът (eq? a b) връща истина (#t) когато двата му аргумента сочат към един и същ обект в паметта, иначе лъжа(#f).
Малко уговорки. В днешната лекция нещата не са такива, каквито изглеждат. Т.е. просто защото аз ти казвам, че по еди-кой-си стандарт нещата са така - това не означава, че на версията, на която ти пишеш нещата ще са същите. Даже е по-вероятно да не са съвсем точно такива. Приемай нещата, които ти казвам, с щипка сол. Не че не са верни, но все пак тествай на твоята имплементация дали е точно така.

(eq? (car l) (caddr l))        ;5 - 5
> #t
(eq? (car l) (cadr l) )        ;5 - 7
> #f
(eq? (cadr l) b)            ;7 - 7
> #t
(eq? (cadddr l) (car (cddddr l))     ;5 - 5
> #t
(eq? (car m) (cadr m))    ;5.5 - 5.5
> #f
(а, какво?)

Ок, защо по дяволите 5 = 5, но 5.5 не е равно на 5.5? Защото Scheme се прави на голям хитрец. Представи си, че имаш програма, която записва оценките на ученици. И никакви такива пост-модернистични глупости като 5.50 с два минуса. Имаш две, три, четири, пет или шест. Имаш, да кажем, 1000 ученика всеки с по 10 предмета и 3 оценки на срок. 30000 оценки. И като го вземеш за пет години стават 150 000. А като имаш предвид и ентусиазираната преподавателка по математика, която всяка седмица дава контролно, лесно стават 200 000. Двеста хиляди числа, всяко от които е едно от общо… пет. Тоест, около Сто деветдесет и девет хиляди, деветстотин деведесет и пет пъти сме забили в паметта нещо, което там вече го има. Има ли причина да хабим толкова памет? В другото училище не я хабят тая памет, нима ще ги оставим да ни се присмиват? Затова често срещаните числа се записват на едно и също място в паметта. Тоест, ако напишеш двадесет милиона пъти числото 10, то пак ще заеме общо 4 байта в паметта2. Според авторите на Scheme често срещаните числа са първите еди-колко-си естествени числа.
Тоест:
(eq? 9 9)
> #t
(eq? 9999999999999999999 9999999999999999999)
> #f

Не е много интуитивно, знам. Но ако това е някакво успокоение, в повечето модерни езици нещата се правят по точно съвсем същия начин. Освен това, те минават едно ниво по-високо. В Python, например, низовете с по-малка от определена дължина се пазят в един и същи обект. В Ruby има специален тип символ, чиято единствена идея е да прави точно това.

eqv?

(eqv? a b) връща #t когато двата му аргумента са функционално еквивалентни. Т.е. ако замениш единия с другия - дали е напълно сигурно, че програмата ти ще работи по един и същи начин. Това е малко по-удобно, защото работи по-близко до интуитивната представа за нещата. Две равни числа са равни3. Два еднакви низа са равни и т.н.
Работата тук е малко тънка. Тези операции работят различно на различни реализации на Scheme. Затова не мога да ти кажа с абсолютна сигурност кое какво е. Чувал съм, че може имплементацията да проверява дали две функции са еднакви(т.е. правят едни и същи операции върху списък и нямат странични ефекти), но не е сигурно. Пробвай и виж при теб как е.
Забележка:

(eqv? (/ 5 2) (/ 10 4))
> #t
(eqv? (/ 5.0 2) (/ 10.0 4))
> #t
(eqv? 5 5.0)
> #f

Сравнението работи само за числа от един и същи вид, т.е. абсолютно точни/неабсолютно точни. Ако се сещаш - числата, за които това е възможно, се пазят като обикновени дроби, а останалите - с няколко цифри след запетаята. Тези двете никога не са равни според eqv?.
В сила е следното правило: ако (eq? x) е истина, то и (eqv? x) ще е истина. Обратното, естествено, не е вярно.

equal?

(equal? a b) връща #t когато двата му аргумента "изглеждат" еднакво. Т.е. ако ги отпечаташ, ще излезе едно и също нещо. Малко неща могат да се кажат за equal?, всичко е точно каквото изглежда. Две функции с еднакъв код са еднакви в този смисъл. Както и две равни числа, два еднакви низа, еднакви списъци…

(eq? (cons 4 5) (cons 4 5))
> #f
(eqv? (cons 4 5) (cons 4 5))
> #f
(equal? (cons 4 5) (cons 4 5))
> #t

Това е най-удобното за хора сравнение. Не се използва прекалено често, освен когато нещо трябва да се печата на екрана..
Отново може да се изкаже следното правило: Ако (eqv? x) е истина, то и (equal? x) ще е истина.
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 3.0 License