Einführung in Nim: Schreiben einer Konsole 2048


    Möchten Sie etwas Neues, Schnelles, Kompiliertes und gleichzeitig Angenehmes? Willkommen bei der Katze, wo wir die Programmiersprache Nim bei der Implementierung des nächsten Klons des Spiels 2048 testen werden: Keine Browser, nur Hardcore, nur Befehlszeile!

    Im Programm:


    Wer ist der Nim?


    Objektiv:


    Nim - statisch geschrieben, imperativ, kompiliert. Es kann als System-PL verwendet werden, da es den direkten Zugriff auf Speicheradressen und das Deaktivieren des Garbage Collectors ermöglicht. Der Rest ist da .

    Subjektiv


    Viele der jetzt erscheinenden Programmiersprachen bieten in der Regel eine (oder mehrere) Killer-Funktionen, mit denen versucht wird, eine Vielzahl von Problemen zu lösen (Go-Routinen, höllische Speicherverwaltung in Rust usw.). Nim bietet keine Besonderheiten an. Dies ist eine einfache Programmiersprache, deren Syntax an Python erinnert. Aber Nim macht das Schreiben von Programmen einfach. Fast so einfach wie in solch einem High-Level-Python. Gleichzeitig sollten die resultierenden Produktivitätsprogramme mit denen in C vergleichbar sein, da die Kompilierung nicht auf der Ebene einer virtuellen Maschine, sondern auf der Ebene von Maschinencodes erfolgt.

    Wie OOP in Nim aussieht


    Der Code ist in Modulen geschrieben (d. H. In Dateien im Python-Stil). Module können in andere Module importiert werden. Es gibt Funktionen ( proc ), es gibt keine Klassen. Es ist jedoch möglich, benutzerdefinierte Typen und Aufruffunktionen mithilfe der UFCS (Uniform Function Call Syntax) zu erstellen , wobei deren Überlastung berücksichtigt wird. Die folgenden zwei Codezeilen sind also äquivalent:

    foo(bar, baz)
    bar.foo(baz)
    

    Und mit dem folgenden Code können Sie OOP ohne Klassen im üblichen Sinne des Wortes arrangieren:

    type
        Game = object
            foo: int
            bar: string
        Car = object
            baz: int
    # * означает, что эта функция будет доступна за пределами этого модуля при импорте 
    # (инкапсуляция)
    proc start*(self: Game) = 
        echo "Starting game..."
    proc start*(self: Car) = 
        echo "Starting car..."
    var game: Game
    var car: Car
    game.start()
    car.start()
    

    Auch gibt es Verfahren ( Methode ). In der Tat ist der Unterschied , genau wie proc , nur im Moment der Bindung. Der proc- Aufruf ist statisch verknüpft, d.h. Typinformationen zur Laufzeit spielen keine Rolle mehr. Die Verwendung der Methode kann nützlich sein, wenn Sie eine Implementierung auswählen müssen, die auf dem genauen Typ des Objekts zum Zeitpunkt der Ausführung in der vorhandenen Hierarchie basiert. Und ja, Nim unterstützt die Erstellung neuer Typen auf der Grundlage vorhandener Typen, etwa der Einzelvererbung, obwohl die Komposition bevorzugt wird. Mehr Details hier und hier .

    Es besteht eine kleine Gefahr - eine solche Implementierung von OOP impliziert nicht eine physische Gruppierung aller Methoden für die Arbeit mit einem beliebigen Typ in einem Modul. Auf diese Weise können Sie rücksichtslos Methoden für die Arbeit mit einem Typ im gesamten Programm verteilen, was sich natürlich nachteilig auf die Code-Unterstützung auswirkt.

    Ein kleines C unter der Haube


    Obwohl Nim bis zum Limit kompiliert, geschieht dies durch eine Zwischenkompilierung in C. Und das ist cool, denn wenn Sie einen bestimmten Hintergrund haben, können Sie sehen, was im Nim-Code tatsächlich passiert. Schauen wir uns das folgende Beispiel an.

    Objekte in Nim können Werte (d. H. Gestapelt) und Referenzen (d. H. Gehäuft) sein. Es gibt zwei Arten von Links - ref und ptr . Verknüpfungen des ersten Typs werden vom Garbage Collector nachverfolgt, und mit einer Anzahl von Nullen (ref) werden Objekte aus dem Heap entfernt. Links des zweiten Typs sind unsicher und werden zur Unterstützung aller Arten von Systemkomponenten benötigt. In diesem Beispiel betrachten wir nur den Referenztyp ref .

    Eine typische Möglichkeit für Nim, neue Typen zu erstellen, ist etwa folgende:

    type
        Foo = ref FooObj
        FooObj = object
            bar: int
            baz: string
    

    Das heißt Der reguläre Typ FooObj und der Typ „Link zu FooObj“ werden erstellt. Nun wollen wir sehen, was beim Kompilieren des folgenden Codes passiert:

    type
        Foo = ref FooObj
        FooObj = object
            bar: int
            baz: string
    var foo = FooObj(bar: 1, baz: "str_val1")
    var fooRef = Foo(bar: 2, baz: "str_val2")
    

    Wir erarbeiten:

    nim c -d:release test.nim
    cat ./nimcache/test.c
    

    Das Ergebnis im Ordner nimcache (test.c):

    // ...
    typedef struct Fooobj89006 Fooobj89006;
    // ...
    struct  Fooobj89006  {  // выглядит как объявление типа FooObj. 
        NI bar;
        NimStringDesc* baz;
    };
    // ...
    STRING_LITERAL(TMP5, "str_val1", 8);
    STRING_LITERAL(TMP8, "str_val2", 8);
    Fooobj89006 foo_89012;
    //...
    N_CDECL(void, NimMainInner)(void) {
        testInit();
    }
    N_CDECL(void, NimMain)(void) {
        void (*volatile inner)();
        PreMain();
        inner = NimMainInner;
        initStackBottomWith((void *)&inner);
        (*inner)();
    }
    // Отсюда программа стартует на выполнение
    int main(int argc, char** args, char** env) {
        cmdLine = args;
        cmdCount = argc;
        gEnv = env;
        NimMain();  // это "главная" функция Nim, которая фактически делает вызов NimMainInner -> testInit
        return nim_program_result;
    }
    NIM_EXTERNC N_NOINLINE(void, testInit)(void) {
        Fooobj89006 LOC1;                                     // это будущая foo и она на стеке
        Fooobj89006* LOC2;                                    // это fooRef и она будет в куче
        NimStringDesc* LOC3;
        memset((void*)(&LOC1), 0, sizeof(LOC1));
        memset((void*)(&LOC1), 0, sizeof(LOC1));
        LOC1.bar = ((NI) 1);
        LOC1.baz = copyString(((NimStringDesc*) &TMP5));
        foo_89012.bar = LOC1.bar;                              // это foo
        asgnRefNoCycle((void**) (&foo_89012.baz), LOC1.baz);
        LOC2 = 0;
        LOC2 = (Fooobj89006*) newObj((&NTI89004), sizeof(Fooobj89006));  // выделение памяти в куче под fooRef
        (*LOC2).bar = ((NI) 2);
        LOC3 = 0;
        LOC3 = (*LOC2).baz; (*LOC2).baz = copyStringRC1(((NimStringDesc*) &TMP8));
        if (LOC3) nimGCunrefNoCycle(LOC3);
        asgnRefNoCycle((void**) (&fooref_89017), LOC2);
    }
    

    Die Schlussfolgerungen können wie folgt gezogen werden. Erstens ist der Code, falls gewünscht, leicht zu verstehen und zu verstehen, was unter der Haube passiert. Zweitens wurde für die beiden Typen FooObj und Foo in C nur eine entsprechende Struktur erstellt. Die Variablen foo und fooRef sind eine Instanz bzw. ein Zeiger auf eine Instanz der Struktur. Wie die Dokumentation sagt, ist foo eine Stapelvariable und fooRef ist auf dem Haufen.

    Instanzen erstellen


    Es gibt zwei Möglichkeiten, Instanzen in Nim zu erstellen. Wenn eine Variable auf dem Stapel erstellt wird, wird sie mit der Funktion initObjName erstellt . Wenn auf dem Heap eine Variable erstellt wird, newObjName .

    type
        Game* = ref GameObj
        GameObj = object
            score*: int
    // result - это неявная переменная, служащая для задания возвращаемого значения функции
    proc newGame*(): Game =
        result = Game(score: 0)  // аналогично вызову new(result)
        result.doSomething()
    proc initGame*(): GameObj =
        GameObj(score: 0)
    

    Das direkte Erstellen von Objekten mithilfe ihrer Typen (Umgehen von Konstruktorfunktionen) ist nicht üblich.

    2048


    Der gesamte Spielcode passt in ca. 300 Codezeilen. Schreiben Sie jedoch ohne ausdrücklichen Grund so kurz wie möglich. Meiner Meinung nach weist dies auf ein ziemlich hohes Sprachniveau hin.

    Aus der Vogelperspektive sieht das Spiel so aus:

    Code "main":

    import os, strutils, net
    import field, render, game, input
    const DefaultPort = 12321
    let port = if paramCount() > 0: parseInt(paramStr(1))
              else: DefaultPort
    var inputProcessor = initInputProcessor(port = Port(port))
    var g = newGame()
    while true:
        render(g)
        var command = inputProcessor.read()    
        case command:
        of cmdRestart:
            g.restart()
        of cmdLeft:
            g.left()
        of cmdRight:
            g.right()
        of cmdUp:
            g.up()
        of cmdDown:
            g.down()
        of cmdExit:
           echo "Good bye!"
           break       
    

    Das Feld wird mit Textgrafiken und Farbcodes auf der Konsole angezeigt. Aus diesem Grund funktioniert das Spiel nur unter Linux und Mac OS. Die Befehlseingabe über getch () war aufgrund des seltsamen Verhaltens der Konsole bei Verwendung dieser Funktion in Nim nicht möglich. Curses for Nim wird derzeit portiert und ist nicht in der Liste der verfügbaren Pakete aufgeführt (obwohl das Paket bereits vorhanden ist). Daher musste ich einen E / A-Handler verwenden, der auf dem Blockieren von Lesevorgängen vom Socket und einem zusätzlichen Python-Client basiert.

    Der Start dieses Wunders ist wie folgt:

    # в терминале 1
    git clone https://github.com/iximiuz/nim-2048.git
    cd nim-2048
    nim c -r nim2048
    # в терминале 2
    cd nim-2048
    python client.py
    

    Was ich vom Entwicklungsprozess anmerken möchte. Der Code wird gerade geschrieben und ausgeführt! Solche Erfahrungen habe ich mit kompilierten Sprachen abgesehen von Java nicht gesehen. Darüber hinaus kann der geschriebene Code als „sicher“ angesehen werden, wenn keine ptr- Zeiger verwendet werden . Die Syntax und das modulare System sind Python sehr ähnlich, sodass Sucht nur einen minimalen Zeitaufwand erfordert. Ich hatte bereits eine fertige Implementierung von 2048 in Python und war angenehm überrascht, als sich herausstellte, dass der Code mit minimalen Korrekturen buchstäblich kopiert und in den Code von Nim eingefügt werden kann, und es beginnt zu funktionieren! Ein weiterer schöner Punkt - Nim wird mit Batterien geliefert. Aufgrund der hohen Pakete net Code Sockel -server weniger als 10 Zeilen nimmt.

    Der vollständige Code des Spiels kann unter eingesehen werdenGithub .

    Anstelle einer Schlussfolgerung


    Nim hübsch! Das Schreiben von Code ist nett und das Ergebnis sollte schnell funktionieren. Die Kompilierung von Nim ist nicht nur in einer ausführbaren Datei, sondern auch in JavaScript möglich. Sie können dieses interessante Feature hier lesen und den in Nim geschriebenen und in JavaScript kompilierten NES-Emulator hier spielen .

    Es bleibt zu hoffen, dass das Schreiben schneller und sicherer Programme dank Nim in Zukunft genauso viel Spaß macht wie das Programmieren in Python. Dies wirkt sich positiv auf die Anzahl der Stunden aus, die wir vor verschiedenen Fortschrittsbalken auf unseren Computern verbringen.

    Jetzt auch beliebt: