W tym szaleństwie jest metoda

Albo rzecz o myśleniu programisty

Wstęp

Cześć,
Tym artykułem chciałbym otworzyć krótką serię artykułów pod tytułem „Teoretyczne minimum programisty”. Seria ta skierowana jest raczej dla początkujących programistów oraz tych, którzy dopiero zaczynają swoją przygodę z programowaniem. Moim celem będzie przybliżenie Wam szerokiego spektrum zagadnień związanych z podstawami informatyki. W tym artykule (jako otwierającym) skoncentruję się raczej na filozoficznej i logistycznej niż technicznej stronie inżynierii oprogramowania. Postaram się przedstawić Wam proces wytwarzania oprogramowania, dowiecie się czym jest myślenie obliczeniowe oraz poznacie podstawowe paradygmaty programiwania – trudne słowo, które wytłumaczę za dłuższy moment 😊.

Czym jest oprogramowanie

Na początku musimy sobie odpowiedzieć na pewne z pozoru błahe pytania – czym jest oprogramowanie oraz jak ono powstaje. W zasadzie komputer, od momentu naciśnięcia przycisku „Włącz”, nawet gdy nie jesteśmy jeszcze w stanie na nim nic fizycznie zrobić wykonuje całą masę programów. Najpierw jest to firmware (oprogramowanie układowe, wpalone w pamięć na płycie głównej), który ładuje do pamięci BIOS lub UEFI. One konfigurują wstępnie sprzęt aby mógł wystartować na nim system operacyjny, który dalej ładuje kolejne sterowniki, a później koordynuje pracę różnych procesów oraz zarządza sprzętem. Kiedy wystartuje system operacyjny (taki jak Windows, Linux czy Mac OS), dopiero wtedy możemy na nim uruchamiać nasze aplikacje użytkowe (takiej jak pakiet biurowy, program do edycji zdjęć czy przeglądarka internetowa) lub gry. Nie inaczej działają konsole do gier, smartphone’y, czy routery.

Warto zwrócić uwagę na drobne rozróżnienie – program jest to zbiór instrukcji dla komputera do zrealizowania, podczas gdy proces jest to aplikacja już uruchomiona tzn. system przydzielił jej zasoby (pamięć oraz czas procesora), załadował program do pamięci i zaczął go wykonywać.

Programy często wykorzystują również biblioteki funkcjonalne, dzięki którym nie trzeba pewnych podstawowych funkcji implementować od podstaw. W końcu nikt (a w szczególności korporacje liczące przede wszystkim na zysk) nie lubi odkrywać koła na nowo. Prawda jest taka, że jest wiele obszarów, które są na tyle ogólne (biblioteki do tworzenia graficznych interfejsów użytkownika czy serializacja danych, tj. zapisywanie danych w formacie umożliwiającym ich przesłanie np. przez Internet), że mogą być użyte dla praktycznie każdego scenariusza biznesowego. Między Bogiem i prawdą, gotowe, popularne biblioteki o otwartych źródłach są często solidnie przetestowane, a niektóre funkcjonalności jak kryptografia są zbyt wrażliwe na błędy by implementować je na własną rękę bez dobrego powodu.

Oprogramowanie możemy podzielić zasadniczo względem ich relacji ze sprzętem i systemem operacyjnym:

  • Niskopoziomowe – kontrolujące sprzęt lub komunikujące się z systemem operacyjnym bezpośrednio poprzez wywołania systemowe (funkcje, które system operacyjny udostępnia programistom),
  • Wysokopoziomowe – aplikacja nie wykorzystuje bezpośrednio elektroniki ani funkcji systemu operacyjnego.

Poza tym też możemy je podzielić względem zastosowań:

  • Biblioteka użytkowa – udostępnia zbiór funkcji, które zazwyczaj są na tyle ogólne, że programiści mogą je zastosować w wielu różnych obszarach lub na tyle specyficzne, że kłopotliwym byłoby pisanie ich od podstaw,
  • Aplikacja użytkowa – program, który jest po prostu użyteczny dla użytkownika,
  • Firmware – oprogramowanie dla układów elektronicznych,
  • Oprogramowanie dla systemów wbudowanych – oprogramowanie, które jest wykorzystywane w tzw. systemach wbudowanych, są to komputery, wyspecjalizowane do konkretnych zadań.
Elektronika
HAL – warstwa abstrakcji sprzętu
System operacyjny / sterowniki
 
 
Biblioteka standardowa języka  
Biblioteki użytkowe  
Aplikacje

Jak powstaje oprogramowanie

Teraz, kiedy wiemy, że cebula ma warstwy, ogry mają warstwy oraz oprogramowanie ma warstwy możemy przejść do krótkiego opisu w jaki sposób powstaje oprogramowanie. Omówię to w kilku (mam nadzieję, że krótkich) punktach

1.     Zbieranie wymagań

