Programiści, którzy „zasmakowali” pracy w językach wysokiego poziomu, takich jak m. in. PHP, bardzo często zapominają o możliwości wykorzystania bardzo niskich mechanizmów do osiągnięcia większej elastyczności kodu. Z reguły wykorzystujemy w kodzie różne wzorce projektowe i inne ułatwienia wprowadzone wraz z paradygmatem obiektowym programowania i myślimy za pomocą dużych komponentów, zamiast skorzystać z tego, co istnieje i ma się dobrze od co najmniej kilkunastu lat. Niniejszy wpis będzie poświęcony jednej z takich właśnie możliwości – wykorzystaniu flag bitowych. Zapraszam do lektury.
Fotografia: Bull3t, CC-BY-SA.
Problem: duża ilość parametrów.
Ci, którzy mieli styczność z językiem C czy C++ na pewno zetknęli się kiedyś z problemem przekazywania ustawień czy opcji do odpowiedniej funkcji. Weźmy pod uwagę przykładowy kod:
/**
* Trim passed string using selected method.
*
* @param string $string Passed string.
* @param bool $trimLeft Trim left side.
* @param bool $trimRight Trim right side.
* @param bool $trimBoth Trim both sides.
* @return string Trimmed string.
*/
function customTrim($string, $trimLeft, $trimRight, $trimBoth)
{
$result = '';
if(true == $trimLeft)
{
$result = ltrim($str);
}
else if(true == $trimRight)
{
$result = rtrim($string);
}
else if(true == $trimBoth)
{
$result = trim($string);
}
return $result;
}
Funkcja ta nie jest specjalnie ambitna, ale dosyć dobrze pokazuje problem – w momencie, kiedy chcielibyśmy, aby jej działanie było sterowane za pomocą kilku binarnych przełączników musimy umieścić w deklaracji funkcji bardzo dużą liczbę parametrów, z których tylko jeden lub kilka ma faktyczne znaczenie i wpływ na wartość wynikową. Oczywiście możemy zmienić listę parametrów na przekazanie jednej tablicy:
/**
* Trim passed string using selected method.
*
* @param string $string Passed string.
* @param array $options Trim options.
* @return string Trimmed string.
*/
function customTrim($string, $options)
{
$result = '';
if(true == $options['trimLeft'])
{
$result = ltrim($str);
}
else if(true == $options['trimRight'])
{
$result = rtrim($string);
}
else if(true == $options['trimBoth'])
{
$result = trim($string);
}
return $result;
}
Jednakże w tym przypadku alokujemy wręcz monstrualną ilość pamięci do tak prostej czynności jak wykonanie jednego z wariantów funkcji trim().
Flagi bitowe: najlepsza kompresja przekazywanych informacji.
Czym są flagi bitowe? Otóż, jak każdy wie – najmniejszą jednostką pamięci komputera dostępną dla programisty jest bit. 8 bitów stanowi 1 bajt, a dalej już wchodzą „standardowe” modyfikatory ilości – kilobajt, megabajt, gigabajt, itd. Większość programistów nie zważa na ilość pamięci, jaką zajmują ich programy / skrypty, stąd nie zauważa także potencjału tkwiącego w pojedynczej zmiennej zawierającej po prostu… liczbę całkowitą. Zauważmy, że w przypadku zwykłej zmiennej typu int, zajmującej 1 bajt w pamięci mamy aż 8 możliwych stanów – przełączników dla każdego bitu:
1: 00000001 173: 10101101 255: 11111111
Każdy z tych stanów możemy odczytać z liczby z pomocą operatora bitowego AND:
define('TRIM_LEFT', 1); // 00000001
define('TRIM_RIGHT', 2); // 00000010
define('TRIM_BOTH', 4); // 00000100
$state = 173; // 10101101
echo $state & TRIM_LEFT; // 1
echo $state & TRIM_RIGHT; // 0
echo $state & TRIM_BOTH; // 4
Flaga musi być potęgą dwójki, tzn. musi zawierać tylko jedną „jedynkę” w reprezentacji binarnej. Wynikiem operacji jest liczba odpowiadająca konfiguracji bitów, jakie były ustawione na tych samych pozycjach w obu liczbach, a więc w zależności od tego, czy trafimy w odpowiednią „jedynkę” zostanie zwrócona wartość flagi lub zero.
Aby nie musieć za każdym razem przemnażać kolejnych flag przez 2, możemy też wykorzystać operator przesunięcia bitowego. W taki przypadku definiujemy tylko pierwszą flagę o wartości jeden, a następnie „przesuwamy jedynkę” o kolejną ilość miejsc w prawo:
define('TRIM_LEFT', 1);
define('TRIM_RIGHT', TRIM_LEFT << 1);
define('TRIM_BOTH', TRIM_LEFT << 2);
Spróbujmy wykorzystać te informacje, aby zmienić naszą funkcję customTrim() w nieco bardziej konfigurowalny twór.
Rozwiązanie: parametr zawierający flagi bitowe.
Wykorzystując flagi bitowe zdefiniowane wyżej kod funkcji będzie wyglądał następująco:
/**
* Trim passed string using selected method.
*
* @param string $string Passed string.
* @param int $flags Trim options.
* @return string Trimmed string.
*/
function customTrim3($string, $flags)
{
$result = $string;
if($flags & TRIM_LEFT)
{
$result = ltrim($result);
}
if($flags & TRIM_RIGHT)
{
$result = rtrim($result);
}
if($flags & TRIM_BOTH)
{
$result = trim($result);
}
return $result;
}
Jak widać, zyskujemy zarówno na ilości parametrów [przekazujemy tylko string i flagi], jak i na pamięci [cały parametr zawierający flagi to... pojedynczy "int"]. Mówiąc o optymalizacji za pomocą flag bitowych wypada także powiedzieć kilka słów na temat sposobu wywołania takiej funkcji. Aby stworzyć wartość odpowiadającą odpowiedniemu zbiorowi flag posłużymy się operatorem binarnym OR:
echo 'x'.customTrim3(' STR STR ', TRIM_LEFT).'x'."\n";
echo 'x'.customTrim3(' STR STR ', TRIM_RIGHT).'x'."\n";
echo 'x'.customTrim3(' STR STR ', TRIM_LEFT | TRIM_RIGHT).'x'."\n";
echo 'x'.customTrim3(' STR STR ', TRIM_BOTH).'x'."\n";
// result:
xSTR STR x
x STR STRx
xSTR STRx
xSTR STRx
A zatem aby przekazać kilka wartości poszczególnych przełączników należy je po prostu połączyć operatorem binarnym OR.
BTW. Zdaję sobie sprawę z tego, że omówiony przeze mnie przykład jest „mocno średni”, dlatego jeśli macie nieco lepszy pomysł na funkcję przedstawiającą opisywany problem, proszę podzielcie się nimi w komentarzach. Do zobaczenia w piątek!
Warto przeczytać.
Trwa ładowanie…
IMHO wybrałeś trochę niefortunny przykład, bo definicja TRIM_BOTH mija się z celem – po to właśnie definiujemy flagi, żeby w przypadku obcinania z obu stron użyć TRIM_LEFT | TRIM_RIGHT (i mam dzięki temu 1 if mnie w cutsomTrim3). Ja w ogóle pozostałbym w tym przypadku przy użyciu standardowych funkcji PHP, a jeśli już naprawdę byłoby konieczne, to zdefiniowałbym to jako customTrim($string, $trimLeft, $trimRight), (2 ostatnie typu bool), albo customTrim($string, $type), gdzie $type to np. ‘l’, ‘r’, ‘m’. Nie mówię, że flagi są be, bo sam niejednokrotnie ich użyłem, marudzę tylko na podany przykład zastosowania :)
Napisałem to w ostatnim akapicie tego posta. Niestety nie byłem w stanie wymyślić niczego lepszego – wpadł mi do głowy trim() i nie chciał wpuścić nic innego. Jeśli masz inny pomysł, chętnie podmienię trim’a na cokolwiek innego, bardziej sensownego. ;]
Linkdump 35- CSS – Coś Strasznie Stylowego
A dobra, zwracam honor, dopiero teraz przeczytałem ostatni akapit, w którym sam zwracasz uwagę na „mocno średni przykład”. Już nie marudze :)
No i nie zdążyłem ze sprostowaniem przed Twoją odpowiedzią. Jeden z chyba najpopularniejszych przykładów użycia flag (jak dla mnie „trochę” przegięty) to MessageBox z WinApi: http://msdn.microsoft.com/en-us/library/ms645505%28v=vs.85%29.aspx
Marudź, marudź, bo inaczej wpadłbym w jakiś narcyzm, a tak przynajmniej wiem, że są błędy. Tak jak mówię, w głowie mam totalną pustkę jeśli chodzi o lepsze przykłady. ;]
Dodałem Ciebie do whitelisty komentujących – zapraszam do częstszego udzielania się na moim blogu.
Linkdump 35- CSS – Coś Strasznie Stylowego
To ciekawy przykład, przyznaję – sam kiedyś z tego korzystałem programując w C++. Gdybyś tylko miał coś bardziej związanego z PHP, to byłoby miło. ;]
Linkdump 35- CSS – Coś Strasznie Stylowego
No wiesz, ale na przykładzie opiera się Twój cały post ;) Trzeba było się trochę wstrzymać.
Ja bitowce widzę wyłącznie w zarządzaniu uprawnieniami usera w prostszych systemach. Trim? Wolę wbudowane funkcje.
4developers 2011 &8211 czy było warto
Mi się zdarzyło zwracać stan 6 różnych rzeczy z funkcji, takie tam moje małe rozwiązanie. I tak jak piszesz – zapalałem sobie określone bity i zwracałem określoną liczbę. „Kod błędu” 45 oznacza, że moje bity były zapalane w ten sposób: 101101. Dzięki temu od razu wiadomo, które części zwróciły jaki wynik.
Przykład? Bugtracker. Ticket może mieć statusy np: otwarty, zamknięty, otwarty ponownie, w trakcie realizacji. Ale równocześnie chciałbyś wiedzieć, dlaczego został zamknięty? Czyli „nie stwierdzono”, „u mnie działa”, „używasz IE 6, to Twoja wina” albo został otwarty ponownie, bo jednak coś nie jest naprawione albo trzeba jeszcze coś dodać.
Wtedy robisz sobie kolumnę w bazie „status”, w komentarzach tabeli dajesz informacje co oznacza każdy bit, a w klasie modelu, która mapuje Ci na tą tabelę ustawiasz consty.
Minusy takiego rozwiązania:
* nikt Ci nie broni zrobić równocześnie ticketa zamkniętego i otwartego – takie sprawdzanie musiałbyś przeprowadzić w modelu – AR np.
* przeglądając czyste wpisy w bazie danych nie wiesz od razu, jaki jest status błędu.
* w przypadku chęci dodania nowego statusu potrzebny jest programista.
* niektórzy pewnie mi powiedzą, że taka tabela nie jest zgodna z sztuką tworzenia baz danych, normalizacja i te sprawy. Ale czasami wymagania biznesowe wygrywają z normalizacją.
* jesteś ograniczony typem pola = liczbą bitów
Inny przykład to uprawnienia – FUDForum miało cały system uprawnień oparty na systemie bitowym.
4developers 2011 Warszawa – relacja
Jeśli chodzi o zarządzanie uprawnieniami, to ja wolę jednak bazę danych z tabelami od ACLa – ciężko byłoby mi się pogodzić z faktem, że zaszywam na sztywno w kodzie tak ważne informacje jak lista dostępnych ról, czyli uprawnień.
Zgadza się, na przykładzie opiera się cały post – spróbuję pokombinować z inną funkcją, ale nadal pusto w głowie… Any suggestions? ;]
Linkdump 35- CSS – Coś Strasznie Stylowego
Ależ singles… Nie trzeba w modelu sprawdzać w tym wypadku, gdyż mamy tutaj wzajemne wykluczenie :) Wystarczy jeden bit dać na otwartość, a drugi na ponowienie i otrzymujemy wszystkie 4 kombinacje, ticket otwarty (zamknięty), ticket ponownie otwarty (zamknięty).
Prawdziwą niedogodnością flag bitowych jest ich nieelastyczność. Raz stworzony w oparciu o nie system ciężko modyfikować. Można jedynie rozszerzać funkcjonalność poprzez majstrowanie z niewykorzystanymi bitami. Ale wymaga taki system mimo wszystko BARDZO dobrego przemyślenia go i niemal od początku bezbłędnej implementacji, gdyż wszelkie zmiany wpłyna mocno na zachowanie.
Czyli jeszcze inny sposób – ja bardziej myślałem o konfigurowaniu wywołania. ;] Swoją drogą, ciekawe, czy Twój sposób dałoby się połączyć z rozwiązaniem opisanym przeze mnie tutaj: http://blog.kowalczyk.cc/2010/11/12/php-zwracanie-wielu-wartosci-z-funkcji/ . Może w jakiś sposób udałoby się odebrać w list() poszczególne bity zwracanej wartości?
Linkdump 35- CSS – Coś Strasznie Stylowego
Zgadza się – powiedziałbym Ci, że potrzebna jest jeszcze dwie tabele z listą stanów i relacją do do listy ticketów. Jedyne rozwiązanie, w jakim dopuściłbym mieszanie danych w kodzie i w bazie danych to ta, w której naprawdę potrzebowałbym ekstremalnej wydajności.
Tylko, że podany przez Ciebie sposób to nadal jest w jakiś sposób operowanie flagami, a nie konfigurowanie wywołania, o czym głównie był ten wpis. Z drugiej strony temat wpisu brzmi jednak „flagi bitowe”, więc już widzę, że jednak wprowadziłem sam trochę zamieszania. Muszę pomyśleć nad refactoringiem tego postu. ;]
Linkdump 35- CSS – Coś Strasznie Stylowego
Jeszcze jako ciekawostkę podam takie info, że mój wykładowca na PG ETI kazał nam w podobny sposób odpowiadać na testach. Z tego co pamiętam wyglądało to mniej więcej tak, że sumowało się różne odpowiedzi i dawało wynik testu podając liczbę całkowitą w dziesiętnym ;)
Wymyślasz… Takie sprawy jak read/write/view się nie zmieniają i świetnie nadają, aby w bazie zapisać właśnie bitowo
4developers 2011 &8211 czy było warto
Czyli co, było dwadzieścia pytań i do każdego dwadzieścia odpowiedzi, jak odpowiedziałeś poprawnie, to „jedynka” wskakiwała na odpowiednie miejsce w punktacji? ;]
Linkdump 35- CSS – Coś Strasznie Stylowego
Zgadzam się, dlatego opisałem problem flag bitowych na przykładzie konfigurowania wywołania funkcji, ponieważ tutaj z dużym prawdopodobieństwem możemy opisać większość wymagań, jakie jej postawimy. Wszystkie inne, „zmienne” możliwości opisałbym jednak jako relacje tabel w bazie danych.
Linkdump 35- CSS – Coś Strasznie Stylowego
To jeszcze zależy od tego, jaki model uprawnień przyjmiemy – jeśli będzie to model pozwól / zabroń albo poziomowy, to możemy odpowiednie pola zapisać na sztywno, bo nawet jeśli się zmienią, to tylko w zakresie przewidzianym w systemie. Podtrzymuję jednak swoje zdanie w przypadku zastosowania ACLa albo innego wariantu RBACa.
Linkdump 35- CSS – Coś Strasznie Stylowego
Chyba nie do końca, jak przypomnę sobie jak to działało to napiszę ;)
Czekam niecierpliwie. ;]
Linkdump 35- CSS – Coś Strasznie Stylowego
To zależy jak zaimplementujesz. Zauważ, że w drugim bicie, się powtarzasz. W pierwszym masz otwarty/zamknięty. W drugim masz ponownie otwarty(powtórzenie) i ponownie zamknięty(powtórzenie). Równie dobrze możesz to zaimplementować na zasadzie: bit 1 = otwarty/zamknięty, bit 2 = ponownie/po raz pierwszy.
Albo inny przykład. Zamykasz ticketa i masz trzy opcje: „powtórzono”, „nie stwierdzono”, „brak supportu dla wybranej przeglądarki, tak więc nie błąd”. Nie zmieścisz tego w jednym bicie, musisz mieć dwa. I wtedy baza nie zabroni Ci ustawić równocześnie „nie stwierdzono” i „brak supportu”, a sensu to nie ma żadnego.
4developers 2011 Warszawa – relacja
Rozumiem, że to odpowiedź do komentarza @thek’a?
Nadal upierałbym się jednak przy kilkutabelowej relacji, chociaż po krótkim przemyśleniu uznałem, że masz rację mówiąc w innym komentarzu o sprawdzaniu stanu w modelu, ponieważ wielopoziomowe stany w stylu „ROZWIĄZANY NAPRAWIONY” będą wymagały określenia, które z nich mogą być razem ustawione na sztywno w kodzie – tutaj relacje bazy danych i jakieś kombinowanie z dodatkową relacją sprawdzającą byłoby już przesytem.
Linkdump 35- CSS – Coś Strasznie Stylowego
Tak, do@thek’a – wybacz, nie zaznaczyłem.
@Tomek – chodzi właśnie o to, że czasami to „prawidłowe” wymaga ąż za dużo pracy i wcale nie sprawdza się tak dobrze.
4developers 2011 Warszawa – relacja
Jasne, dlatego dopuszczam inne wersje pod pretekstem wydajności, czy spójności systemu. W momencie, kiedy jednak miałbym wolną rękę w implementacji takiego rozwiązania, skłaniałbym się za „porządnym”. ;]
Linkdump 35- CSS – Coś Strasznie Stylowego
@singles: zauważ, że ticket ponownie otwarty i po raz pierwszy otwarty wynikają same z wartości bitów. Raz 00, a raz 10. To czy ticket będzie otwarty 2 czy 100 razy nas w tym momencie nie interesuje. Dla nas ważny jest stan. Informacje typu „brak supportu dla przeglądarki X” i wszelkie formy, gdzie występują więcej niż 2 możliwe stany powinny iść albo do komentarzy albo innej formy zapisu informacji. My możemy najwyżej określić stopień: 0 – w całości wyeliminowany, 1 – częściowo wyeliminowany i szczegóły podać w komentarzu od problem zależy. To już jednak rozważania akademickie w tym momencie i zależne mocno od przypadków użycia, określonej implementacji w systemie. Musielibyśmy tutaj od zera przykład napisać i jego wdrożenie zrobić. W tej formie jak teraz przykład nasz wisi mam 3 bity informacyjne i tabelę komentarzy do ticketów. I sądże, że to dobre rozwiązanie, gdyż mamy zarówno niezbędne informacje, jak i szczegóły tyczace informacji o tickecie oraz ewentualnych problemach z nim czy też doprecyzowanie.
Ciekawy pomysł:)
A coś więcej? ;]
Linkdump 35- CSS – Coś Strasznie Stylowego
@thek, ok – przekonałeś mnie, ze podany przeze mnie przykład nie wymaga walidacji po stronie modelu – mój błąd:)
Mam jeszcze jeden, który lepiej się nadaje, ale to wymaga wprowadzenia w realia pewnej gry, którą kiedyś robiłem. A to duuużo czasu;)
4developers 2011 Warszawa – relacja
@singles: A tam błąd od razu ;) Po prostu przyjąłeś inne założenia i tyle, czy też może inaczej podszedłeś do problemu. Jak wspomniałem, wiele zależy od pomysłu na implementację flag i trzeba na wstępnym etapie dużo posiedzieć. Czasem warto wyjść poza same flagi w inne formy, co ja zrobiłem jako komentarze do ticketów w przykładzie. Nie zawsze jest bowiem sens trzymania się ich kurczowo. Tak więc Twoje podejście nie ma w sobie nic złego, ale jest po prostu nieco inne. To co Ty chciałeś przenieść na inna warstwę (do modelu), ja nieco inaczej rozwiązałem, ale czy dobrze to też nie mogę arbitralnie stwierdzić. Wiele bowiem zależy od tego do czego konkretnie i jakie przypadki zakłada projekt. A bez tego dyskusja jest czysto teoretyczna.
Pomysł stary jak świat ;)
FireFox 4 wypuszczony na polowanie w sieci!
Cokolwiek byś nie napisał, będzie stare jak świat. ;]
PHP- Flagi bitowe
Napisz proszę artykuł jak w PHP obsługiwać typy unsigned int (DWORD) z C/C++, to jest dopiero ciekawe :)
Przeczesałem Internet, aby sprawdzić, czy nie wymagasz ode mnie czegoś trywialnego – okazuje się, że nie wymagasz. Dopisuję w takim razie do listy artykułów z priorytetem „istotny”. ;]
Linkdump 36- CSS3
Ostatnio miałem „przyjemność” przepisywania kodu C++ na PHP, mała biblioteka, szyfrowanie było tam (czyli operacje rotacji, shifty, + – * / mod) operacje na 32 bitowych unsigned typach, do tego dochodziło operowanie na binarnych strukturach danych z C++ (pomaga trochę pack/unpack), rzutowania etc., ale ogólne wrażenia mam takie, że jest to jedno z mniej przyjemnych doświadczeń programistycznych :)
Ogólnie przepisywanie kodu z jakiegokolwiek języka na inny nie jest zbyt przyjemną czynnością. A przynajmniej nie taką, jaką statystyczny programista chciałby się zajmować 24/7. ;] Częścią takiej właśnie problematyki zajmuję się w tworzonym aktualnie wpisie, przed północą się ukaże. ;]
Linkdump 36- CSS3
Pingback: PHP: Odczytywanie wartości bajtów liczby całkowitej. « Tomasz Kowalczyk
Pingback: PHP: Obsługa wartości typu DWORD. « Tomasz Kowalczyk
Jak tak jeszcze trochę poczytam co piszesz to może jeszcze uwierzę, że php nie jest tak potężnie ograniczony, ale to jeszcze trochę Ci brakuje. Swoją drogą ciekawy tekst, nie przyszło mi do głowy, żeby implementować w php jednak dość niskopoziomowe zagrywki ( o ile można tak powiedzieć)
Dzień gry- dzień drugi
Bo PHP nie jest ograniczony, co najwyżej inwencja programisty nie pozwala mu się wybić na odpowiedni poziom. Ja opisuję to, co udało mi się zgłębić na tyle dokładnie, żeby Czytelnikom nie opowiedzieć jakichś bzdur. Ale jak widzę, co robią niektórzy ludzie w swoim kodzie, to się trochę głupio czuję – dlatego cały czas dziubię. ;]
Skoro mówisz, że jeszcze trochę brakuje – może jakieś sugestie? O czym chciałbyś poczytać, żeby się przekonać? Chętnie opracuję tematy, jakie podasz. ;]
Linkdump 37- jQuery
W pierwszym listingu tu byłby błąd:
chyba, że string === str ;)
Jeśli chodzi o pozostałe, to całkiem logicznie i po kolei. Uważam, jednak, że należałoby zrobić albo tak:
function customTrim3($string, $flags) { $result = $string; if($flags & TRIM_LEFT) { $result = ltrim($result); } if($flags & TRIM_RIGHT) { $result .= rtrim($result); } if($flags & TRIM_BOTH) { $result .= trim($result); } return $result; }Albo zmienić te ify na else ify, albo zrobić tak:
function customTrim3($string, $flags) { $result = $string; if($flags & TRIM_LEFT) { return ltrim($result); } if($flags & TRIM_RIGHT) { return rtrim($result); } if($flags & TRIM_BOTH) { return trim($result); } throw UnknownFlag('Flag ' . $flags); } class UnknownFlag extends Exception {}W Twoim kodzie tak naprawdę nawet jeśli przekażemy złożoną flagę, to i tak będzie w wyniku jedynie ostatnia sprawdzana opcja.
jeśli chodzi o Exception to zapraszam: http://www.yarpo.pl/2010/11/29/hierarchia-klas-wyjatkow/
Wiesz, testowałem ten kod i zdawał się działać, ale zdaję sobie sprawę z tego, że pewnie sytuacje brzegowe jak zwykle dadzą się we znaki. Dzięki za nową wersję i komentarz. ;]
Potyczki Algorytmiczne 2011- Zadanie próbne „Tulipany”