Szybko i elegancko - Samouczek: Język programowania Nim

Opublikowane:

01.04.2018

Nim to nowoczesny język programowania o ogromnych możliwościach i znakomitej wydajności.

Autor: Mariusz Bielecki

Rozpoczynając nowy projekt, często stoimy przed wyborem języka programowania. Czasem jest on oczywisty ‒ związany z zewnętrznymi ograniczeniami, takimi jak platforma docelowa, współpraca z pozostałymi komponentami danego systemu, wymagania klienta itd. Jeśli jednak mamy pełną swobodę decyzji, warto dać szansę nowym językom, które ostatnio zyskują coraz większą popularność. Wśród nich na szczególną wagę zasługuje Nim, zwłaszcza jeśli zależy nam na wydajności.

Składnia Nima inspirowana jest konstrukcjami znanymi z innych popularnych języków, takich jak Python, C#, Lisp czy Go, łatwo więc go sobie szybko przyswoić. Kod programów pisanych w Nimie pozbawiony jest jednak narzutu związanego z interpreterem czy maszyną wirtualną. Generowany kod jest też przenośny w ramach danej architektury. Jeśli szukamy języka, który z jednej strony byłby wygodny i nie odstraszał nietypową składnią, a z drugiej ‒ generował bardzo szybko działający kod, warto przyjrzeć się Nimowi.

Nim nie generuje kodu wynikowego bezpośrednio, lecz poprzez fazę pośrednią: najpierw tworzy plik C, który dopiero potem poddawany jest zwykłemu procesowi kompilacji i konsolidacji. Możemy przy tym wybrać dowolny kompilator, taki jak GCC, Clang czy ICC Intela. Co ciekawe, podobnie jak ClojureScript, Nim potrafi generować również JavaScript.

Gdyby chcieć zdefiniować jeden aspekt, który wyróżnia Nima na tle konkurencji, jest to zdecydowanie wydajność. Nim to praktycznie jedyny język „pythonowy” (tj. wysokiego poziomu, w którym kod tworzy się równie wygodnie jak w Pythonie i podobnych językach) o wydajności zbliżonej do C.  Nim zawiera oczywiście szereg konstrukcji nieobsługiwanych przez C, które są następnie tłumaczone na składnię C, więc kod Nima nigdy nie będzie wydajniejszy od C, nie pozostaje jednak za nim daleko w tyle. Oczywiście wszelkie testy porównujące wydajność różnych języków nie są miarodajne i mogą często wprowadzać w błąd, wynika z nich jednak jasno, że języki takie jak D i Nim w wielu popularnych zastosowaniach nie odbiegają zbytnio pod względem wydajności od C ‒ a z pewnością wypadają znacznie lepiej niż Python, Ruby, Lua, Perl, C# czy Tcl [1]. Jeśli jesteśmy zainteresowani optymalizacją Nima pod kątem wydajności C, powinniśmy zapoznać się ze szczegółowymi informacjami na ten temat [2].

Instalacja

Nima pobieramy z witryny projektu [3]. Instalacja sprowadza się do wydania polecenia:

curl https://nim-lang.org/choosenim/init.sh -sSf | sh

Następnie dodajemy do zmiennej $PATH ścieżkę dostępu do menedżera pakietów Nima, Nimble:

export PATH=/home/UŻYKOWNIK/.nimble/bin:$PATH

Powyższy wiersz najwygodniej jest dodać do pliku ~/.profile lub ~/.bashrc. Zwróćmy uwagę, że nim i narzędzia towarzyszące (nimble, choosenim, nimble, nimgrep, nimsuggest) nie są instalowane globalnie (w /usr/bin czy /usr/local/bin), lecz lokalnie w katalogu domowym użytkownika, w podkatalogu .nimble/bin. Odtąd możemy instalować kolejne wersje poleceniem choosenim.

Jeśli chcemy, możemy też zainstalować Nima za pomocą menedżera pakietów, np. w Debianie zrobimy to poleceniem:

apt install nim nim-doc

Jak to jednak zwykle bywa, wersje dystrybucyjne pozostają w tyle za oficjalnymi. Oczywiście w każdej chwili możemy też zbudować Nima ze źródeł. W tym celu wydajemy polecenia przedstawione na Listingu 1 (dla najnowszej w chwili pisania niniejszego artykułu wersji 18.0). Pamiętajmy, by dodać do zmiennej $PATH ścieżkę dostępu do ~/.nimble/bin oraz katalogu bin

Listing 1: Ręczna instalacja Nima ze źródeł

wget -c https://nim-lang.org/download/nim-0.18.0.tar.xz

tar xf nim-0.18.0.tar.xz

cd cd nim-0.18.0/

sh build.sh

bin/nim c koch

./koch tools

Witaj, świecie!

Najprostszy program w Nimie nie wymaga dołączania plików nagłówkowych, importu modułów itd. i ma następującą postać:

echo "Witaj, świecie!"

Plik o powyższej zawartości zapisujemy jako witaj.nim, a następnie kompilujemy poleceniem:

nim c witaj.nim

Po chwili w bieżącym katalogu ujrzymy plik wykonywalny witaj, po uruchomieniu którego ujrzymy znajomy napis. Z kolei w podkatalogu nimcache znajdziemy kilka plików obiektowych, plik JSON definiujący kompilację oraz pliki pośrednie C używane do wygenerowania plików wynikowych. Plik binarny jest stosunkowo duży ‒ ma aż 200 KB, ponieważ domyślnie zawiera informacje wspomagające odpluskwianie. Chcąc wygenerować plik finalny, używamy opcji -d:release:

nim c -d:release witaj.nim

Spróbujmy zmodyfikować nasz program tak, by korzystał ze zmiennej. Spróbujmy skompilować przykład z Listingu 2. Próba kompilacji zakończy się błędem: w Nimie deklaracja zmiennej musi zostać poprzedzona słowem kluczowym var (Listing 2). Z kolei zmienne deklarujemy za pomocą const (istnieje również słowo kluczowe let, które pozwala deklarować zmienne, których wartość po przypisaniu nie może ulec zmianie).

Listing 2: Brak słowa kluczowego var

a = "Witaj, świecie!"

echo a

Listing 3: „Witaj, świecie” ze zmienną

var a = "Witaj, świecie!"

echo a

Typy danych

Listing 4: Wyświetlenie elementów tablicy

type

  IntArray = array[0..5, int]

var

  x: IntArray

x = [1, 2, 3, 4, 5, 6]

for i in low(x)..high(x):

  echo x[i]

echo repr(x)

echo len(x)

Listing 5: Suma zbiorów

type

  CharSet = set[char]

var

  letters, digits, union: CharSet

letters = {'a'..'z'}

digits = {'0'..'9'}

union = letters + digits

for i in union:

  echo i

Oprócz mniej interesujących typów danych, takie jak zmienne boolowskie (bool), znakowe (char), napisy (string), liczby całkowite (int, ale także int8, int16, int32, int64, uint, uint8, uint16, uint32 oraz uint64), zmiennoprzecinkowe (float, float32 oraz float64), dostępnych jest kilka bardziej interesujących typów. Tablice zachowują się podobnie jak w innych językach; przykład widzimy na Listingu 4: definiujemy tablicę o sześciu elementach typu int, a następnie używamy pętli for do iteracji. Mniej typowa jest konstrukcja low(x)..high(x): za pomocą wbudowanych funkcji low() i high(), zwracających wartość minimalną i maksymalną tworzymy zakres, a dopiero potem przeprowadzamy iterację na tym zakresie. Wyrażenie echo repr(x) wyświetla wszystkie elementy tablicy, zaś echo len(x) ‒ jej długość.