Podobno programista to osoba, która rozwiąże Twój problem, którego nie miałeś, w sposób, który nie rozumiesz przetwarzając w tym celu snikersy, Coca-Colę oraz kawę w kody programów. Niestety, to nie jest do końca prawda – klienci płacą tylko za sprawne programy (przynajmniej w dostatecznym stopniu), które realizują to czego się od nich oczekuje.

Aby mieć pewność, że wszystko jest jasne tworzy się listę wymagań funkcjonalnych – w świecie automotive’u nierzadko uzupełnionych o uzasadnienie oraz listę przeanalizowanych alternatyw razem z przyczyną ich odrzucenia.

Na potrzeby branży motoryzacyjnej powstał cały zbiór procesów, które należy przejść tworząc oprogramowanie aby zapewnić wysoką jakość produktów. Możecie pogooglować frazę ASPICE albo chwilę zaczekać bo planuję napisać o tym parę słów za jakiś czas). Czasem polemizowałbym z ich sensem gdyż bywają one tak sztywne, że nie wprowadza się poprawek, o ile nie są krytyczne,
gdyż wymuszałoby to tonę dodatkowej papierkowej roboty,

2.     Projektowanie architektury

W tej fazie architekci decydują o podziale aplikacji na mniejsze komponenty funkcjonalne oraz decydują o wewnętrznej strukturze aplikacji oraz planują w jakiej kolejności będą one implementowane.

Zazwyczaj projektując system staramy się zrobić to w taki sposób, by był on maksymalnie rozszerzalny i modyfikowalny.

3.     Implementacja

Chyba najprzyjemniejsza faza tworzenia oprogramowania. Programiści tworzą kody źródłowe w języku programowania, następnie kompilator tłumaczy je do formy zrozumiałej dla komputera (tzw. Języka maszynowego, ten etap kompilacji nazywamy fazą translacji) po czym linker łączy tak wygenerowane pliki z bibliotekami (nazywamy to fazą linkowania lub rzadziej, konsolidacji).

Kod źródłowy 1
Kod źródłowy 2
Biblioteka 1
 
Plik objektowy 1
Plik objektowy 1
Translacja
Translacja
 
Linker
 
 
 
Pliki obiektowe
i biblioteki są przekazywane do  linkera
 
Program / Biblioteka
Linkowanie

Często sam system budowania aplikacji (tj. przekształcania kodu w działającą aplikację) jest projektowany tak aby proces kompilacji był szybszy gdyż przekłada się to na produktywność programisty – szybszy czas budowania, mniejsze straty czasu przy testach. Odczuwalne jest to w szczególności gdy pracuje się z systemami takimi jak Yocto (na szczęście używa się go tylko i wyłącznie do budowania całych dystrybucji systemu Linux)…

Edgar Dijikstra napisał kiedyś równanie:

Programy = Algorytmy + Struktury danych

Niektórzy dopisują tam również sterowanie, gdyż rzadko programy nie wymagają żadnej interakcji ze światem zewnętrznym – choćby w postaci danych.

Wracając do meritum czym jest algorytm? Nazwa wzięła się od łacińskiego algorithmus (oznaczającego obliczenia przy użyciu liczb arabskich) będącego zlatynizowaną formą arabskiego nazwiska Al Chwarizmi, matematyka perskiego z IX wieku.

Komputer ze swojej natury jest sekwencyjny – procesor pobiera z pamięci i wykonuje kolejne instrukcje. Algorytm jest to sekwencja kroków niezbędnych do uzyskania konkretnego efektu. Przykładowo algorytm parzenia kawy mógłby wyglądać następująco.

  1. Weź kawę z półki
  2. Jeśli nie jest zmielona – zmiel ją,
  3. Zaparz wodę,
  4. Zalej wrzącą wodą,
  5. Odczekaj 5 minut żeby się nie poparzyć,
  6. Dodaj cukru dopóki nie będzie odpowiednio słodka.

Widać tutaj trzy niezbędne elementy algorytmu:

  • Sekwencja – kroki są wykonywane jeden po drugim,
  • Bloki warunkowe – niekiedy musimy rozróżnić jakie zachowania podjąć w danych okolicznościach,
  • Iteracje – niektóre kroki będziemy podejmować do momentu spełnienia pewnych warunków brzegowych, wynika z tego jedna z podstawowych zalet komputerów, nie powiedzą Ci po wykonaniu jednego kroku 1000 razy, że są zmęczone 😊.

Struktury danych definiują natomiast w jaki sposób dane są przechowywane w pamięci – co również może wpłynąć na prędkość ich przetwarzania.

W kolejnej części tego artykułu pochylimy się nad tematem wyobraźni obliczeniowej, dzięki której jesteśmy w stanie zaprojektować algorytm (tj. wyspecyfikować kroki, potrzebne do uzyskania oczekiwanego rezultatu).

4.     Testowanie

Dość wredny etap tworzenia oprogramowania… W końcu nikt nie lubi jak wskazuje mu się błędy i niedoróbki 😊.

