2015-07-15

To goto or not to goto

Ovo nije post o tome da li da idem ili ne, jeste da je i potencijalni odlazak vezan za programiranje, ali ovo je priča o goto, omrženoj komandi. Svi koji se bave programiranjem su u ranoj fazi (osim ako nisu kao neki, započeli fortranom kao prvim jezikom) naučili da se goto ne koristi. Da samo loši programeri koriste goto. Da je to fuj-no-no-kakano i ko samo i pomisli na goto oćelaviće prerano, oslepeće, zavrsiće kao profesor informatike, ... .

Tako sam se i ja klonio goto komande, izbegavao je u širokom luku. Uvek sam bio svestan da postoji, nekada me je continue ili break podsećalo na goto, ali njih niko nije toliko ispljuvao :). I tako jednog dana sam radio nešto što se često radi kada se programira za kernel. Napisao "lep" kod (bez goto), ali ček, previše je, da kažemo, nečitljiv. Hm, ali ako bi iskoristio goto ... . Ne, ne, ne i ne. Kroz glavu mi prolazi ono ćelavenje, ćorav već jesam, ali profesor informatike ... . Imao sam strašnu dilemu, nisam znao šta da radim. Sa jedne strane "pravilo" kojim su me vaspitali, sa druge strane čitljivost i praktičnost koda.

Bacio sam se dosta na istraživanje, čitao pametnice (koje su uglavnom smislili i pisali Linux kernel) koje savetuju da je ok koristiti tu i tamo goto zarad čitljivosti koda. I finalno sam presudio. Na ovom mestu i u ovoj situaciji goto je najbolje rešenje. Ovo sada nije da Vam se pravdam, samo nekada se treba zapitati da li su sve dogme dobre (reče čovek koji uporno radi u c-u).

Da izložimo slučaj, predstaviću dva koda napisana bez upotrebe goto, i jedan napisan sa goto, pa sami presudite šta je bolje, ili još bolje dajte predlog novog koda bez goto. Zadatak je bio da se odradi niz od recimo 5 (u mom realnom životu je bilo 12) operacija. Ali ako neka od njih ne uspe, potrebno je opozvati prethodno pozvane. Mozda zvuci složeno, ali da evo pojednostavimo: pozivamo doA(), pa doB(), ali ako nije uspeo doB(), potrebno je pozvati undoA(). I tako do naših 5 poziva.

response_t work(void)
{
  if(doA() == fail)
    return fail;
  if(doB() == fail)
  {
    undoA();
    return fail;
  }
  if(doC() == fail)
  {
    undoB();
    undoA();
    return fail;
  }
  if(doD() == fail)
  {
    undoC();
    undoB();
    undoA();
    return fail;
  }
  if(doE() == fail)
  {
    undoD();
    undoC();
    undoB();
    undoA();
    return fail;
  }
  return done;
}
Ovo smo baš hteli da napišemo, ali čemu ovoliko ponavljanja, ajde da to ugnjezdimo u pitalice.
response_t work(void)
{
  if(doA() != fail)
  {
    if(doB() != fail)
    {
      if(doC() != fail)
      {
        if(doD() != fail)
        {
          if(doE() != fail)
            return done;
          undoD();
        }
        undoC();
      }
      undoB();
    }
    undoA();
  }
}
return fail;
Čisto da jos jednom podsetim, moj realni, životni primer je imao 12 operacija, ovo je samo sa 5 i već vidite da nije jednostavno. Ajde da pokušamo koristeci goto:
response_t work(void)
{
  if(doA() == fail)
    goto failA;
  if(doB() == fail)
    goto failB;
  if(doC() == fail)
    goto failC;
  if(doD() == fail)
    goto failD;
  if(doE() == fail)
    goto failE;
  return done;
failE:
  undoD();
failD:
  undoC();
failC:
  undoB();
failB:
  undoA();
failA:
  return fail;
}
Šta Vam je čitljivije i šta zahteva najmanji mentalni napor da shvatite o čemu se radi. Naravno da se generiše uvek "isti" mašinski kod, ali baš me zanima šta bi ste Vi izabrali. goto ili ne goto?

P.S. Naravno u ovim primerima doX funkcije nemaju argumente i vraćaju uvek isti tip, ali u praksi su svakojake, sa različitim argumentima i različitim povratnim vrednostima :).