Z kolei na Listingu 5 widzimy, jak korzystać ze zbiorów. Najpierw definiujemy typ zbioru składającego się ze znaków, a następnie trzy zmienne tego typu. Dalej przypisujemy dwom pierwszym zmiennym wartości ‒ robimy to za pomocą zakresów. Trzecia zmienna jest sumą zbiorów dwóch poprzednich utworzoną za pomocą operatora +. Oprócz sumy zbiorów możemy też m.in. utworzyć ich różnicę (-), część wspólną (*) lub sprawdzić, czy dany element należy do określonego zbioru lub nie (in, notin).

Instrukcje sterujące

Oprócz wspomnianej wyżej pętli for w Nimie możemy też korzystać z innych instrukcji sterujących znanych z innych języków. Poniżej widzimy najprostszą implementację uniksowego polecenia yes w Nimie, która bazuje na instrukcji while przetwarzającej kolejne wyrażenia, o ile spełniony jest warunek (w tym wypadku jest nim true):

while true:

  echo "y"

Listing 6: Instrukcja if

from strutils import parseFloat

echo "Podaj kwotę w złotych: "

let n = parseFloat(readLine(stdin))

if n < 0:

  echo "Nie warto zaciągać kredytów"

elif n == 0:

  echo "Niczego nie masz"

else:

  echo "Masz ", n/3.4, " dolarów"

Przykład użycia instrukcji if, else oraz elif (skrót od „else if”) widzimy na Listingu 6. Najpierw importujemy funkcję pomocniczą parseFloat z modułu strutils. Jest to niezbędne, ponieważ funkcja odczytująca dane ze standardowego wejścia (readLine(stdin)) oczekuje napisu, musimy więc przeprowadzić konwersję do liczby zmiennoprzecinkowej. Jeśli warunków jest więcej, poręcznie jest skorzystać z instrukcji case.

Funkcje

Funkcje definiujemy za pomocą słowa kluczowego proc. Jeśli pamiętamy Pascala, wiemy, że w tym języku istnieje rozróżnienie między funkcjami a procedurami: te pierwsze zwracają wynik, zaś te drugie jedynie wykonują określone zadanie, niczego nie zwracają. Jednak podobieństwo między procedurą a proc jest pozorne: proc w Nimie to po prostu funkcja, która zachowuje się tak samo jak funkcje w innych językach.

Listing 7: Przykład z Listingu 6 z funkcją

from strutils import parseFloat

proc convert(amount: float): float =

  return amount/3.4

echo "Podaj kwotę w złotych: "

let n = parseFloat(readLine(stdin))

if n < 0:

  echo "Nie warto zaciągać kredytów"

elif n == 0:

  echo "Niczego nie masz"

else:

  echo "Masz ", convert(n), " dolarów"

Przyjrzyjmy się Listingowi 7. Jest to program identyczny jak ten z Listingu 6, z jedną zmianą: zdefiniowaliśmy funkcję pomocniczą convert. Przyjmuje ona jeden argument typu float i zwraca rezultat takiego samego typu. Zwróćmy uwagę, że w Nimie każda funkcja musi zostać zadeklarowana przed użyciem.

W praktyce

Listing 8: Badanie dostępności usług

import httpclient, os

var

  x: seq[string]

x = @["http://google.com", "http://google.pl", "http://google.fr", "http://google.deix"]

let client = newHttpClient()

while true:

  for website in @x:

    try:

      let response = client.request(website)

                  echo "Witryna: ", website, ", status: ", response.status

    except:

      echo "Błąd!"

    sleep(1000)

Listing 8: Wynik działania programu z Listingu 8

Witryna: http://google.com, status: 200 OK

Witryna: http://google.pl, status: 200 OK

Witryna: http://google.fr, status: 200 OK

Błąd!

Witryna: http://google.com, status: 200 OK

Witryna: http://google.pl, status: 200 OK

Witryna: http://google.fr, status: 200 OK

Błąd!

