Мутатори и Деструктори

Мутатори, деструктори… Заглавието звучи все едно е епизод от Костенурките Нинджа, нали?
Донатело vs Шрьодер. Последната битка, изправят се един срещу друг и Донатело изревава:

Ти не спазваш функционалната парадигма! Умриии!

Мдам, програмистите имат великото умение да измислят невероятни имена на тривиални концепции.

За какво са нужни мутаторите?

Отговор с две думи? За програмиране. Да, точно така, до момента правихме много мощни процедури, голяма красота и т.н. Само че липсваше един забележимо основен елемент. Присвояване на стойност. Мдам, при цялото величие на Lisp/Scheme, в момента не можем да направим следните неща:

x = 5
x = x - 3

Точно така, днес ще правим оператор равно.

set!, set-car!, set-cdr!

Новите процедури, които ще ни трябват се казват set!, set-car! и set-cdr!. Общото между тях? Имат удивителни накрая. Точно така се означават в Lisp/Scheme операциите, които променят списъци. Наричат се още деструктори, защото след тях списъкът вече не е същият1.

(set! a (cons b c)) съпоставя точковата двойка (b.c) на a в текущата среда. Тоест, става следната работа:

(define a (cons 0 1))
(define b 2)
(define c 3)
a
> (0 . 1)
(set! a (cons b c))
a
> (2 . 3)

set! може да се използва на всичко, на което очакваш и на нищо, което не очакваш. Т.е. следните неща не са валиден Scheme.

(set! 1000 (cons b c))
(set! nil (cons b c))

Ако не можеш да се сетиш какво правят set-car! и set-cdr!, моля, върни се обратно в първи курс и започни отначало :)
(set-car! a b)
(set-cdr! a b)

И това бяха примитивните начини за променяне на обекти. Всички други начини минават през тези.

Кога се ползват

Да кажем, че е нужно да се напише програма. На Scheme. Тя трябва да може да… променя някаква стойност. Точно така. Ей, колко удобен пример за днескашния урок! Вчера цял ден го мислих. Точно така, програма, която приема a, b и c и съпоставя на а двойката (b . c).

(define d (cons 5 5))
(define e 3)
(define f 4)
(define (replace! a b c)
  (set! a (cons b c)
  a
)
(replace! '(1) 3 5)
> (3 . 5)
(replace! d 3 5)
> (3 . 5)

Окей, това работи, супер. И всичко е наред, докато не погледнем какво има в d.
d
> (5 . 5)

Ъъ… ама то не се е променило. Защо това е така? Защото set! пипа в текущата среда. Малко по-подробно. Какво прави (set! a b)? Променя указателя към a - той вече сочи към b. Само че този указател в някой хубав момент изчезва. А именно - когато програмата излезе от replace!. Защото на replace! точно това му е подадено - указател към стойността, което е в a. Сериозно, надявам се УП и ООП в първи курс да не са били проблем, защото тук се работи с указатели, независимо дали ги виждаш или не. Така де, ще използваме друга магия.
(define (replace2! a b c)
  (set-car! a b)
  (set-cdr! a c)
  a
)
(replace2! d e f)
> (3 . 4)
d
> (3 . 4)

Защо този път работи различно? Защото променяме не това, което сочи към a. Променяме това, към което сочи a. Ето един пример на C++:
#include<iostream>
using namespace std;

int a = 1, b = 2;

void move_pointer(int * a, int * b)
{
    //Това прави replace!
    a = b;
    cout<<"In: move_pointer | a = "<<*a<<endl;    //Тук вътре a = 2
    return;
}
void change_pointer(int * a, int * b)
{
    //Това прави replace2!
    *a = *b;
    cout<<"In: change_pointer | a = "<<*a<<endl;    //Тук също a = 2
    return;
}

int main()
{
    cout<<"In: main | a = "<<a<<endl;    //В началото е 1.
    move_pointer(&a, &b);
    cout<<"In: main | a = "<<a<<endl;    //Все още е 1.
    change_pointer(&a, &b);
    cout<<"In: main | a = "<<a<<endl;    //Вече е 2.
    return 0;
}

Точно това е и разликата между set! и set-car!/set-cdr!.

Кога не се ползват

Мутаторите са зло. Старите Lisp програмисти считат за лош стил да имаш дори един деструктор в програмата си. Казват, че Ричард Сталман успял да напише триста хиляди реда код, без веднъж да напише удивителна. За един ден. Ти сега сигурно си казваш "Айде бе, за едно равно такава олелия". И нормално - всеки, на който първият му език е бил C/Java/Paskal/нещо-от-сорта така ще реагира. Защо е зле да се използват деструктивни операции? Виж следния код:

(define a 5)
(define b (* a a))
> 25

Какво прави b? Повдига числото a на квадрат. Връща 25. Винаги. А какво прави това:
(define (b a) 
  (lambda (x)
    (set! a (- a x))
    a
  )
)

(let ((c (b 5)))
  (display (c 2))
> 3
  (display (c 2))
> 1
)

Малко куц пример, ама хайде сега. Във всеки случай, човек би очаквал, че (c 2) като се извика два пъти ще направи, съответно върне, два пъти едно и също нещо. Да, ама не. Деструкторите чупят функционалната парадигма, а освен това с тях програмата се проследява много по-трудно. Нещо повече, човек въпреки всичко може бързо да види какво прави една програма с деструктори, но компютър - друг път.
Ето какво прави компилаторът, когато види горния код, в който нямаше мутатори:

Яяя, какъв хубав код. Ще взема да го пооптимизирам малко. А такаа, сега работи за 0.000000001 секунда.

А ето какво става, когато види кода с мутаторите.

Оф, това тука(почес, почес)… бе де да знам. Може и да може да се пипне нещо, ама ще видим. Знаеш ли какво, я го остави тука, ела да си го вземеш след две седмици и ще му сложа ново окачване, а?

Компилаторът не може да оптимизира код, за който не се знае какво прави. А мутаторите правят кода точно такъв.
Следствие 1: Функционалният код е доста по-бърз от обикновения.
Следствие 2: Функционален код се пише по-трудно.

Освен това, мутаторите чупят почти всички неща, които сме правили до момента и ще правим вбъдеще.
Сещаш ли се за процедурата map? Е, използвайки map от предишните уроци, пробвай този код:

(define l (list 1 2 3 4 5))
(map (lambda (x) (set-cdr! (cddr l) 5)) l)

A? Как е? Ами това?
(map (lambda (x) (set-cdr! l l)) l)

Ок, ако си пуснал последния ред и имаш стар компютър, вероятно сега ме мразиш. Да, това е безкраен цикъл. Да, препълнил ти е рама, да, трябваше да рестартираш. Животът е гаден, свиквай. За всичко това обвинявам деструкторите. Те развалят хубавия стил на програмиране и правят живота на програмистите гаден. Въобще не се връзват с функционалния стил, направо ще си помислиш, че са от друга парадигма.
Не ги използвай, освен ако не е напълно наложително.
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 3.0 License