PHP 8 - co ciekawego?

Mimo, że od premiery najnowszej wersji PHP 8 za chwilę minie rok to ja dopiero teraz zdecydowałem się napisać co ciekawego ta wersja przynosi. Powód? Bardzo prosty, musiałem na własnej skórze sprawdzić, które ulepszenia są naprawdę przydatne w mojej codziennej pracy. Przyjrzyjmy się tej liście.

Na pierwszy ogień idą argumenty nazwane (named arguments), pozwalają one zmieniać kolejność przekazywania argumentów do funkcji/metody, a tym samym unikać konieczności podania wartości z zdefiniowaną domyślną wartością. Dla przykładu:

<?php

function rysuj(int $x, int $y = 25, string $figura = 'kwadrat') {
    printf ('Rysuje w punkcie x: %d, y: %d figure: %s', $x, $y, $figura);
}

rysuj(
 figura : 'trojkat', // tym samym unikamy podania parametru y - zostanie użyta wartość domyślna
  x: 12
);

Długo wyczekiwanym przez mnie usprawnieniem PHP są adnotacje (lub też inaczej atrybuty) wbudowane w język. Nie musimy już posiłkować się bibliotekami jak Doctrine\ORM. Pomimo, że wybór właściwej notacji był niezwykle burzliwy to osiągnięto kompromis definiując adnotacje przy użyciu Syntax #[…]. Należy też nadmienić, że twórcy w sposób staranny i przemyślany projektowali ten feature. Z tego też powodu będę chciał bardziej szczegółowo opisać adnotacje w osobnym wpisie.

Zdecydowanie najczęściej używaną przeze mnie nowością jaką zaoferowało PHP 8 to bezpieczny operator zerowy (nullsafe operator). Pozwala uniknąć całej ifozy sprawdzeń przy wypakowywaniu konkretnej wartości. Sprawdźmy to na przykładzie z trzema klasami, użytkownik posiada adres, adres posiada obiekt kraj.

<?php
class Country {
    private $code = null;
    
    public function getCode(): ?string
    {
        return $this->code;
    }
}

class Address {
    private ?Country $country;
    
    public function getCountry(): ?Country
    {
        return $this->country;
    }
}

class User {
    private ?Address $address = null;
    
    public function getAddress(): ?Address
    {
        return $this->address;
    }
}

$user = new User();

echo $user?->getAddress()?->getCountry()?->getCode();

W przypadku wystąpienia wartości null nie otrzymamy błędu, kod dalej będzie się wykonywać.

Jednakże nie wszystkie feature PHP 8 przypadły mi do gustu. Unie są dobrym przykładem tezy, że nie zawsze rozwój jest postępem. W tym przypadku mówimy o cofnięciu się w rozwoju języka do czasów z przed PHP 7 ponieważ unie to konstrukcja pozwalająca na definiowania kilku typów dla danej zmiennej (co praktycznie oznacza brak typu).

<?php

class samochod {
    private int|float|string $cena;
    
    public function ustawCena(int|float|string $cena)
    {
        $this->cena = $cena;
    }
    
    public function pobierzCena(): int|float|string
    {
        return $this->cena;
    }
}

$fiat = new samochod();
$fiat->ustawCena(99.99);

echo $fiat->pobierzCena();

Nie dostrzegam również korzyści z wyrażenia dopasowania (match expressions). Jak dla mnie to tylko nakładka na switch case z kilkoma wyjątkami.

<?php
$name = match(2) {
    1 => 'One',
    2 => 'Two',
};

echo $name; // "Two"

choć może to jednak kwestia przyzwyczajenia się do tego całego PHP 8?

CUDA, czyli wpis o programowaniu z użyciem kart graficznych

Z racji tego, że ostatnio na uczelni zderzyłem się z tematem współbieżnych programów opartych o procesor karty graficznej to postanowiłem podzielić się z Wami tymi rewalacjami. Postaram się tutaj po krótce opisać podstawowe elementy i koncepcje, którymi rządzi się takie podejście do programowania. Od razu zawęże, że mowa tu będzie tylko o programowaniu kart graficznych opartych o architekturze CUDA (Compute Unified Device Architecture).

Zacznijmy od scharakteryzowania głównej różnicy. Mamy tutaj dwa procesory. Procesor CPU zwany w terminologii hostem oraz procesor karty graficznej (GPU) zwany urządzeniem. Ogólna zasada programowania sprowadza się do:

  • uruchomienia programu na procesorze CPU,
  • skopiowaniu potrzebnych danych do pamięci karty graficznej (tak GPU posiada własną pamięć L2, DRAM),
  • wykonaniu kodu współbieżnego (z podziałem na bloki/wątki) przez procesor karty graficznej,
  • skopiowaniu wyników z urządzenia (GPU) do pamięci operacyjnej RAM i dalsze kroki np. prezentacja wyników.

Podział kodu na część, która ma być wykonywana przez GPU określamy za pomocą słowa kluczowego __global__ przy deklaracji funkcji. Taka funkcja zostanie wywołana przez procesor CPU, i wykonana przez procesor karty graficznej. Podczas wywołania w nawiasach ostrych <<< >>> określamy na ilu blokach i ilu wątkach ma się uruchomić nasz kod.

__global__ void mykernel(void) {

}

int main(void) {
   mykernel<<<1,1>>>();
   printf("Hello World!\n");
   return 0;
}

Pomówmy teraz o alokacji pamięci, czyli o wymianie danych między procesorami. W przypadku zarządzania pamięcią GPU dostajemy funkcje cudaMalloc(), cudaFree(), cudaMemcpy() będące odpowiednikiem z języka C do zarządzania pamięcią operacyjną - malloc(), free(), memcpy(). Przy czym należy pamiętać, że wskaźniki operujące na pamięci urządzenia mogą być przetworzone do i z kodu na hoście. Nie można natomiast wykonywać dereferencji (odwołania się do wartości danej zmiennej) przez kod na hoście. Analogicznie dla wskaźników operujących na pamięci hosta.

int main(void) {
int a, b, c; // host copies of a, b, c
int *d_a, *d_b, *d_c; // device copies of a, b, c
int size = sizeof(int);

// Allocate space for device copies of a, b, c
cudaMalloc((void **)&d_a, size);
cudaMalloc((void **)&d_b, size);
cudaMalloc((void **)&d_c, size);
// Setup input values
a = 2;
b = 7;


// Copy inputs to device
cudaMemcpy(d_a, &a, size, cudaMemcpyHostToDevice);
cudaMemcpy(d_b, &b, size, cudaMemcpyHostToDevice);
// Launch add() kernel on GPU
add<<<1,1>>>(d_a, d_b, d_c);
// Copy result back to host
cudaMemcpy(&c, d_c, size, cudaMemcpyDeviceToHost);
// Cleanup
cudaFree(d_a); cudaFree(d_b); cudaFree(d_c);
return 0;
}

Do powyższego kodu można dopisać funkcje dodawania, która wykona się równolegle w ramach bloków. Koncepcja bloków, choć podobna do wątków ma zasadniczą różnice - bloki są od siebie odseparowane tj. nie mogą wpływać wzajemnie na swoją pamięć. W architekturze CUDA, bloki stanowią dodatkową abstrakcje tj. grupują wątki, które z kolei posiadają wspólną pamięć. Zmienna blockIdx.x dostarcza identyfikator konkretnego wątku. Stąd przykładowa definicja metody sumującej macierz do poprzedniego kodu mogła by wyglądać w następujący sposób.

__global__ void add(int *a, int *b, int *c) {
c[blockIdx.x] = a[blockIdx.x] + b[blockIdx.x];
}

Dla osób, które złapały bakcyla do programowania kart graficznych odsyłam po więcej informacji do strony producenta https://www.nvidia.com/docs/IO/116711/sc11-cuda-c-basics.pdf

Dokeryzowany NuxtJs

Czas wreszcie nadszedł by wszystkie usługi w mojej apce spiąć w jednym miejscu zarządzanym przez docker-compose. Skąd taka potrzeba? Powody są trzy. Mam już dość ręcznego uruchamiania bazy danych, serwera backendowego, frontendu, dodatkowych providerów etc. Wymaga to ode mnie wiedzy gdzie co się znajduje, jak to się odpala, jak trzeba konfigurować. Po za tym coś czuje, że wkrótce będę chciał to wszystko skalować przy użyciu Kubernetes^^. Nie przedłużając, dzisiejszym wpisem chciałbym się z Wami podzielić jak łatwo można puścić w kontener aplikacje napisaną w Vue z użyciem NuxtJs.

Po wielu godzinach spędzonych w internecie widzę, że królują dwa podejścia do tego problemu. Albo użyjemy Dockerfile, sami budując obraz z wykorzystaniem obrazu node albo użyjemy gotowca. Rozwiązanie ze stawianiem systemu i instalowaniem w nim node wydawało mi się zbyt karkołomne, więc w ogóle go nawet nie rozpatrywałem. Prostota zwyciężyła, postawiłem na gotowca.

# Plik docker-compose.yml
version: "3.3"
services:
  frontend:
    image: node:11.13.0-alpine
    command: npm run docker
    volumes:
      - ./web:/usr/src/app
    working_dir: /usr/src/app
    ports:
      - "3000:3000"
    environment:
      NUXT_HOST: 0.0.0.0
      NUXT_PORT: 3000