Testy można podzielić ze względu na sposób ich przeprowadzania tj. manualne (żmudne, pracochłonne, każdy krok musi być ręcznie wykonany) czy automatyczne, albo ze względu na ich funkcję:

  • Testy jednostkowe – pisane przez programistów sprawdzające podstawowe jednostki z których zbudowany jest program,
  • Testy komponentowe – sprawdzają działanie konkretnych komponentów,
  • Testy funkcjonalne – sprawdzają działanie konkretnych funkcjonalności,
  • Testy integracyjne – testują działanie dwóch lub więcej zależnych od siebie komponentów.

Od testerów często oczekuje się certyfikatu ISTQB. Testerzy automatyczni dość często są już także praktycznie programistami choć często oczekiwania od nich są wciąż nieco mniejsze.

Według mnie,  chyba najbardziej upierdliwym elementem tego procesu jest specyfikowanie oraz dokumentacja testów. W moim obecnym projekcie dodatkowo wspieramy tzw. tracing requirement’ów. Przy każdym teście mamy wskazanie na wpis w dokumentacji use case’a testowego oraz odniesienie do wymagań funkcjonalnych – co sprawia, że czasem perspektywa poprawiania komentarzy w kodzie skutecznie zniechęca do wprowadzania poprawek w kodzie.

5.     Wdrażanie i utrzymanie

Z doświadczenia najnudniejszy etap – oprogramowanie jest wprowadzane na systemy produkcyjne, robota programisty sprowadza się zazwyczaj do łatania błędów (popularnie zwanych bug’ami).

W moim obecnym projekcie, pierwsze prototypy naszego komponentu powstawały na normalnym komputerze, dopiero później przy jednym z pierwszych release’ów (jest to moment wydawania nowej wersji w cyklu życia programu) przetestowaliśmy go na płytce, która docelowo trafiała
do samochodu.

Proces, który Wam opisałem nosi nazwę tzw. Waterfall’a, jest to tak zwany kaskadowy model tworzenia oprogramowania. Stosowany jest w sytuacji gdy wymagania są proste i przejrzyste w szczególności do prostych aplikacji.

Modnym ostatnio modelem jest SCRUM (z ang. młyn).

Oprogramowanie powstaje w sposób przyrostowy. Proces jest podzielony na sprinty, na koniec każdego dostarczony jest produkt z coraz większą liczbą funkcji. W SCRUMie każdy sprint ma swój ceremoniał składającego się z szeregu spotkań:

  1. Sprint refinement – w ramach którego wykonuje się wstępną inwestygację zadań do wykonania w najbliższym czasie,
  2. Sprint planning – zespół estymuje ile czasu zajmą zadania i wybiera te, które zostaną zrealizowane w ramach sprintu,
  3. Daily – codzienne spotkanie w czasie których dyskutowane są postępy w realizacji zadań,
  4. Sprint review – ewaluacja sprintu, dyskusje co udało się zrobić a które będą kontynuowane w kolejnym sprint’cie,
  5. Sprint retrospective – wymiana wrażeń na temat pracy w danym sprintcie, problemy, wyzwania i co można poprawić w kooperacji zespołu.

Sam osobiście nie jestem wielkim miłośnikiem tego modelu. O ile pomysł iteracyjnego tworzenia oprogramowania i by nie rozwiązywać na samym początku problemów których jeszcze nie mamy projektując od razu całą aplikację od zera uważam za dobry tak sam SCRUM może być kłopotliwy.

Zależnie od typu zadań i tego jak długo trwają scrumowe gusła (tutaj czyt. potkania 😉) może to drastycznie skracać faktyczny czas na zrealizowanie zadań. Zdarzyło mi się pracować w zespole w którym podzielono nas na dwa podzespoły:

  1. Realizujący nowe funkcjonalności niezależne od HAL’a maszyny,
  2. Dodający funkcjonalności zależne od HAL’a maszyny, które wymagały często dodatkowej analizy starego kodu, dodatkowo napisanego nie do końca zgodnie ze sztuką inżynierii oprogramowania… Strzeżcie się kodu pisanego przez naukowców 😊

Okazało się, że o ile dwutygodniowy okres sprintu dla podzespołu 1 był wystarczający tak w drugim optymalną długością były trzy tygodnie. W szczególności, że refinement’y, planningi, retrospektywy pochłaniały praktycznie około 2-3 dni roboczych (czyli de facto 20-30% czasu sprintu) bo nieraz wypadało się jeszcze do tych spotkań przygotować. W późniejszym okresie udało się to nieco skrócić 😉.

Na szczęście mimo wszystko moje wspomnienia z retrospektyw mimo wszystko są raczej pozytywne – dyskusje i spostrzeżenia dotyczące komunikacji w zespole i analizy co poszło dobrze, jakie problemy się pojawiły dość często faktycznie poprawiały sytuację i umacniały ducha zespołowego a… teraz w końcu przejdę do nieco bardziej filozoficznej części artykułu!

Myślenie obliczeniowe

W akapicie poświęconemu implementacji rozwiązań wspomniałem o pojęciu wyobraźni obliczeniowej. Chcąc zaproponować rozwiązanie algorytmiczne problemu z tzw. realnego świata, analizujemy je zazwyczaj podobnie jak w czasach szkolnych rozwiązywaliśmy zadania z matematyki.

Aby zdefiniować zbiór kroków, które komputer musi wykonać aby rozwiązać problem. Pierwsze co próbujemy zrobić to dokonać dekompozycji problemu tj. rozłożyć problem na mniejsze, które z dużym prawdopodobieństwem będzie łatwiej rozwiązać. Kolejnym krokiem jest dokonanie generalizacji, rozważenia czy istnieją pewne ogólne własności, pomocne w rozwiązaniu zadania. Następnie możemy zastosować logiczne wnioskowanie,aby wybrać użyteczne dla nas własności by koniec końców przejść przez wszystkie proste problemy, wyodrębnione w fazie dekompozycji. Ważnym elementem jest spojrzenie na problem z pewnego poziomu abstrakcji – należy uzmysłowić sobie w jaki sposób elementy prawdziwego świata można przedstawić tak. by rozwiązać zadany problem.

Zatem podstawowymi narzędziami przy projektowaniu algorytmów są:

  • Dekompozycja,
  • Generalizacja
  • Logiczne wnioskowanie
  • Abstrakcja.

Używamy ich, aby rozwiązać problem wciąż na pewnym poziomie abstrakcji a następnie używając już myślenia algorytmicznego definiujemy sekwencję kroków do wykonania dla komputera aby wykonał część obliczeniową.

Często też okazuje się, że wiele problemów sprowadza się do tego samego. Spójrzcie na tak zdefiniowany problem: jaka jest najkrótsza droga pomiędzy Wrocławiem a Warszawą?

Problem ten możemy podzielić na podproblemy:

  1. Jakich danych potrzebujemy?

Najlepiej mapę drogową z podanymi długościami odcinków drogowych.

  • Jak reprezentować mapę?

Możemy wyobrazić sobie każdą wioskę, miasto i miasteczko jako wierzchołek w grafie. Każda droga pomiędzy nimi jest krawędzią o wadze, która odpowiada długości odcinka.

  • Jak znaleźć najkrótszą drogę?

Problem znalezienia najkrótszej ścieżki pomiędzy dwoma wierzchołkami w grafie z wagami dodatnimi (ciężko przejechać -100 km 😊) jest już klasycznym problemem rozwiązujemy go dość często algorytmem Dijikstry (w tym miejscu nie jest istotne jak on działa).

Zatem program odpowiadający na nasz problem mógłby wyglądać tak:

  1. Wczytaj drogi pomiędzy miastami,
  2. Utwórz graf sieci dróg,
  3. Ustaw wierzchołek początkowy na Wrocław,
  4. Ustaw wierzchołek końcowy na Warszawę,
  5. Uruchom algorytm Dijikstry by znalazł najkrótszą drogę z Wrocławia do Warszawy.

Problemy grafowe są dość wdzięczne pod względem liczby zastosowań. Wyobraźcie sobie, że dowódcy wojskowi zajmują się logistyką i planują w jaki sposób zabezpieczyć dostawy na front tak aby zminimalizować ryzyko przerwania trasy kolejowej przez uszkodzone mosty. W tym przypadku, też będziemy wykorzystywać grafy, ale nie interesuje nas odległość a liczba krawędzi więc każda będzie miała wagę 1. Moglibyśmy również użyć algorytmu Dijikstry ale o wiele łatwiej jest to rozwiązać poprzez tzw. przejście grafu wszerz. Dzięki dokładnej analizie problemu jesteśmy w stanie wybrać odpowiednie algorytmy dla naszego problemu.

Grafy mogą być używane także zarówno w kontekście sprawdzania poprawności certyfikatów kryptograficznych, logistyce, jak i analizie sprawności sieci komputerowych i uwierzcie mi na słowo, że jest tego o wiele więcej.

Języki programowania

Kiedy już mamy algorytmy możemy się zastanowić w jaki sposób zrealizować je z wykorzystaniem komputera i tutaj pojawia się nasz ostatni element układanki czyli język programowania. Podstawowym językiem dla komputera jest kod maszynowy czyli zestaw rozkazów procesora reprezentowanych jako ciąg liczb. Prawda jest jednak taka, że w miarę rozwoju technologii używanie go dość szybko okazało się niewygodne i powstały asemblery – rodzina języków programowania,
w której kody poszczególnych operacji zostały zamienione na łatwe do zapamiętania skróty (tzw. mnemoniki). Później powstawały kolejne języki, które miały zwiększyć produktywność programistów oraz naukowców (gdyż oni pierwsi wykorzystywali komputery, najczęściej do celów wojskowych, takich np. jak projekt Manhattan czy przewidywania pogody).

Wiele eksperymentowano… pierwsze języki przypominały trochę eksperymenty małego dziecka, które nie do końca zdaje sobie z tego co potrafi. W ubiegłym wieku powstało ich pewnie co najmniej kilkaset, część z nich wprowadzały nowe pomysły dotyczące paradygmatu programowania (sposobu w jaki możemy myśleć tworząc programy, rozwinę to za chwilę), inne składniowe (niektóre się przyjęły, inne nie) albo zwiększały poziom abstrakcji i uniezależnienia od maszyny. W pewnym momencie zauważycie, że znaczna część z nich jest do siebie w pewnym stopniu podobna, przynajmniej dopóki implementują ten sam paradygmat.

Paradygmaty programowania

Pierwszy paradygmat był prostą konsekwencją tego jak działa procesor. Procesor pobiera z pamięci jedną instrukcję, dekoduje ją i wykonuje. Proces trwa dopóki komputer jest włączony. Instrukcje natomiast są różne… Mogą dodawać, odejmować, zapisywać do pamięci i odczytywać z pamięci i wiele innych ale co ważne w tym momencie dla nas umożliwiają skok do innego obszaru kodu, który może być wykonany pod pewnymi warunkami.

Dość szybko zauważono, że przy użyciu skoków i instrukcji warunkowych procesora można implementujemy pewne podstawowe struktury, którymi są pętle (blok kodu, który jest wykonywany dopóki warunek jest spełniony) oraz bloki warunkowe (fragmenty kodu wykonywane w określonych warunkach). Tak powstało programowanie strukturalne.

Kod maszynowy       AssemblerKod w języku C

Później spostrzeżono, że część kroków w kodzie jest używana na zasadzie kopiuj-wklej – kolejnym, naturalnym etapem było więc wydzielenie osobnych funkcji i łączenie ich w większe moduły, które byłyby ponownie używalne na przestrzeni całego kodu. I tak oto doszliśmy do etapu w którym wynaleziono programowanie proceduralne.

Kolejną ewolucją, która na dobre zagościła w świecie języków programowania jest programowanie obiektowe. Jest to obecnie chyba najpopularniejszy paradygmat a co zabawne zręby idei pojawiły się już… w starożytnej Grecji. Platon z Arystotelesem stworzyli teorię nazywaną dzisiaj teorią idei. Mówi ona, że:

Zgodnie z dominującą wykładnią filozofii (i powyższej alegorii) Platona ogół tego, co istnieje, dzieli się na dwie sfery – sferę przedmiotów fizycznych, wśród których aktualnie żyjemy, i sferę przedmiotów idealnych, które są poza naszym bezpośrednim zasięgiem. Pierwsze mają się do drugich tak, jak cienie do autentycznych rzeczy lub jak niedoskonałe ucieleśnienia do ich doskonałych wzorców. Owe rzeczy lub wzorce – stanowiące „prawdziwszą” lub „lepszą” rzeczywistość – Platon nazwał ideami lub formami.

Skąd wiadomo, że idee istnieją? Jeden z argumentów za ich istnieniem wygląda następująco. Przedmioty fizyczne układają się w grupy (zbiory), których elementy są do siebie podobne. Każdą z tych grup opatrujemy osobnym pojęciem i nazwą.

Źródło: https://antycznahellada.com/2020/03/15/swiat-idei-platona/

W programowaniu proceduralnym dane i procedury nie są ze sobą w żaden sposób związane podczas gdy programowanie obiektowe zmienia to drastycznie. Programiści tworzą klasy, które są swego rodzaju matrycami opisujące świat przedstawiony w programie. Klasy zawierają dane potrzebne do opisu przedmiotu oraz metody czyli zachowania specyficzne, które też mogą być dziedziczone. Ważną cechą języków obiektowych jest hermetyzacja danych – nie wszystkie są dostępne na zewnątrz.

Przykładowo, gdybyśmy pisali program do przechowywania informacji o zwierzętach w ZOO moglibyśmy stworzyć klasę Animal, która przechowywałaby informację o imieniu zwierzęcia i klasy tj. Reptiles czy Mammals, które będą miały bardziej charakterystyczne cechy tych grup gatunków jak np. pełzaj dla płazów i skacz dla ssaków 😉.

Rysunek 1 ClientFactory przy tworzeniu otrzymuje obiekt typu ConfigurationBundle, używa go aby utworzyć konkretny typ klienta

Cechą wspólną dla tych trzech paradygmatów jest to, że reprezentują one grupę paradygmatów imperatywnych – programista opisuje w nich proces wykonania jako sekwencję instrukcji zmieniających stan programu. Przeciwieństwem ich są paradygmaty deklaratywne (ostatnio modne programowanie funkcyjne czy programowanie w logice) w których programista raczej definiuje co ma zostać obliczone. Na przestrzeni ostatnich lat można zauważyć coraz silniejszy wpływ języków funkcyjnych na nowe funkcjonalności w językach imperatywnych. W językach czysto funkcyjnych nie są przechowywane żadne stany oraz funkcje nie są rozumiane jako sekwencje instrukcji do wykonania a raczej w sensie matematycznym, które przekształcają dziedzinę (argumenty) w zbiór wartości.

Programując w logice programista definiuje raczej zbiór relacji, które muszą zajść aby rozwiązać problem.

Język a ekosystem

Pamiętacie jak na początku artykułu wskazałem, że programowanie niskopoziomowe jest to tworzenie oprogramowania na styku systemu operacyjnego lub elektroniki? W przypadku języków programowania podział na języki niskiego poziomu i wysokiego poziomu jest dość subiektywny.

Co do jednego nikt się nie kłóci – na pewno językiem niskopoziomowym jest asembler. Język C? Za pewne niektórzy hardcore’owi niskopoziomowcy by się kłócili, mnie natomiast przekonuje argument, że jest to język w którym ręcznie zarządzamy pamięcią oraz są w nim pisane systemy operacyjne, sterowniki oraz oprogramowanie na mikrokontrolery.

Języki wysokopoziomowe są to języki dzięki którym tworzony kod będzie wyrażał bardziej abstrakcyjne idee. Bardzo często języki te łączą cechy wielu paradygmatów, najpopularniejsze języki łączą w sobie programowanie strukturalne, obiektowe oraz pewne cechy funkcyjne. Przykładem języków wysokopoziomowych są C++, Java, C#, Python, Ruby itd… I z tej listy tak na prawdę jedynie C++ jest kompilowany bezpośrednio do kodu maszynowego.

Języki takie jak Java oraz C# są kompilowane do swego rodzaju kodu bajtowego, który jest wykonywany przez środowisko uruchomieniowe – abstrakcyjną maszynę dzięki której program może być wykonany na dowolnym systemie na którym jest ona dostępna. Python oraz Ruby nie są kompilowane a interpretowane na bieżąco przez interpreter. Najczęściej językami interpretowymi są języki skryptowe (tj. Python, Ruby, Perl czy bash; skrypty były to proste programy wykonywane przez wiersz poleceń aby zautomatyzować pewne czynności administracyjne), chociaż istnieją wyjątki takie jak język D.

Ciekawym przykładem jest C++ – z jednej strony udostępnia wysokopoziomowe funkcje a z drugiej wciąż mamy możliwość programowania niskopoziomowego. Będąc profesjonalnie programistą C++, kiedy spotykam się z kolegami tęskniącymi z asemblerem jestem programistą pracującym na wysokim poziomie abstrakcji, podczas gdy dla kolegów Javowców jestem niskopoziomowcem… Jest to jedyny akceptowalny dla mnie konflikt w poczuciu własnej tożsamości 😊.

Wiecie, tak na dobrą sprawę, na stronie OSDev (wiki.osdev.org) można znaleźć artykuły jak napisać system operacyjny zarówno w języku C, jak i w językach wysokopoziomowych takich jak C++, D, Rust, czy Ada.

Kilka przykładów języków, które warto kojarzyć

Możecie znaleźć na internecie kursy wielu różnych języków programowania. Ja skoncentruję się na tym blogu pewnie na Rust’cie, którego się aktualnie uczę (i co by nie mówić zyskuje ostatnio coraz większą popularność), coś być może pojawi się z C/C++’a z którymi pracuję od lat i nęcącej mnie od jakiegoś czasu Adzie jednak chciałbym podać Wam krótką listę języków programowania, które warto kojarzyć. Zastawienie jest mocno subiektywne i czasem nie do końca poważne 😉. W większości języki te są imperatywne chyba, że wprost napisałem, że jest inaczej (np. jest funkcyjny albo służy do programowania w logice, jest tutaj tylko jeden przedstawiciel tej grupy).

