My Profile Photo

kubuszok.pl


Na codzień po prostu deweloper bez żadnego X z przodu.

Lubię uczyć się nowych rzeczy, zwłaszcza bardziej abstrakcyjnych jak matematyka czy algorytmika.

Obecnie pracuję w Scali, gdyż dla mnie programowanie funkcyjne jest bliższe matematyki, bardziej estetyczne i czyste. Mam również doświadczenie komercyne w pracy z językami imperatywnymi jak Java i C++, jak również kilka projektów w innych językach.


Pierwsze kroki

Dawno temu czytałem, pewien artykuł o nauce programowania. Można go streścić mniej więcej tak: na początku ludzie robią pewne założenie jak działa język programowania. Później stosunkowo niewielu z nich jest w stanie samemu je zmienić. Tak więc to, czy ktoś od razy złapie bakcyla, czy może się odbije, zależy od tego, czy założenia jakie poczynił na początku przypadkiem się pokrywają z tym jak dany język faktycznie działa.

Eksperyment był przeprowadzany na laboratoriach z języka C. Podobne, powrarzane później doświadczenia prawdopodobnie też. Jednym z pytań, które silnie pokazywało, kto się przebije przez kurs, a kto polegnie, wyglądało mniej więcej tak:

a = 10
b = 20
a = b

jakie na koniec są wartości a oraz b?

Dla większości programistów odpowiedź na to pytanie jest banalna: zarówno a jak i b mają wartość 20. Ale jednak, jeśli się nad tym zastanowić odpowiedź wcale nie jest banalna i oczywista.

Różne modele

Każdy z nas na matematyce poznał znak =. Jeśli go postawimy między dwoma wartościami, oznacza to, że uznajemy je za równe. Jeśli nie są, równe to zdanie logiczne jest fałszywe. Teraz, spróbujmy odczytać jeszcze raz powyższe zadanie używając wyniesionego z klasy rozumowania:

  1. a jest równe 10 (tak więc za każdym razem jak widzimy, a możmy myśleć 10 i na odwrót)
  2. b jest równe 20 (podobnie jak wyżej)
  3. postawiliśmy zdanie logiczne a jest równe b. Wiedząc, że pierwsza wartość to 10, a druga to 20, widzimy, że postawiono nam zdanie 10 = 20, które jest oczywistym fałszem
  4. uznajemy, że 2 pierwsze zdania opisują nam wartości a i b, zaś trzecie zdanie nie ma sensu.

Ale jak w takim razie programiści dochodzą do wniosku, że a i b są równe 20? Mniej więcej tak:

  1. nadajemy a wartość 10
  2. nadajemy b wartość 20
  3. nadajemy a wartość b, które zawiera 20 - od tej pory a posiada nową wartość!
  4. zarówno a jak i b posiadają wartość 20.

Pokazuje nam to zasadniczą różnicę w rozumowanie. W matematyce zmienna to pewna nazwa. Używamy jej w zastępstwie konkretnej wartości, ale w danym kontekście ta wartość jest niezmienna. Gdy podajemy wzór na pole trójkąta:

$s = \frac{ah}{2}$

nie uważamy, że a, b i s możemy dowolnie podmieniać - określamy natomiast pewien stały związek - jeśli w danym kontekście przez s oznaczymy pole trójkąta, przez a jego podstawę, zaś przez h wysokość, to aby wartości miały sens, muszą zachodzić stosunki opisane wzorem. Kiedy jednak już wyznaczymy kontekst - np. wskazując palcem na konkretny trójkąt - to w danym kontekście wartości nie zmienią się nigdy. Dla odróżnienia stała w matematyce również jest pewnym aliasem, jednak takim, który zachowuje swoją wartość niezależnie od kontekstu. Np. niezależnie od tego na jakie koło patrzymy liczba pi będzie zawsze tą samą liczbą.

Jeśli jednak zaczniemy programować, te 2 słowa zmienią jednak znaczenie. W programowaniu zmienna to kontener na wartość, którą możemy w dowolnym momencie nadpisać. Dlatego też możemy powiedzieć teraz a będzie oznaczało 10, a już za chwilę później a teraz a będzie zawierało wartość b. To co rozumiemy w matematyce przez zmienną - wartość ustalona, ale tylko w pewnym kontekście - jest w programowaniu stałą. To co w matematyce jest stałą często również.

Okazuje się, że są to 2 całkowicie różne filozofie podejścia do programowania i, co istotne, każda z nich posiada języki dla których tylko ona jest poprawna. Być może studenci, który odpadli na pierwszym semestrze poradziliby sobie lepiej, gdyby trafili na język z tej drugiej szkoły.

Podejście imperatywne

Zmienność (inaczej: mutowalność) wartości zmiennych oraz obiektów leży u podstaw programowania imperatywnego. W tej filozofii wywodzącej się z teoretycznego modelu maszyny Turinga (tego gościa od Enigmy, granego w Grze Tajemnic przez Benedicta Cumberbatcha) opisujemy kolejny kroki jakie ma wykonać maszyna/nasz program: weź te dane stąd, zrób z nimi to i tamto, jeśli wartość zmiennej jest taka, a taka zrób to i to, w przeciwnym razie tamto.

# znajdź 5 pierwszych 5-literowych imion
names = ["Jackson", "Aiden", "Lucas", "Liam", "Noah",
         "Ethan", "Mason", "Caden", "Oliver", "Elijah"]

result = []             # utwórz pusty wynik
for name in names:      # dla każdego imienia z listy
  if len(name) == 5:    # sprawdź, czy imię ma 5-liter
    result.append(name) # jeśli tak dodaj je do wyniku
  if len(result) >= 5:  # jeśli zebraliśmy już 5 imion
    break               # zakończmy szukanie

print(result)

Jest to model dość mało przyjemny dla człowieka. Zmusza do zejścia na poziom maszyny i rozumowania w takich kategoriach w jakich operuje komputer. (Do pewnego stopnia może wyjaśniać to dlaczego, niektórzy programiści są jakoś skrzywieni).

Ma ono tylko jedną zaletę - ponieważ myślisz jak komputer i zapisujesz rozkazy w natywnym języku komputera, programy mają szansę być niezwykle wydaje w działaniu. Każda optymalizacja wymaga zrozumienia, jakie dokładnie operacje wykonuje komputer, co jest (względnie) łatwe, jeśli opis działania oraz stan faktyczny nie różnią się specjalnie.

Ta jedyna zaleta, często jest jednak decydująca.

Podejście deklaratywne

Podejście deklaratywne opiera się na definiowaniu zależności między kolejnymi wynikami pośrednimi:

# znajdź 5 pierwszych 5-literowych imion
names = ["Jackson", "Aiden", "Lucas", "Liam", "Noah",
         "Ethan", "Mason", "Caden", "Oliver", "Elijah"]

# weź wartość każdego 5-literowego imienia z listy
# zaś z powstałej listy weź 5 pierwszych elementów
result = [name for name in names if len(name) == 5][:5]

print(result)

Często (po opanowaniu składni i konwenci) okazuje się być krótsze, czytelniejsze i mniej podatne na błędy niż podejście imperatywne. Niestety, jest też nieco mniej wydajne - skupia się bowiem na czytelności dla człowieka, a nie komputera.

Dość dobrze łączy się z programowanie funkcyjnym, pod które podstawy teoretyczne podłożył rachunek Lambda Churcha (nawiasem mówiąc promotora doktoratu Turinga).