27/04/2013

Przeszukiwanie przestrzeni stanów

Home

Post ten stanowi fragment serii na temat przeszukiwania przestrzeni stanów.

Zacznijmy od rozwiązania zadania z poprzedniego posta. Poprawna sekwencja portów to:

Gdańsk
Szczecin
Kołobrzeg
Szczecin
Gdańsk
Gdańsk
Kołobrzeg
Kołobrzeg
Szczecin
Gdańsk
Szczecin
Kołobrzeg
Szczecin
Gdańsk
Gdańsk
Kołobrzeg
Kołobrzeg
Szczecin
Gdańsk

Zapewne można do niej dojść stosując algorytm na chłopski rozum czyli:
  1. Wiemy, że ostatnim portem był Szczecin.
  2. Patrzymy więc na ostatnie wpisy w dziennikach z Gdańska i Kołobrzegu.
  3. Okazuje się, że do Szczecina statek przypłynął z Gdańska.
  4. Wykreślamy ten wpis.
  5. Patrzymy na ostatnie wpisy w dziennikach z Szczecina i Kołobrzegu.
  6. Okazuje się, że do Gdańska statek przypłynął z Kołobrzegu.
  7. Wykreślamy ten wpis.
  8. Patrzymy na ostatnie wpisy w dziennikach z Szczecina i Gdańska.
  9. Okazuje się, że do Kołobrzegu statek mógł przypłynąć Szczecina lub Gdańska i musimy rozważyć oba scenariusze.
  10. ...
Dla dużej liczby portów jest to zadanie karkołomne. Ja przy takich problemach stosuję przeszukiwanie przestrzeni stanów (ang. State space search), w skrócie PPS. Podejście to pozwala rozwiązać wiele problemów algorytmicznych, które pozornie wydają się bardzo trudne, w prosty sposób. Między innymi stosowane jest w uczeniu maszyn, warto więc je znać.

PPS zakłada, że problem reprezentujemy przy pomocy:
  • Definicji stanu początkowego.
  • Formuły, która powie nam jakie stany można odwiedzić, z bieżącego stanu. Albo inaczej zbioru akcji, które powodują zmianę stanu.
  • Warunku stopu, który powie nam czy znaleźliśmy rozwiązanie.
  • Opcjonalnie funkcji kosztu, która pozwala nam wybrać lepsze, bardziej optymalne rozwiązanie.
Na tej podstawie PPS znajduje sekwencję akcji prowadzących od stanu początkowego do rozwiązania. Dla opisanego przeze mnie problemy wygląda to tak:
  • Definicji stanu początkowego - port końcowy + stan dzienników.
  • Formuła/Akcje - Znalezienie listy portów, z których statek mógł przypłynąć do bieżącego portu. Wybranie portu oznacza dodanie go do listy już odwiedzonych portów oraz wykreślenie odpowiedniego wpisu z dziennika.
  • Warunku stopu - Wszystkie dzienniki są puste.
  • Funkcja kosztu - brak.
Stan to para składająca się z aktualnej listy odwiedzonych portów oraz aktualnego stanu dzienników. Mając definicję problemu możemy przejść do implementacji o czym napiszę w kolejnym poście.

26/04/2013

Zadanie do pogłówkowania

Home

Post ten stanowi pierwszy z serii, w której opisze podejście do rozwiązywania pewnej klasy problemów. W ostatnim czasie po raz kolejny zastosowałem to podejście do rozwiązania problemu, jaki napotkałem, i dlatego postanowiłem o tym napisać. Na początek proponuję zastanowić się nad takim zadaniem.

Treść zadania:

Załóżmy, że statek podróżuje pomiędzy pewną liczną N portów. Za każdym razem kiedy zawinie do portu jest to odnotowywane przez kapitanat w dzienniku. Kapitanat zapisuje również informacje o kolejnym docelowym porcie podróży.

Mając zbiór dzienników ze wszystkich portów, oraz port końcowy należy odtworzyć trasę podróży statku. Statek może wielokrotnie odwiedzać ten sam port. Statek może również wypłynąć z portu A i do niego wrócić.

Niestety dzienniki są prowadzone niechlujnie i nie możemy polegać na datach wpisów. Wpisy są natomiast ułożone chronologicznie w ramach dziennika, czyli jeśli wpis A jest wcześniej w danym dzienniku niż wpis B to znaczy, że statek najpierw odpłynął do A, wrócił po jakimś czasie i popłynął do B.

Przykład 1:

Rozważmy przypadek z 3 portami Szczecin, Gdańsk oraz Kołobrzeg. Portem końcowym jest Szczecin. Dzienniki dla pewnego statku wyglądają w następujący sposób:

SzczecinGdańskKołobrzeg
GdańskKołobrzegGdańsk
KołobrzegKołobrzegSzczecin
SzczecinGdańsk

Z dziennika dla Szczecina możemy odczytać, że statek najpierw odpłynął do Gdańska potem wrócił i odpłynął do Kołobrzegu itd.

Rozwiązanie 1:

Rozwiązaniem jest następująca trasa:

Szczecin
Gdańsk
Kołobrzeg
Gdańsk
Kołobrzeg
Szczecin
Kołobrzeg
Gdańsk
Szczecin

Teraz w ramach ćwiczeń, zanim opiszę swoje podejście, proponuję rozwiązać problem dla poniższych danych. Port końcowy to Gdańsk.

Przykład 2:

GdańskSzczecinKołobrzeg
SzczecinKołobrzegSzczecin
GdańskGdańskKołobrzeg
KołobrzegGdańskSzczecin
SzczecinKołobrzegSzczecin
GdańskGdańskKołobrzeg
KołobrzegGdańskSzczecin

11/04/2013

Codility

Home

Jestem wielkim zwolennikiem sprawdzania kandydatów na programistów przy pomocy zadań wymagających napisania kodu. Sam również byłem egzaminowany w ten sposób nie raz i nie dwa. W pamięci zapadły mi jednak rekrutacje z udziałem portalu Codility, który weryfikuje nie tylko poprawność kodu ale również jego wydajność, za każdym razem było to dla mnie ciekawe wyzwanie.

Postanowiłem więc skontaktować się z Codility i zapytać czy w ofercie mają produkt pozwalający programistom ćwiczyć ich umiejętności. Odpowiedź na zapytanie dostałem bardzo szybko i niestety okazała się negatywna, ale zostałem zaproszony do ich biura w Warszawie aby porozmawiać o tym pomyśle.

Trochę to trwało zanim udało się nam ustalić termin spotkania, ale w końcu pewnego popołudnia wsiadłem w tramwaj i pojechałem na Plac Bankowy. Na miejscu przywitała mnie przemiła Czeszka Zuzana, miałem okazję poznać zespół pracujący nad rozwojem Codility oraz porozmawiać o ich pracy. Ponieważ nie miałem wcześniej okazji korzystać z portalu od strony rekrutera pokazano mi jak to wygląda.

Na koniec wręczono mi upominek w postaci książki Looking For a Challenge? z opisem kilkudziesięciu ciekawych zadań programistycznych, przygotowanych przez zwycięzców międzynarodowych konkursów programistycznych.

À propos problemów algorytmicznych, dowiedziałem się również, że część zadań Codility dostępna jest w Internecie dla każdego programisty, ale nie wszystkie łatwo znaleźć. Poniżej, dzięki uprzejmości Codility, macie ich pełną listę. Lista ta z czasem będzie z czasem rozszerzana o tzw. zadania well known czyli takie, które są dobrze znane i nie ma sensu przy ich pomocy testować kandydatów ale idealnie nadają się do ćwiczeń.
Wizytę wspominam bardzo miło. Tym bardziej, że Codility odwiedziłem nie jako klient, ale jako "człowiek z ulicy". Cieszy również, że to polski start-up odnoszący sukcesy na świecie.

23/03/2013

Migracja bazy danych w EF

Home

Już dawno temu pisałem o zastosowaniu RavenDB w swoim projekcie. Początkowo byłem zadowolony z tego wyboru ale jakiś czas temu zdecydowałem się przejść na Entity Framework + SQLCe. Czemu? To temat ta cały post, tak w skrócie to zirytowało mnie to, że RavenDB stara się być mądrzejszy od programisty oraz to jak trudno osiągnąć w nim niektóre rzeczy. Nie twierdzę, że RavenDB jest zły, ale tak jak inne dokumentowe bazy danych nie do wszystkiego się nadaje.

Wracając do tematu, przechodząc do EF zdecydowałem się wykorzystać podejście code first. Wcześniej tego nie próbowałem i muszę powiedzieć, że byłem bardzo zadowolony, migracja odbyła się w miarę bezboleśnie. Swoją drogą zaprocentowała początkowa decyzja aby ukryć technologię dostępu do danych za dobrze określonym interfejsem. Dzięki temu zmiany musiałem wprowadzić w niewielu miejscach. Nie ma jednak róży bez końców. Kiedy po jakimś czasie chciałem dodać nową właściwość do swoich encji otrzymywałem błąd:

The model backing the context has changed since the database was created...