W powyżym pliku docker-compose.yml tworze serwis frontend z gotowego obrazu node11. Wykonuje mapowanie między moim katalogiem web gdzie trzymam zawartość aplikacji frontendowej a katalogiem osiągalnym dla Node. Do kontenera dostarczam zmienne środowiskowe NUXT_HOST, NUXT_PORT, które rozpozna NuxtJs nasłuchując tym samym na porcie 3000, na wszystkich interfejsach. By całość zadziałała poprawnie musimy jeszcze utworzyć komendę 'npm run docker', dodając ją do pliku package.json mniej więcej tak:

 "scripts": {
    "docker": "npm run dev",
  (...)
}

Teraz pozostaje już w głównym katalogu wywołać docker-compose up. Pamiętajmy tylko, że jest to konfiguracja dla wersji deweloperskiej a nie produkcyjnej!

Ściągawka z tablic w JavaScript

Przez to ciągłe przełanczanie się między językami programowania, często zdarza mi się zapomnieć składnie lub konstrukcje niektórych elementów. Dlatego dzisiaj prezentuje taką moją ściągawkę jeśli chodzi o podstawowe operacje na tablicach w JavaScript. Mam nadzieje, że to bedzie dla Was pomocne.

Asocjacje

Dla programistów PHP pewnym zaskoczeniem może być to, że JavaScript nie obsługuje tablic asocjacyjnych. Wszystkie indeksy muszą być typu liczbowego. Można to obejść stosując obiekty.

var myObject = {'book' : 'Learning JavaScript Design Patterns', 'author' : Addy Osmani };

Tworzenie tablicy

Array.of(element0[, element1[, …[, elementN]]]) - stworzenie tablicy z podanymi elementami.

Array.from(arrayLike[, mapFn[, thisArg]]) -  metoda tworzy nową instację tablicy z obiektu podobnego do tablicy lub obiektu iterowalnego.

var new_array = old_array.concat(wartość1[, wartość2[, …[, wartośćN]]]) - zwraca nową tablicę złożoną z tablicy, na której wywołano tę metodę, połączonej z innymi podanymi tablicami lub wartościami.

Czy to jest tablica ?

W przypadku tablic lepiej unikać porównania przez operator typeof - dla tablicy zwróci wartość object. Zamiast tego można skorzystać z Array.isArray(obj) lub instanceof. W przypadku tablicy właściwość length zwróci nam ilość jej elementów.

Operacje LIFO / FIFO

arr.push(element1, …, elementN) - dodaje jeden lub więcej elementów na koniec tablicy i zwraca jej nową długość. Metoda ta zmienia długość tablicy.

arr.pop() - usuwa ostatni element z tablicy zwracając go. Metoda ta zmienia długość tablicy.

arr.shift() - usuwa pierwszy element z tablicy i zwraca go. Metoda ta zmienia długość tablicy.

arr.unshift([element1[, …[, elementN]]]) - dodaje jeden lub więcej elementów na początek tablicy i zwraca jej nową długość.

Manipulowanie indeksami

arr.indexOf(searchElement[, fromIndex = 0]) - zwraca pierwszy (najmniejszy) indeks elementu w tablicy równego podanej wartości lub -1, gdy nie znaleziono takiego elementu.

delete (tablica[1]) - usunięcie elementu o zadanym indeksie przez ustawienie wartości undefined.

array.splice(start, deleteCount[, item1[, item2[, …]]]) - lepszy sposób na usuwanie rekordów. Zmienia zawartość tablicy, dodając nowe elementy podczas usuwania starych elementów.

Iterowanie

arr.forEach(callback[, thisArg]) - wykonuje dostarczoną funkcję jeden raz na każdy element tablicy.

