Dostępność

Czym jest Duckling i dlaczego powstaje?

Po co tworzyć nowy język programowania?

Język programowania jest podstawowym narzędziem wykorzystywanym przy tworzeniu oprogramowania. Posiada on olbrzymi wpływ na produktywność programisty, jak i na cechy tworzonych w nim aplikacji, takie jak ich bezpieczeństwo czy wydajność. Wraz z rozwojem branży IT, pojawiają się również nowe pomysły i możliwości na tworzenie języków programowania, które pozwalają usprawnić pracę. Znanym przykładem takiego zjawiska jest tak zwany „borrow checker”, wprowadzony w języku Rust. Dodatkowo, dynamiczny rozwój narzędzi programistycznych takich jak IDE, czy modele sztucznej inteligencji, silnie wpływają na to czego oczekujemy od języków programowania oraz ich kompilatorów i powiązanych narzędzi.

Powoduje to, że na przestrzeni lat tworzone i adoptowane są nowe języki programowania, które z czasem wypierają swoich poprzedników.

Korzenie Duckling: C++ i inne języki kompilowane

Oryginalną inspiracją dla języka Duckling była koncepcja stworzenia „nowoczesnego C++” i bazowała silnie na istniejących językach kompilowanych. Jest to koncepcja, której wpływ wciąż przejawia się wśród podstawowych założeń i cech tworzonego przez nas języka. Bardzo wcześnie pojawiła się również idea połączenia możliwości kompilacji do kodu natywnego, z uruchomieniem na maszynie wirtualnej pozwalającej wnikliwie i precyzyjnie analizować program.

Aktualne założenia języka znacząco się zmieniły w pierwszych etapach rozwoju. Prowadzone badania, zgromadzona wiedza i analiza nowatorskich, niestandardowych pomysłów skłoniła nasz zespół do poświęcenia większej uwagi takim aspektom jak możliwość pisania skryptów i szybkiego prototypowania.

Poniżej przedstawiamy zbiór kilku cech języka Duckling, będących wynikiem prac wstępnych oraz badawczych. Są to cechy, które naszym zdaniem najsilniej go wyróżniają. Na ten moment nie prezentujemy jeszcze faktycznych przykładów kodu. Takie przykłady pojawią się wraz z pierwszą dokumentacja języka, którą planujemy udostępnić w 2025 roku. Wraz z nią pojawi się również obszerniejsza lista cech tego rodzaju i ich wpływu na wydajność programowania.

Kluczowe idee i koncepcje Duckling

Wysoka wydajność

Wydajność czasowa i pamięciowa równa lub zbliżona do języków takich jak C++ i Rust

Wydajność produkowanych w języku programowania aplikacji jest jedną jego z kluczowych cech. Niska wydajność bezpośrednio przekłada się na ograniczenia zastosowań języka. Dzięki kompilacji do kodu natywnego, wydajność Duckling będzie w ścisłej czołówce dostępnych na rynku języków programowania. Wiążę się to z szeregiem zalet takich jak szeroka gama zastosowań, szybsze wykonanie, niższe zużycie energii, niższe czasy odpowiedzi aplikacji. Ponadto pozwoli to tworzyć ekosystem języka, który nie zależy od innych języków programowania przy implementacji funkcjonalności wymagających wysokiej wydajności.

Unikalny debugger

Debugger oparty o dedykowaną maszynę wirtualną

Jednym z kluczowych elementów pracy programisty jest naprawa błędów w tworzonym oprogramowaniu. Mimo że nowoczesny język programowania może wykrywać lub zapobiegać powstawaniu wielu błędów jeszcze przed uruchomieniem aplikacji, to ich pełna eliminacja nie jest możliwa.

Jednym z ważnych celów projektowych jest więc stworzenie wydajnej maszyny wirtualnej, której architektura będzie dedykowana do jak najpełniejszego i sprawnego wykrywania błędów w działaniu aplikacji. Duckling, poza kompilacją do kodu natywnego, będzie również posiadał możliwość kompilacji do bajtkodu tejże maszyny wirtualnej, która będzie pełnić rolę debuggera. Poza możliwością zaprojektowania specjalnej architektury, dedykowana maszyna wirtualna może pozwolić na bardzo precyzyjną kontrolę stanu i działania programu. Otwiera to bardzo szerokie możliwości przy debugowaniu i testowaniu aplikacji we wszystkich trzech najważniejszych kategoriach błędów:

  • błędów związanych z wielowątkowością
  • błędów związanych z pamięcią
  • błędów związanych z logiką programu

Uniwersalny język

Możliwość wydajnego pisania zarówno krótkich skryptów jak i wielkoskalowych aplikacji

Choć te dwie cechy mogą wydawać się sprzeczne, to naszym zdaniem jest wręcz przeciwnie. Chcemy połączyć te dwa światy programistyczne w jednym języku programowania, który zapewni wygodę i ekspresywność każdego z nich.

Dzięki udostępnieniu świata skryptowego w świecie wielkoskalowych aplikacji możemy pozwolić na bardzo szybkie prototypownie, sprawne testowanie i realną interakcję z kodem, przy pracy z zarówno małymi jak i wielkimi aplikacjami.

Dzięki udostępnieniu świata wielkoskalowych aplikacji w świecie skryptowym możemy pozwolić na wykorzystywanie dużych modułów i aplikacji w skryptach bez jakiejkolwiek komplikacji. Wszystko zamknięte jest w jednym spójnym ekosystemie i języku.

Oba te światy wymagają swojego zestawu funkcjonalności, które łączymy w jednym języku przy jedynie minimalnej ingerencji w to jak zachowuje się kompilator przy pracy ze skryptem, w porównaniu do pracy z kodem "standardowego" modułu.

Strukturalna wielowątkowość

Struktura wątków wprost związana ze strukturą kodu.

Jednym z coraz ważniejszych tematów w obecnym świecie programistycznym są aplikacje wielowątkowe, które są zdolne do wykorzystywania wielu rdzeni procesora. Jednocześnie pisanie i projektowanie takich aplikacji jest jednym z trudniejszych zadań programisty.

Aby odpowiedzieć na ten problem w Duckling wprowadzamy tak zwaną „wielowątkowość strukturalną”. Naszym celem jest, aby programista analizujący program widział strukturę wątków w taki sam sposób, w jaki widzi w kodzie strukturę modułów czy funkcji. Dodatkowo wprowadzamy jasne rozróżnienie na fragmenty kodu, która są i nie są bezpieczne wątkowo, jak również na fragmenty kodu reprezentujące stan współdzielony przez wiele wątków.

Takie podejście znacznie ułatwia projektowanie i analizowanie programów przez programistę. Pozwala to również na wprowadzanie do kompilatora analizy statycznej programu, która może wykryć niektóre błędy jeszcze przed uruchomieniem aplikacji.

Nowoczesny system typów

Nowoczesne połączenie interfejsów, typów algebraicznych i generyków

Jednym z ważnych założeń języka Duckling jest statyczne typowanie. Pozwala ono na wykrywanie wielu błędów w czasie kompilacji, znacznie lepsze wsparcie edytorów kodu jak również na kompilację do wydajnego kodu maszynowego. Naszym celem jest aby system typów Duckling możliwie pomagał programiście, a jak najmniej mu przeszkadzał. Wiąże się to ze zdefiniowaniem od podstaw założeń tego systemu, czerpiąc przy tym inspiracje z wielu istniejących obecnie języków oraz wprowadzeniem nowych, autorskich pomysłów. Bardzo ważnym konceptem jest częściowe ukrycie systemu typów w skryptach napisach w Duckling. Takie skrypty wciąż będą statycznie typowane, ale programista będzie miał możliwość nie pisania explicite wszystkich definicji.

Naszym zdaniem, wśród najważniejszych konceptów tworzonego systemu typów są:

  • interfejsy, pozwalające na precyzyjne określenie własności spełnianych przez dany typ,
  • typy algebraiczne, które w wygodny i czytelny sposób wyrażają alternatywę lub krotkę kilku typów,
  • generyki, których poprawność jest sprawdzana przed ich użyciem.

Duckling wprowadza również rozwinięty system referencji, który pozwala precyzyjnie wyrazić intencję programisty i zapobiega większości niepoprawnych operacji jeszcze w czasie kompilacji. Dbamy przy tym o to, aby standardowe programy wymagały jedynie minimalnych oznaczeń tego jakie własności posiada dana referencja.

Praktyczne podejście do języka

Język tworzony z praktycznym podejściem na pierwszym planie