Nazwa językaKomentarz
COBOLWspomniany wcześniej Edgar Dijikstra powiedział kiedyś „Używanie COBOL’a uwstecznia; jego nauczanie powinno być tym samym uznane za przestępstwo kryminalne”. Bardzo stary i nie powstają w nim żadne nowe projekty… O ile wiem programista COBOL’a jest poszukiwany gdy jego poprzednik zmarł.
FortranJęzyk ogólnego przeznaczenia (tzn. można w nim zrobić praktycznie wszystko) choć pierwotnie zaprojektowany do programów obliczniowych… I prawdę mówiąc kojarzy mi się jedynie jako język dla naukowców do obliczeń numerycznych i symulacji.
SchemePierwszy język z tej listy z którym miałem przyjemność obcować chwilę dłużej. Język będący dialektem Lisp’u – prawie legendarnego języka, który szybko stał się najchętniej wybieranym pod kątem badań nad sztuczną inteligencją. Jest to język funkcyjny, którego cała składnia opiera się na listach… czego konsekwencją jest masakrycznie duża liczba nawiasów.
Przykład kodu – implementacja algorytmu szybkiego sortowania
HaskellPodobnie jak poprzednik, jest to język funkcyjny… CZYSTO funkcyjny! (krzyk jest nie przypadkowy) Nie ma stanów, nie ma pętli a jest tylko wszechogarniająca rekurencja 😊 Programiści Haskell’a przez jego leniwość (obliczenia są wykonywane dopiero wtedy kiedy ich wynik jest potrzebny) potrafią stworzyć nieskończoną listę elementów w skończonym czasie. Nie znam go dobrze, ale zawsze sprawiał na mnie wrażenie dość eleganckiego języka.   Przykład kodu – implementacja algorytmu szybkiego sortowania
BasicStary język… Nie jeden programista zaczynał od niego swoją przygodę z programowania np. na komputerach Comodore. Pochodzę z tego pokolenia, których jedynym kontaktem z tym językiem były opowieści starszych kolegów po fachu. Swego czasu Microsoft stworzył na jego podstawie całkiem fajny język do nauki programowania dla młodzieży o nazwie SmallBASIC. Poza tym, chyba stosunkowo nie dawno Microsoft wyrugował aplikację Visual Basic z ich środowiska programowania Visual Studio. Obecnie pozostał on w użyciu chyba jedynie w pakiecie Microsoft Office do obsługi makr.
PascalUczono go w klasie równoległej do mojej kiedy byłem w liceum… Obecnie nie jest zbyt popularny choć miał swój okres chwały. Przez wiele lat, firma Borland (obecnie Embarcadero) tworzyła swój własny dialekt o nazwie Delphi i środowisko programistyczne, które umożliwiało w łatwy sposób tworzyć aplikacje okienkowe.
CPierwszy język, którego byłem uczony na studiach, choć nasz wykładowca nie był jego wielkim fanem. Język, który stanowi lingua franca informatyki i wywarł znaczący wpływ na składnię chyba większości współczesnych języków.   Zaimplementowano w nim jądro Linux’a, do dziś pisze się w nim sterowniki, aplikacje na mikrokontrolery i na wszystko co ma mocno ograniczone zasoby albo platforma na którą piszemy jest tak rzadka, że nic innego nie jest dostępne 😉 Jeśli jest coś co przetrwa apokalipsę zombie to pewnie będzie oprogramowane w C! Wciąż króluje także w świecie mechatroniki motoryzacyjnej za sprawą Classic Autosara.   Jest najbliższy do Torvaldsowego (twórca Linuksa) zdania: „Gdzie się podziały te czasy, gdy mężczyźni byli mężczyznami i sami pisali swoje sterowniki” – programiści C muszą sami zarządzać pamięcią, przez co większość programistów uznaje go za język trudny w obyciu.
C++Język od którego sam zaczynałem moją przygodę z programowaniem… Zanim napiszę o nim parę słów od siebie parę cytatów:
„C pozwoli Ci łatwo odstrzelić sobie stopę; C++ czyni to trudniejszym, ale jak już wybuchnie to rozwali całą nogę” – Bjarne Stroustrup, twórca języka „Ja wcale nie nienawidzę C… Po prostu moje odczucia wobec niego są ambiwalentne” – Scott Meyers, ekspert języka C++, twórca książki „Skuteczny, nowoczesny C++”   Język wysokopoziomowy i obiektowy ale wciąż może być wykorzystywany do tworzenia systemów operacyjnych i oprogramowania na mikrokontrolery… Chociaż tam wciąż spisuje się lepiej C ze względu na mniejsze rozmiary skompilowanego kodu. C++’a używamy obecnie najczęściej w systemach wbudowanych na mocniejszych platformach (np. w automotive, C++ jest językiem wiodącym w platformie Adaptive Autosar) oraz grach komputerowych ze względu na większą produktywność niż w C.   Programista wciąż musi myśleć o zarządzaniu pamięcią, chociaż dzięki tzw. inteligentnym wskaźnikom jest to o wiele przyjemniejsze.
JavaJęzyk obiektowy, który w założeniu miał działać na wszystkim o ile jest dostępna maszyna wirtualna. Obecnie najczęściej wykorzystywany do tworzenia oprogramowania na telefony komórkowe oraz do oprogramowywania backendu tzn. logiki biznesowej aplikacji webowych (np. systemów sprzedaży internetowej e-commerce).   Chyba najbardziej charakterystyczną cechą jest garbage collector – mechanizm, który odpowiada za sprzątanie nieużywanej pamięci… Problem z nim jest taki, że włącza się kiedy chce, istnieje polecenie do wywołania tego mechanizmu, ale maszyna wirtualna traktuje to wyłącznie jako sugestię. Nie jestem specjalnie fanem tego języka, też często przez mentalność programistów, która mówi – „Mam gdzieś zasoby, w razie czego zmienię maszynę na mocniejszą”.
C#Microsoftowa odpowiedź na Javę… i powiedziałbym, że ciut ładniejsza (jeśli kierować się względami estetycznymi). Najczęściej używana do tworzenia aplikacji na Windowsa i backendu aplikacji webowych. Jakiś czas temu powstał projekt o nazwie dotnet Core dzięki któremu możemy korzystać z C# także na Linuksach i Mac’ach
DMoja pierwsza miłość jeśli chodzi o mniej standardowe języki programowania. Język za którym stoi wiele mądrych głów ale niestety zbytnio się nie przyjął – mimo to używano go między innymi w Facebooku. Wspiera programowanie obiektowe, zaprojektowany przez Waltera Bright’a, twórcę pierwszego kompilatora C++ tłumaczącego bezpośrednio do kodu assemblera (wcześniej był tłumaczony najpierw do C a potem C było kompilowane do assemblera).   Cechą, która mi się podobała jest to, że posiada garbage collector, który można wyłączyć gdy potrzebujemy sami zarządzać pamięcią dla zwiększenia wydajności. Natomiast zniechęcił mnie system budowania z którym za bardzo nie potrafiłem się dogadać.   Wydaje mi się, że mógł się nie przyjąć przez fakt, że istniały dwie iteracje tego języka – D1 i D2 (współcześnie znany jako D). W języku D1 istniały dwie biblioteki standardowe Phobos (opracowywany m.in. przez Alexandrescu, eksperta od C++’a, rozwijała się powoli) oraz Tango (szybciej rozwijana bo przez środowisko, ale gorzej przetestowana), które były niekompatybilne względem siebie. W D2 (znanym dalej jako D) rozwiązano problem wprowadzając standardowy mechanizm obsługi wątków i garbage collectora.
AdaNa razie za wiele o nim nie wiem. Zaprojektowany dla wojska a używany głównie w lotnictwie. Z tego co czytałem wydaje się wręcz stworzony do systemów wymagających bezpieczeństwa.
RustNowoczesny język, który nadaje się do programowania systemów wbudowanych. Ostatnio powoli wchodzi jako jeden z języków używanych w ramach jądra Linux’a oraz Androida. Przy okazji wydaje się, że będzie coraz częściej używany także w branży automotive patrząc, że są prowadzone prace by można go było wykorzystywać w sytuacjach safety tj. kiedy muszą zostać spełnione normy ISO-26262. Zaprojektowany przez Mozillę.
GolangInny stosunkowo młody język, zaprojektowany przez Google’a, który całkiem fajnie się sprawdza w zadaniach rozproszonych. Moje główne skojarzenia z nim odnoszą się głównie z aplikacjami od Docker’ów.
PythonW mojej opinii fajny język dopóki używa się go do prototypowania. Często używany do skryptowania, małych aplikacji webowych oraz przez naukowców do symulacji i obliczeń.
RubyKolejny język skryptowy, który kojarzę głównie za sprawą swego czasu modnego frameworku Ruby on Rails, i chyba to jest jego najczęstsze zastosowanie.
PHPMartin Lechowicz śpiewał kiedyś „Programuję w dotnecie już trzecie stulecie bo kto się w PHPie połapie”… Kolejny język używany w aplikacjach internetowych o którym zbyt wiele nie wiem.
JavaScriptJęzyk skryptowy wykonywany przez przeglądarki internetowe, najczęściej służy do oprogramowania działania front-endu, czyli tzw. widocznej części aplikacji internetowych.
PrologJęzyk deklaratywny, służący do programowania w logice. Program stanowi sekwencję przesłanek, które muszą zostać spełnione, aby rozwiązać problem. :- – oznacza implikacje, tj. quicksort([X|Xs], Ys) :- … oznacza, że element X skonkatenowany z Xs jest w relacji quicksort z Ys jeśli spełnione są warunki …