Spróbujmy napisać nieco bardziej przydatny program. Będzie on monitorował zdefiniowaną listę witryn i sygnalizował błąd, jeśli któraś z nich będzie niedostępna. Na potrzeby przykładu uznajmy, że niedostępnością będzie zwrócenie kodu statusu HTTP innego niż 200.

Najpierw importujemy dwa moduły: httpclient zawiera implementację klienta HTTP, zaś os zawiera funkcję delay, która opóźnia działanie programu o podaną liczbę milisekund. Następnie definiujemy zmienną x, która będzie sekwencją napisów. Sekwencje tym różnią się od tablic, że ich rozmiar nie jest z góry ustalony (co w naszym przykładzie nie jest niezbędne). Następnie przypisujemy naszej sekwencji listę wartości ‒ czyli witryn, które mają być monitorowane (w istocie odwołania do google.pl, google.de itd. powodują nawiązanie połączenia z tym samym serwerem). Zwróćmy uwagę, że w ostatniej nazwie jest błąd, który posłuży nam do przetestowania, czy program zachowa się poprawnie, jeśli nie znajdzie witryny.

Następnie konstruujemy nowego klienta HTTP i tworzymy nieskończoną pętlę while true:. Wewnątrz niej przeprowadzamy iterację po wszystkich witrynach z sekwencji, próbując nawiązać z nimi połączenie. Próba połączenia znajduje się w bloku try...except, co jest niezbędne: gdybyśmy go pominęli, program zakończyłby działanie przy wystąpieniu błędu. Na koniec używamy funkcji opóźniającej (1 sekunda), by nie atakować serwerów zbyt częstymi żądaniami. Wynik działania programu widzimy na Listingu 8.

Wnioski

Ramy niniejszego artykułu nie pozwalają nam opisać wszystkich aspektów tego niewątpliwie interesującego języka, jakim jest Nim. Mamy jednak nadzieję, że to wystarczy, by zainspirować Czytelnika do bliższego przyjrzenia się Nimowi. Składni można nauczyć się stosunkowo szybko, znajdziemy w niej bowiem wiele elementów znanych z innych języków. Kolejnym plusem jest znakomita wydajność: programy pisane w Nimie działają niemal tak szybko jak ich odpowiedniki napisane w C.

Jakie są więc wady Nima? Dla niektórych przeszkodą jest fakt, że nie stoi za nim żadna korporacja. Nie przeszkodziło to jednak twórcom tego języka pracować nad nim przez kilkanaście lat i stworzyć narzędzie używane w wielu miejscach na całym świecie, mimo że wersja 1.0 nie została jeszcze wydana. Dla wielu problemem jest też fakt osobliwego traktowania przez Nima nazw zmiennych: pominąwszy pierwszą literę, Nim ignoruje wielkość liter i znak podkreślenia. Trzecia problematyczna kwestia dotyczy importu: użyte przez nas wcześniej polecenie import os powoduje, że w bieżącej przestrzeni nazw oprócz delay pojawia się wiele innych obiektów. W praktyce jednak nie jest to problemem, ponieważ kompilator poinformuje nas o próbie utworzenia zmiennej o takiej samej nazwie jak obiekt zaimportowany z modułu; możemy też importować poszczególne funkcje oddzielnie, tak jak się to robi zwykle np. w Pythonie czy Javie, zamiast zaśmiecać bieżąca przestrzeń nazw wieloma elementami. 

Info

[1] Testy wydajności Nima: https://github.com/def-/nim-benchmarksgame, https://github.com/kostya/benchmarks
[2] Optymalizacja wydajności Nima: http://blog.johnnovak.net/2017/04/22/nim-performance-tuning-for-the-uninitiated/
[3] Instalacja Nima: https://nim-lang.org/install_unix.html

Więcej podobnych

Aktualnie przeglądasz

Kwiecień 2018 - Nr 170
April-2018a

Top 5 czytanych

Znajdź nas na Facebook'u

Opinie naszych czytelników

Nasze wyróżnienia i partnerzy:partnerzy linux magazine
wiper-pixel