Programowanie równoległe w Erlangu

22.07.2010 - Marek Materzok
TrudnośćTrudność

Termy i dopasowanie wzorca

Na razie jedynymi wartościami, jakich używaliśmy, były liczby. Nie jest to oczywiście jedyny rodzaj wartości w Erlangu. Wartości w Erlangu nazywamy termami. Nie posiadają one typów - każdy term można użyć w dowolnym miejscu w programie. Różne rodzaje termów można rozróżnić za pomocą instrukcji warunkowych, ale najczęściej wykorzystuje się do tego tak zwane dopasowanie wzorca. Więcej o nim później.

Najprostszym rodzajem termu w Erlangu, jeszcze prostszym od liczb są tak zwane atomy. Atomem jest dowolne słowo zaczynające się małą literą (To dlatego nazwy zmiennych muszą się zaczynać wielką literą!), ponadto ciągi znaków zamknięty znakami ' (pojedynczy cudzysłów) również są traktowane jako atomy - na przykład, 'to jest atom' jest atomem. Atomy można porównywać ze sobą. Sprawdzić, czy dany term jest atomem, można za pomocą funkcji is_atom.

Do czego mogą się przydawać atomy? Przede wszystkim zastępują występujące w innych językach enumeracje, używa się ich też do oznaczania rodzajów komunikatów, o czym później. Atomy oznaczają też nazwy funkcji i modułów, co jest wykorzystane w poniższym kodzie:

test() -> X = inna, X(). % wywołuje inna()

Warto też zauważyć, że w Erlangu brak osobnych wartości logicznych. Wartość logiczną prawdy reprezentuje atom true, a fałszu - false.

Kolejnym rodzajem termu jest krotka. Krotki zbierają w jeden term kilka dowolnych innych termów. Na przykład {celsius, 36.6} jest parą (krotką dwuelementową) zawierającą jako pierwszy element atom celsius, a jako drugi liczbę 36.6. Taka krotka może oznaczać temperaturę wraz z jej jednostką. Alternatywnie, można by utworzyć krotkę {fahrenheit, 97.9}. Sprawdzić, czy dany term jest krotką, można za pomocą funkcji is_tuple.

W jaki sposób wydostać z krotki zawarte w niej termy? Wprawdzie istnieje funkcja element, wydobywająca n-ty term z krotki (na przykład element(2, {celsius, 36.6}) oblicza się do 36.6), ale nie jest to bardzo wygodne. Prościej jest skorzystać z instrukcji przypisania, na przykład tak:

first(X) -> {A, B} = X, A.

Jest to prosta odmiana wspomnianego wcześniej dopasowania wzorca. Jeśli X jest parą, powyższa instrukcja przypisania wykona się i przypisze do A i B pierwszy i drugi element pary, w przeciwnym wypadku nastąpi błąd.

Jeszcze prościej jest zrobić tak:

first({A, _}) -> A.

Jak widać, dopasowania wzorca można używać również w parametrach funkcji. Użyty tu też został symbol _, oznaczający, że nie jesteśmy zainteresowani wartością znajdującą się w tym miejscu.

Pamiętacie, że możemy definiować funkcje przez kilka klauzul? Nie ma problemu, aby w różnych klauzulach pojawiały się różne wzorce. Dla przykładu, poniższa funkcja konwertuje temperaturę do stopni Celsjusza:

to_celsius({celsius, X}) -> {celsius, X};
to_celsius({fahrenheit, X}) -> {celsius, (X-32)*5/9};
to_celsius({kelvin, X}) -> {celsius, X-273.15}.

Istnieje również instrukcja, która pozwala na warunkowe wykonywanie kodu w zależności od wyniku dopasowania wzorca - case. Za pomocą tej instrukcji powyższy kod można (mniej ładnie) napisać tak:

to_celsius(E) ->
    case E of
        {celsius, X} -> {celsius, X};
        {fahrenheit, X} -> {celsius, (X-32)*5/9};
        {kelvin, X} -> {celsius, X-273.15}
    end.
Snippet icon Napisz analogicznie funkcję to_fahrenheit, obliczającą wartość temperatury w skali Fahrenheita.

Instrukcję case można również używać jako instrukcji warunkowej pozbawionej ograniczeń na postać wyrażenia w instrukcji if w następujący sposób:

case jakiś_warunek of
    true -> jeśli_prawda;
    false -> jeśli_fałsz
end