Choć "praktyczne podejście" jest szerokim i nieprecyzyjnym pojęciem, to dla nas wyraża ważną myśl, która jest szczególnie istota w języku z obszernym zakresem funkcjonalności jakim jest Duckling. Przez takie podejście rozumiemy analizę tworzonego języka z ogólniejszego punktu widzenia niż takiego, które służy rozwiązywaniu pojedynczych problemów. Zależy nam aby tworzony język był spójny, intuicyjny w użyciu i prosty w nauce. Nawet jeśli język w całości będzie względnie skomplikowany, do jego pojedyncze „cegiełki” powinny być możliwie przystępne. Ograniczamy funkcjonalności, które wprowadzają do języka komplikacje, posiadając przy tym jedynie niszowe zastosowania. Skupiamy się również na cechach, które często nie dotyczą pojedynczych funkcjonalności takich jak ograniczanie tak zwanego boilerplate'u.

Nowoczesny kompilator

Nowoczesny kompilator, którego rozwój nie skupia się jedynie na kompilacji programu, ale również na kompilacji inkrementalnej, bogatym wsparciem dla IDE i wysokiej jakości błędów kompilacji

W ciągu ostatnich dekad świat programistyczny przeszedł szeroką ewolucję. Tworzone projekty są coraz większe, a wykorzystywane przez programistów narzędzia coraz bardziej zaawansowane. Dlatego, przy projektowaniu kompilatora nie skupiamy się jedynie na samej podstawowej kompilacji programu, ale również na szeregu innych, ważnych cech.

Po pierwsze zależy nam na wysokiej jakości wsparcia przez edytory kodu. Wymaga ono kompilatora mogącego pełnić rolę serwera języka, o bardzo niskich czasach odpowiedzi i szerokim wsparciu dla funkcjonalności IDE.

Po drugie od początku tworzymy kompilator z myślą o wspieraniu zaawansowanej kompilacji inkrementalnej. Taka kompilacja może drastycznie zmniejszyć czas rekompilacji programów, który zaczyna mieć znaczenie od nawet kilku tysięcy linii kodu.

Zależy nam również o uzyskanie możliwie dobrej jakości błędów kompilacji. Taka jakość wiążę się nie tylko z samą treścią i formatem błędów, ale również ze wsparciem konkretnych funkcjonalności takich jak przykładowo kontroli poziomu szczegółowości błędów.

Metaprogramowanie o dużych możliwościach

Rozwinięty system metaprogramowanie oparty o makra proceduralne, szablony i moduły parametryzowane, który ogranicza powtarzalny kod do minimum

W wielu obecnie wykorzystywanych językach programowania pojawia się możliwość szeroko pojętego metaprogramowania. W Duckling wprowadzamy rozwinięty system metaprogramowania, który odbywa się w pełni w czasie kompilacji. Realizujemy go poprzez kilka mechanizmów, które pozwalają na modyfikację lub generację kodu na różnych poziomach szczegółowości.

W celu operowania na największych fragmentach kodu, wprowadzamy moduły parametryzowane. Pozwalają one tworzyć całe drzewa modułów, zawierające wiele plików źródłowych, które wszystkie zależą od danego zbioru parametrów. Takimi parametrami mogą być zarówno typy jak i wartości.

W celu precyzyjnej modyfikacji lub generacji kodu na najniższym poziomie szczegółowości wprowadzamy makra proceduralne, wzmocnione o system refleksji. Pozwalają one na operacje takie jak na przykład automatyczna generacja kompletnych fragmentów kodu lub automatyczne dodawanie metod do klas.

Pomiędzy tymi dwoma poziomami wprowadzamy również szablony, które zachowują się się podobnie jak moduły parametryzowane, ale dotyczą fragmentów kodu takich jak funkcje, klasy czy przestrzenie nazw.

Zintegrowany system kompilacji i menedżer pakietów

Możliwość kompilacji nawet najwiekszych projektów przy użyciu minimalnej konfiguracji i pojedynczej komendy

Ważnym celem Duckling jest uproszczenie procesu kompilacji, tak aby przy standardowym użyciu, programista nie musiał pisac więcej niż kilka linii konfiguracji w przypadku pisania paczki, oraz zero linii konfiguracji w przypadku skryptów. Jest to możliwe dzięki wprowadzeniu precyzyjnego systemu pakietów i modułów, pozwalającego na jednoznaczną interpretację plików źródłowych przez kompilator. Zadania takie jak kompilacja, uruchomienie aplikacji czy przetestowanie paczki powinny wymagać napisania pojedynczej komendy.

Dodatkowo sprecyzowanie pojęcie paczki Duckling pozwala na wprowadzanie zintegrowanego menadżera pakietów, co pozwala tworzyć spójny ekosystem. Dzięki temu możliwe będzie publikowanie pakietów, oraz korzystanie z upublicznionych pakietów poprzez pisanie jedynie kilku linii konfiguracyjnych.