2015-06-25

A sada malo o C makroa ...

Šta najviše smeta C++-ašima u C-u. Pa makroi, i onda su ih prvo nazvali štetnim i lošim i zamenili nekim drugim mehanizmima (na sličan način štetnim, po meni). Da ne ulazim sada u tu priču, pokušaću da pokažem neke stvari koje se mogu uraditi standardnim makroima u C-u. Ovog puta izbeći cu priču o x-makroima, koga zanima neka pogleda https://en.wikipedia.org/wiki/X_Macro.

U jednoj od brojnih razmena misljenja sa dragim Mrkijem (uvek nesto naučim posle bar dva minuta priče sa njim) došli smo do retoričkog pitanja: Kako razrešavas u C-u a->b->c->d->e, ali da vrati NULL ako je bilo koji od a, a->b, a->b->c, a->b->c->d baš NULL. Prva ideja sa nekom funkcijom koja bi ... pa ček kako ćes funkciji proslediti ime? Ne ide. Ajde da napišemo konkretno neki kod, pa da vidimo kako bi to moglo da se ulepša, generalizuje, makroizuje.

if
(
  a == NULL ||
  a->b == NULL ||
  a->b->c == NULL ||
  a->b->c->d == NULL
)
  result = NULL;
else
  result = a->b->c->d->e;
Prvo što primećujem je da možemo ovu pitalicu da zamenimo sa omiljenim tenarnim ?:, ali ajde pre toga da razvijem gomilu or-ova. Zašto, pa eto, iskustvo mi govori da to treba uraditi :).
if(a == NULL)
  result = NULL;
else if(a->b == NULL)
  result = NULL;
else if(a->b->c == NULL)
  result = NULL;
else if(a->b->c->d == NULL)
  result = NULL;
else
  result = a->b->c->d->e;
Sada možemo da iskoristomo ?: i dobijemo izraz, koji se može koristiti, pa i dodeliti u neki result:
(a == NULL ? NULL : (a->b == NULL ? NULL : (a->b->c == NULL ? NULL : (a->b->c->d == NULL ? NULL : a->b->c->d->e))))
Kako bi bilo lepo da ovo mozemo da pozovemo recimo sa N(a, b, c, d, e). Eh, steta sto C makroi nisu tako moćni. Ili možda jesu?

Šta primećujem, da ako bi uzeli prva dva argumenta (a i b), zatim proverili da li je a jednako NULL pa vratili ili NULL ili rekurzivni poziv (umesto prva dva argumenta imamo a->b i ostatak). Treba još da obezbedimo izlazak iz rekurzije, što je jednostavno, kada ima samo dva argumenta zaustavi se tu. Ali ono sto je teži deo posla, to je sto C makroi ne podržavaju rekurziju. Šta uraditi?

Ajde da napišemo nekoliko funkcija, dogovorimo se da svaka poziva prehodnu, nazovimo ih N_2, N_3, N_4, ... dokle tako, pa ajde da se dogovorimo i da kazemo da nikada neće biti vise od 16 promenljivih u ovom nizu. Da ispišemo te makroe, barem do N_5:

#define N_2(a, b) (a == NULL ? NULL : a->b)
#define N_3(a, b, c) (a == NULL ? NULL : N_2(a->b, c))
#define N_4(a, b, c, d) (a == NULL ? NULL : N_3(a->b, c, d))
#define N_5(a, b, c, d, e) (a == NULL ? NULL : N_4(a->b, c, d, e))
Odlično, probamo sa gcc -E (prepuštam vama da proverite, ja verujem sebi, za sada :) ). Radi! Dobro, može ovako da se ispiše do N_16, pa i do N_100 ako zatreba i eto. Nije baš praktično, moramo da pozovemo tačnu funkciju, i složeno je za pisanje. Pa ajde da optimizujemo nekako. Primetimo da sve posle trećeg argumenta samo ponavljamo, dakle super, moze moja omiljena forma: ellipsis. Ne znam da li ste primetili, ali ja baš volim ellipsis ...
#define N_2(a, b, ...) (a == NULL ? NULL : a->b)
#define N_3(a, b, ...) (a == NULL ? NULL : N_2(a->b, __VA_ARGS__))
#define N_4(a, b, ...) (a == NULL ? NULL : N_3(a->b, __VA_ARGS__))
#define N_5(a, b, ...) (a == NULL ? NULL : N_4(a->b, __VA_ARGS__))
Ah, kako me nervira ovo ponavljanje oblika (x == NULL ? NULL : y), zameniću ga makrom _N. O istom trošku dodaću i N_0 i N_1, neka se nadju, zatrebaće možda.
#define _N(x, y) (x == NULL ? NULL : y)
#define N_0(...) NULL
#define N_1(a, ...) a
#define N_2(a, b, ...) _N(a, a->b)
#define N_3(a, b, ...) _N(a, N_2(a->b, __VA_ARGS__))
#define N_4(a, b, ...) _N(a, N_3(a->b, __VA_ARGS__))
#define N_5(a, b, ...) _N(a, N_4(a->b, __VA_ARGS__))
Ovo već liči na nešto, kada bi mogli nekako da izračunamo broj argumenata zadatih makrou i pozovemo odgovarajući makro. Nešto oblika:
#define N(...) N_##NUMBER(__VA_ARGS__)(__VA_ARGS__)
Na žalost, ne postoji ništa što moze da izracuna broj argumenata, osim da to sami ne napišemo kao makro. A i tada, nece doci do zamene, pa ajde da ubacimo koji makro više (više ćete saznato o tome kada naučite kako makroi rade, i kada/kako vrše zamenu):
#define N__(n, ...) N_##n(__VA_ARGS__)
#define N_(n, ...) N__(n, __VA_ARGS__)
#define N(...) N_(NUMBER(__VA_ARGS__), __VA_ARGS__)
Sve je to lepo, ali i dalje nam nedostaje makro NUMBER koji računa koliko je bilo argumenata.

Ideja je jednostavna, ako iza __VA_ARGS__ navedemo redom brojeve, pa uzmemo neki na fiksnoj poziciji i dobićemo duzinu. Naravno treba namestiti da lepo radi.

#define NUMBER_(x5, x4, x3, x2, x1, n, ...) n
#define NUMBER(...) NUMBER_(__VA_ARGS__, 5, 4, 3, 2, 1)
Ovo računa broj argumenata samo do duzine 5, ali eto, lako se da produžiti. Ne radi za prazan ulaz, ali i to se može srediti vezivanjem __VA_ARGS__ za prethodni argument. Na žalost to nije deo standarda, ali naravno gcc to guta, nadam se da će uskoro i to da prihvate u standardu. Naime x, ##__VA_ARGS__ će se razviti u samo x (nema zareza) ako je __VA_ARGS__ prazno, inace normalno se radi konkatenacija.
#define NUMBER_(x, x5, x4, x3, x2, x1, n, ...) n
#define NUMBER(...) NUMBER_(x, ##__VA_ARGS__, 5, 4, 3, 2, 1, 0)
Ha, imamo sve, sada možemo da spojimo, i da proširimo do recimo 16:
#define NUMBER_(x, x16, x15, x14, x13, x12, x11, x10, x9, x8, x7, x6, x5, x4, x3, x2, x1, n, ...) n
#define NUMBER(...) NUMBER_(x, ##__VA_ARGS__, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
#define _N(x, y) (x == NULL ? NULL : y)
#define N_0(...) NULL
#define N_1(a, ...) a
#define N_2(a, b, ...) _N(a, a->b)
#define N_3(a, b, ...) _N(a, N_2(a->b, __VA_ARGS__))
#define N_4(a, b, ...) _N(a, N_3(a->b, __VA_ARGS__))
#define N_5(a, b, ...) _N(a, N_4(a->b, __VA_ARGS__))
#define N_6(a, b, ...) _N(a, N_5(a->b, __VA_ARGS__))
#define N_7(a, b, ...) _N(a, N_6(a->b, __VA_ARGS__))
#define N_8(a, b, ...) _N(a, N_7(a->b, __VA_ARGS__))
#define N_9(a, b, ...) _N(a, N_8(a->b, __VA_ARGS__))
#define N_10(a, b, ...) _N(a, N_9(a->b, __VA_ARGS__))
#define N_11(a, b, ...) _N(a, N_10(a->b, __VA_ARGS__))
#define N_12(a, b, ...) _N(a, N_11(a->b, __VA_ARGS__))
#define N_13(a, b, ...) _N(a, N_12(a->b, __VA_ARGS__))
#define N_14(a, b, ...) _N(a, N_13(a->b, __VA_ARGS__))
#define N_15(a, b, ...) _N(a, N_14(a->b, __VA_ARGS__))
#define N_16(a, b, ...) _N(a, N_15(a->b, __VA_ARGS__))
#define N__(n, ...) N_##n(__VA_ARGS__)
#define N_(n, ...) N__(n, __VA_ARGS__)
#define N(...) N_(NUMBER(__VA_ARGS__), __VA_ARGS__)
Ah, a pa zato sam definisao N_0 i N_1 ... sada je i meni jasno :). Ostaje da sami proverite da li radi i da krenete da ozbiljno razmi[ljate o makroima u C-u.

P.S. Ako ste dokoni, uradite na slican nacin makro MAP(f, ...) koji na niz argumenata primenjuje (makro) f().

2015-01-01

A sada malo sed-a

Svako ko je uzeo da uči shell scripting, pre ili kasnije upozna magičnu komandu sed. Malo ko zna sta znači njeno ime, kako je nastala, ali to je nešto što već može da se nadje svuda po netu. Uglavnom svi koriste kao u sledećem primeru
echo mama | sed 's/m/t/g'
Na stranu što to može da se uradi i nekom drugom jednostavnijom komandom (mislim na ovaj konkretni primer). Ali tu staje prica oko sed-a. Oni malo napredniji nauče sed komandu d, pa napišu nešto ovako:
last | sed '1,10\!d'
Dobro, možda im nije zatrebala sva ta genijalnost koju nosi u sebi komanda sed. A sada pravi razlog ovog posta.

Pre neki dan videh na facebook-u ovaj strip:

i u mom redovnom ..., da kažemo lošem ponašanju gde nipodaštavam one koji znaju manje od mene (znam da nije lepo, ali eto trudim se da ih prodrmam :) ) dodjemo do toga da
rm -rf /
neće da radi. Zašto, prosto postoji u samom kodu za rm komandu zaštita koja ne dozvoljava da argument bude / kao i neki alternativni načini zadavanja slash-a.