Na koniec wspomnę jeszcze o jednym rodzaju termów - listach. Listy są podobne do krotek, mogą jednak mieścić dowolną liczbę elementów. Można zapisywać je, wymieniając wszystkie elementy, na przykład tak: [1,1,2,3,5]. Tej samej konstrukcji można też używać przy dopasowaniu wzorca. Jednak pisząc funkcje przetwarzające listy, lepiej skorzystać z innej formy zapisu. Napis [A|AS] oznacza listę, której pierwszym elementem jest A, a pozostałymi elementami AS. Zatem, jeśli wykonamy poniższy kod:

[A|AS] = [1,1,2,3,5]

wartością A będzie 1, zaś AS będzie równe [1,2,3,5]. Funkcję obliczającą długość listy można napisać tak:

length(L) -> length(L, 0).
length([], K) -> K;
length([_|AS], K) -> length(AS, K+1).
Snippet icon Napisz analogicznie funkcję sumlist, obliczającą sumę liczb w liście. Pamiętaj, że suma listy pustej wynosi 0!

Na tym zakończę omówienie podstaw programowania w Erlangu. W dalszej części zajmiemy się tym, co zmotywowało nas do nauki Erlanga, czyli programowaniem równoległym.

Programowanie równoległe w Erlangu - podstawy

Podstawowym elementem programu równoległego jest proces - czyli obliczenie wykonywane niezależnie od pozostałych. W Erlangu utworzenie procesu jest wyjątkowo proste - wystarczy użyć funkcji spawn w następujący sposób:

spawn(fun () -> ...kod procesu... end)

Użyta tu konstrukcja fun oznacza funkcję anonimową. Utworzony przez spawn proces będzie wykonywać kod tej funkcji i zakończy działanie, gdy ta funkcja zostanie wykonana w całości lub jej wykonanie zakończy się błędem.

Wynikiem funkcji spawn jest pid, czyli identyfikator procesu. Identyfikator ten umożliwia komunikowanie się z procesem za pomocą operacji wysłania komunikatu.:

Pid ! Komunikat

Treścią komunikatu może być dowolny term - w szczególności może on zawierać identyfikatory innych procesów. Komunikaty dostarczane są w sposób asynchroniczny - wysłany komunikat trafia do skrzynki odbiorczej procesu docelowego, a proces nadający kontynuuje działanie nie czekając na odebranie komunikatu. Wysyłanie komunikatu nigdy nie wywołuje błędu.

Komunikaty odbiera się za pomocą instrukcji receive. Instrukcja ta jest zbliżona do poznanej wcześniej instrukcji case, różnica polega na tym, że wzorce dopasowywane są nie do zadanego termu, a do otrzymanych komunikatów. Ze skrzynki odbiorczej procesu wyjmowany jest pierwszy komunikat, który dopasowuje się do któregoś z podanych wzorców i wykonywany jest odpowiedni fragment kodu. Na przykład, poniższy kod odbiera komunikat w postaci albo listy dwuelementowej, albo krotki dwuelementowej, i oblicza sumę elementów:

test() ->
    receive
        [X, Y] -> X+Y;
        {X, Y} -> X+Y
    end.
Snippet icon Napisz funkcję pomnozDo(Pid), która odbiera w komunikacie parę liczb i wysyła do procesu Pid ich iloczyn.

A co, jeśli czekamy na komunikat, który nigdy nie nadejdzie - na przykład dlatego, że proces, który miał go nadać, przestał działać z powodu błędu? Wystarczy dodać na końcu instrukcji sekcję after, która mówi, co należy wykonać, jeśli w czasie zadanej liczby mikrosekund nie pojawi się żaden komunikat. Oto przykład:

test() ->
    receive
        {dodaj, X, Y} -> X+Y;
        {pomnoz, X, Y} -> X*Y
    after
        1000 -> za_pozno
    end.

Przy okazji, powyższy kod jest ilustracją często używanego schematu oznaczania rodzaju komunikatu za pomocą atomu w pierwszym elemencie krotki.

Omówione powyżej elementy języka wystarczają, żeby zacząć pisać równoległe programy. Poniżej przekonamy się, w jaki sposób ich użyć, aby zbudować bardziej złożony program.

Snippet icon Eksperymentuj! Napisz dowolny program zawierający funkcję test(). Twoja funkcja zostanie wykonana, a jej wynik zostanie wypisany poniżej.
4.666665
Twoja ocena: Brak Ocena: 4.7 (3 ocen)

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com