Za pierwszym razem poradziłem sobie z tym tworząc bazę danych od początku i importując dane ale na dłuższą metę byłoby to uciążliwe. Zacząłem kombinować nad rozwiązaniem, które zmieniłoby schemat bazy danych przed użyciem EF. Wtedy natknąłem się na artykuł opisujący to zagadnienie i był to strzał w dziesiątkę. Oto krótki opis co zrobiłem:.
  • Dodałem nową właściwość do encji.
  • Zmodyfikowałem logikę biznesową.
  • Uruchomiłem Package Manager Console.
  • Jako Default project wybrałem projekt, z klasę dziedziczącą po DbContext. Jeśli takich klas jest więcej zostanie zgłoszony błąd.
  • Wydałem polecenie Enable-Migrations.
  • Do projektu został dodany katalog Migrations, a w nim klasa Configuration.
  • Wydałem polecenie Add-Migration tj. Add-Migration AddColumn.
  • W projekcie została utworzona klasa AddColumn dziedzicząca po DbMigration. Dla mnie interesujące była metoda Up, w której umieściłem kod odpowiedzialny za uaktualnienie bazy danych. Wyglądało to w następujący sposób:

    public override void Up()
    {
       AddColumn("TranslationEntities", "Translation2", c => c.String(true, maxLength: 4000, storeType: "nvarchar"));
    }
    

    Metoda AddColumn to metoda klasy DbMigration. Jest ich dużo więcej np.: AddForeignKey, CreateIndex czy RenameColumn.
  • Na koniec użyłem klasy MigrateDatabaseToLatestVersion w celu zainicjowania bazy danych:

    Database.SetInitializer(new MigrateDatabaseToLatestVersion<ExpressionsDataContext, Configuration>());

  • Przy kolejnym uruchomieniu aplikacji zadziały się dwie rzeczy. Do tabelki TranslationEntities została dodana nowa kolumna oraz utworzona została nowa tabela __MigrationHistory. Dzięki tej nowej tabelce EF będzie śledził historię wprowadzanych zmian i nie uruchomi tej samej migracji więcej niż raz.
To bardzo prosty przykład. Jestem ciekawy jak ta funkcjonalność spisuje się w bardziej skomplikowanych scenariuszach. Czy macie jakieś doświadczenia?

22/03/2013

Konwencja wołania

Home

Konwencja wołania (ang. Calling convention) to zestaw zasad, który określa w jaki sposób metoda wołająca przekazuje parametry do metody wołanej i odbiera od niej wyniki. Programując w .NET, o ile nie współpracujemy z kodem natywnym, w ogóle nie musimy się tym interesować. Zgłębiając ten temat można jednak nauczyć się kilku ciekawych rzeczy.

Zacznijmy od tego jakiej konwencji wołania używa CLR. Na to pytanie można odpowiedzieć na dwa sposoby. Odpowiedź "wysoko poziomowa" brzmi: CLR został zaimplemntowany jako maszyna stosowa i nie używa rejestrów, a więc parametry oraz wyniki zostaną przekazane na stosie. Oto prosty przykład, zacznijmy od klasy:

public class Test
{
        public string ReturnString(int i, bool b)
        {
            return i.ToString() + b.ToString();
        }

        public string ReturnString(Fun f)
        {
            return f.ToString();
        }
}

Teraz spójrzmy na kod w C# i odpowiadający mu kod IL, w którym użyto tych metod:

var t = new Test();

newobj instance void ConsoleApplication1.Test::.ctor()
stloc.0 

var res1 = t.ReturnString(10, true);

ldloc.0 
ldc.i4.s 10
ldc.i4.1 
callvirt instance string ConsoleApplication1.Test::ReturnString(int32, bool)
stloc.1 

var res2 = t.ReturnString(new Fun());

ldloc.0
newobj instance void ConsoleApplication1.Fun::.ctor()
callvirt instance string ConsoleApplication1.Test::ReturnString(class ConsoleApplication1.Fun)
stloc.2 

Istotne fragmenty zaznaczyłem na czerwono. ldloc.0 odpowiada za wrzucenie na stos obiektu, na rzecz którego zostanie wywołana metoda (this). Potem wrzucamy argumenty dla wołanej metody czyli ldc.i4.s 10 oraz ldc.i4.1. Po wywołaniu metody zdejmujemy natomiast wynik ze stosu i zapisujemy w zmiennej lokalnej o indeksie 1 przy pomocy komendy stloc.1. Dla drugiego wywołania wygląda to podobnie. Polecenie newobj tworzy nowy obiekt i umieszcza referencję do niego na stosie.

Czemu jednak CLR został zaimplementowany jako automat stosowy (maszyna wirtualna Java zresztą też), a rejestry pojawiają się dopiero kiedy program jest wykonywany i IL jest zamieniany na kod natywny? Otóż CLR to komercyjna implementacja VES (ang. Virtual Execution System). VES jest częścią standardu ECMA-335, który jest niezależny od docelowej architektury, a w szczególności nie wspomina nic o żadnych rejestrach. Dzięki temu konkretne implementacji standardu mogą używać rejestrów w dowolny sposób.

I tutaj dochodzimy do odpowiedzi "nisko poziomowej" na postawione pytanie, czyli jakiej konwencji używa kod natywny wygenerowany na podstawie IL dla Windows'a. Otóż jest to konwencja fastcall, w której na przykład dla architektury x86 dwa pierwsze parametry przekazywane są w rejestrach ECX oraz EDX. Więcej szczegółów na ten temat można znaleźć na blogu Joe Duffy'ego.

Od siebie dodam, że jeśli korzystamy z P/Invoke to domyślnie metody natywne wołane są przy pomocy konwencji Winapi, która w systemie Windows jest tożsama z Stdcall. Zachowanie to możemy zmienić przy pomocy właściwości CallingConvention atrybutu DllImportAttribute.