I naravno kako sam vec drčan, postavih zadatak: obrisati sve diskove ali iz sed-a :). Pre sam već koristio sed da pronadje apsolutnu putanju pozvanog shell skripta (molim one koji se bave jezikom da se ne uzbudjuju, znam da je plurale tantum, ali ne ide mi da kazem shell skripte kada je samo jedna skripta :) ). To se nalazi na mom sajtu kao primer lepog sed-a:

#!/bin/sh
echo $0|sed -e"
      \|^/|!{h
      s|.*|pwd|;e
    G;s|\n|/|};:x
      s|/[^/]*/\.\./|/|
      s|/\./|/|
      s|//|/|
      s|^/\.\.||;tx
      s|/[^/]*$||"
E sada, ako sam rekao da može, onda moram to i da pokažem. Evo ga sed-a koji prikazuje aktivne stalne diskove na sistemu, naravno ne brišem ih, ne želim da budem destruktivan, zaustavio sam se kod toga da ih samo prikaže:
#!/usr/bin/sed -f
1{s|.*|find /sys/block -type l|;e
s|/sys/block/||g;H;:l g;/^\n/!b e;s|^\n\([a-z0-9]*\).*$|cat /sys/block/\1/uevent|;e
/DEVTYPE=disk/!b n;g;s|^\n\([a-z0-9]*\).*$|cat /sys/block/\1/capability|;e
/^50$/!b n;g;s|^\n\([a-z0-9]*\).*$|cat /sys/block/\1/removable|;e
/^0$/!b n;g;s|^\n\([a-z0-9]*\).*$|cat /sys/block/\1/size|;e
/^0$/b n;g;s|^\n\([a-z0-9]*\).*$|\1|;p;:n g;s|^\n[a-z0-9]*\(.*\)$|\1|;h;b l;:e q}
Neću objašnjavati kako tačno radi, to prepuštam onima koji žele da uče, ali ukratko ovaj sed simulira shell i koristi find i cat komande, kao i /sys filesystem. Po startovanju trazi enter da bi zapoceo rad.

Eto, možda će neko sada posvetiti više vremena tome da prouči sed.

A ja, pokušaću da implementiram neku igricu jednostavnu u sed-u.