var new_array = arr.map(function callback(currentValue, index, array){ // Zwróć element nowej tablicy }[, thisArg]) - metoda map() tworzy nową tablicę zawierającą wyniki wywoływania podanej funkcji dla każdego elementu wywołującej tablicy.

(Uwaga nie wszystkie przeglądarki) var newArray = arr.filter(callback(element[, index[, array]])[, thisArg]) - metoda filter() tworzy nową tablicę z wszystkimi elementami, które przechodzą test określony w postaci funkcji.

(Uwaga nie wszystkie przeglądarki) arr.find(callback[, thisArg]) - metoda find() zwraca pierwszy element tablicy, który spełnia warunek podanej funkcji testującej - wykonuje break.

Pozostałe przydatne metody

str = arr.join([separator = ',']) - łączy wszystkie elementy tablicy w jeden łańcuch znaków.

arr.sort([compareFunction]) - sortuje elementy tablicy.

Źródło
https://developer.mozilla.org/pl/docs/Web/JavaScript/Reference/Global_Objects/Array
https://www.w3schools.com/js/js_array_iteration.asp

Cechy (ang. Trait) w PHP

Dzisiaj słów kilka na tema cech (ang. trait) czyli sposobu w PHP na wielodziedziczenie. Dzięki nim możemy użycie tych samych metod w wielu klasach. Zobaczmy poniższy przykład.

<?php
trait Cukier { 
   private $zawartosc = 0;

   public function dosyp(int $ilosc) 
   {
       $this->zawartosc += $ilosc;
   }
}

trait Mleko {
 private $zawartosc = 0;

 public function dodaj(int $ilosc) 
   {
       $this->zawartosc += $ilosc;
   }
}

class Herbata {
   use Cukier;
   
   public function pobierzZawartosc(): int
   {
       return $this->zawartosc;
   }
}

class Kawa {
  use Cukier, Mleko;
}

$herbata = new Herbata();
$herbata->dosyp(2);
echo $herbata->pobierzZawartosc();

Definiujemy tu cechę Cukier, która będzie posiadać prywatną zmienną $zawartosc i metodę dosyp(). Cecha ta zostanie użyta w klasach Kawa i Herbata, czyli obie klasy uzyskają dostęp do metody i jej składowej. Co ciekawe dopuszcza się (gdy nie jest włączony tryb strict) powielenie składowej zarówno w innych cechach jak i klasach z nich korzystających (tak jak w tym przykładzie). Jak również możemy użyć niezdefiniowanej składowej pod warunkiem, że finalnie zostanie ona dostarczona przez inną cechę albo klasę. Oczywiście nic teraz nie stoi na przeszkodzie, aby cecha Cukier była adaptowana również do innych napojów, jak i by napój mógł korzystać z wielu różnych składników (Cukier, Mleko etc.) tak jak w przypadku Kawy.

Co jednak w przypadku konfliktu nazw? Załóżmy, że zarówna klasa jak i cecha, z której korzysta posiadają tą samą nazwę metody. W takim przypadku zostanie użyta metoda z klasy. Natomiast w sytuacji, w której konflikt nazw dotyczy cech (np. Cukier jak i Mleko posiadają wspólnie metodę dodaj()) uzyskamy fatal error. Możemy taką sytuacje naprawić przez określenie, która z metod ma zastosowanie w takim przypadku, tak jak poniżej.

class Kawa {
  use Cukier, Mleko {
     Cukier::dodaj insteadof Mleko;
     Mleko::dodaj as dolej;
   }
   
   public function pobierzZawartosc(): int
   {
       return $this->zawartosc;
   }
}

W powyższym przykładzie konflikt zostaje rozwiązany przez użycie metody z cechy Cukier, z kolei dla użycia metody dodaj() z cechy Mleka należy użyć wywołania spod aliasu dolej().

Nie mamy też przeszkody by zmieniać zakres widoczności metod w klasach używających danej cechy. Dla przykładu, mleka do kawy sobie nie dodamy :-(

class Kawa {
 use Mleko { dodaj as protected; }

   
   public function pobierzZawartosc(): int
   {
       return $this->zawartosc;
   }
}

$kawa = new Kawa();
$kawa->dodaj(2); //wygeneruje Uncaught Error: Call to protected method
echo $kawa->pobierzZawartosc();

Cechy mogą być również użyte do zgrupowania innych (wielu różnych) cech.

trait Dodatki {
  use Cukier, Mleko;
}

class Kawa {
 use Dodatki;
 public function pobierzZawartosc(): int
   {
       return $this->zawartosc;
   }
}

$kawa = new Kawa();
$kawa->dodaj(2);
echo $kawa->pobierzZawartosc();

Jeszcze inna ciekawa właściwość to stosowanie abstrakcji. Możemy zadeklarować daną metodę jako abstrakcyjną, gdzie klasa używająca cechy dostarczy jej definicje (analogicznie jak dla klas abstrakcyjnych). Zatem poprawiając dzisiejszy przykład, bardziej poprawnie byłoby zapisać go w następujący sposób.

<?php
trait Cukier { 
   protected $zawartosc = 0;

   public function dodaj(int $ilosc) 
   {
       $this->zawartosc += $ilosc;
   }
   
   abstract public function pobierzZawartosc(): int;
}

class Kawa {
 use Cukier;
 public function pobierzZawartosc(): int
   {
       return $this->zawartosc;
   }
}

$kawa = new Kawa();
$kawa->dodaj(2);
echo $kawa->pobierzZawartosc();

Podsumowując myślę, że możliwości jakie dają nam cechy w języku PHP stanowią lepszą alternatywę do tradycyjnych dziedziczących po sobie klas. Oczywiście w przypadku gdy już na etapie projektowania wiemy, że ich metody mogą zostać wykorzystane w różnych częściach naszej aplikacji.