Ostatnio też zauważalna jest tendencja do tworzenia języków kompatybilnych z ekosystemami związanymi z istniejącymi językami programowania:

  • Java => Scala, Clojure, Kotlin
  • JavaScript => TypeScript,
  • C++ => Carbon.

Po co tworzyć takie języki? Historia każdego jest nieco inna… Język programowania jest formą dzięki której programiści mogą wyrażać swoje myśli. Dobrym przykładem tego jest Clojure, który posiada składnię LISPu, ale wciąż działa na maszynie wirtualnej Javy dzięki czemu oba mogą koegzystować w ramach jednej aplikacji. Z drugiej strony istniejące języki nie rzadko cierpią na chorobę wieku dojrzałego. Dodajemy w nich nowe funkcje, ale nie zmieniamy części jego wcześniejszych cech dla zachowania kompatybilności przez co stają się coraz bardziej skomplikowanymi (przypadek C++’a, w którym niedługo sam diabeł nie dojdzie co się dzieje 😉). Nowy język może zachować zgodność z bibliotekami napisanymi w starym języku jednakże oferując czystszą składnię – tego przykładem jest Carbon, którego celem w teorii jest stopniowe zastępowanie części starego kodu w C++’ie. Jednak wciąć twórcy tego języka zalecają, że jeśli projekt pisany jest od podstaw, zalecane jest użycie Rust’a (pomijając nawet fakt, że nie jest on jeszcze dość dojrzały, żeby używać go na produkcji).

Zakończenie

To już koniec, mam wrażenie, że macie już pewne wyobrażenie jak wygląda praca programisty. W kolejnych artykułach przejdę na reszcie do technikaliów 😊

Miłego wieczoru!

Paweł

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *