Знакомство с Nim: пишем консольную 2048

от автора


Хочется чего-то нового, быстрого, компилируемого, но при этом приятного на ощуп? Добро пожаловать под кат, где мы опробуем язык программирования Nim на реализации очередного клона игры 2048. Никаких браузеров, только хардкор, только командная строка!

В программе:

Who is the Nim?

Объективно:

Nim — статически типизированный, императивный, компилируемый. Может быть использован в качестве системного ЯП, так как позволяет прямой доступ к адресам памяти и отключение сборщика мусора. Остальное — тут.

Субъективно

Многие из появляющихся сейчас языков программирования стремятся предоставить одну (или несколько) killer-feature, пытаясь с помощью них решить широкий класс задач (go routines в Go, адское управление памятью в Rust и пр). Nim не предлагает какой-либо особенной возможности. Это простой язык программирования, по синтаксиску напоминающий Python. Зато Nim позволяет писать программы легко. Практически также легко, как на столь высокоуровневом Python. При этом получающиеся на выходе программы по производительности должны быть сравнимы с аналогами на C, так как компиляция происходит не до уровня какой-либо виртуальной машины, а именно до машинных кодов.

Как выглядит ООП в Nim

Код пишется в модулях (т.е. в файлах, Python-style). Модули можно импортировать в других модулях. Есть функции (proc), классов нет. Зато есть возможность создавать пользовательские типы и вызывать функции с помощью Uniform Function Call Syntax (UFCS) с учетом их перегрузки. Таким образом следующие 2 строки кода эквивалентны:

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

А следующий код позволяет устроить ООП без классов в привычном понимании этого слова:

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() 

Также есть методы (method). Фактически то же, что и proc, отличие лишь в моменте связывания. Вызов proc статически связан, т.е. информация о типе в runtime уже не имеет особого значения. Использование method же может пригодиться, когда нужно выбирать реализацию на основании точного типа объекта в существующей иерархии в момент исполнения. И да, Nim поддерживает создание новых типов на основе существующих, что-то вроде одиночного наследования, хотя предпочтение отдается композиции. Подробнее тут и тут.

Есть небольшая опасность — такая реализация ООП не подразумевает физическую группировку всех методов по работе с каким-либо типом в одном модуле. Таким образом, можно опрометчиво разбросать методы по работе с одним типом по всей программе, что, естественно, негативно скажется на поддержке кода.

Немного C под капотом

Хотя Nim компилируется до предела, он это делает через промежуточную компиляцию в C. И это круто, потому что при наличии определенного бэкграунда можно посмотреть, что же на самом деле происходит в коде на Nim. Давайте рассмотрим следующий пример.

Объекты в Nim могут быть значениями (т.е. располагаться на стеке) и ссылками (т.е. располагаться в куче). Ссылки бывают двух типов — ref и ptr. Ссылки первого типа отслеживаются сборщиком мусора и при нулевом количестве ref count, объекты удаляются из кучи. Ссылки второго типа являются небезопасными и нужны для поддержки всяких системных штук. В данном примере мы рассмотрим только ссылки типа ref.

Типичный для Nim способ создания новых типов выглядит примерно так:

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

Т.е. создается обычный тип FooObj и тип «ссылка на FooObj». А теперь давайте посмотрим, что происходит при компиляции следующего кода:

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") 

Компилируем:

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

Результат в папке 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); } 

Выводы можно сделать следующие. Во-первых, код при желании легко понять и разобраться, что же происходит под капотом. Во-вторых, для двух типов FooObj и Foo была создана всего одна соответствующая структура в C. При этом переменные foo и fooRef являются экземпляром и указателем на экземпляр структуры, соответственно. Как и говорится в документации, foo — стековая перменная, а fooRef находится в куче.

Создание экземпляров

Создавать экземпляры в Nim принято двумя способами. В случае, если создается переменная на стеке, ее создают с помощью функции initObjName. Если же создается переменная в куче — 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) 

Создавать объекты напрямую с использованием их типов (в обход функций-конструкторов) не принято.

2048

Весь код игры уместился примерно в 300 строках кода. При этом без явной цели написать как можно короче. На мой взгляд, это говорит о достаточно высоком уровне языка.

С высоты птичьего полета игра выглядит так:

Код «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 buy!"        break        

Отрисовка поля происходит в консоль при помощи текстовой графики и цветовых кодов. Из-за этого игра работает только под Linux и Mac OS. Ввод команд не удалось сделать через getch() из-за странного поведения консоли при использовании этой функции в Nim. Curses для Nim сейчас в процессе портирования и не указан в списке доступных пакетов (хотя пакет уже существует). Поэтому пришлось воспользоваться обработчиком ввода/вывода на основе блокирующего чтения из сокета и дополнительного python-клиента.

Запуск этого чуда выглядит следующим образом:

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

Что хотелось бы отметить из процесса разработки. Код просто пишется и запускается! Такого опыта в компилируемых языках, не считая Java, я не встречал до этого. При этом написанный код можно считать «безопасным», если не используются указатели ptr. Синтаксис и модульная система очень сильно напоминают Python, поэтому привыкание занимает минимальное время. У меня уже была готовая реализация 2048 на Python, и я был приятно удивлен, когда оказалось, что код из нее можно буквально копировать и вставлять в код на Nim с минимальными исправлениями, и он начинает работать! Еще один приятный момент — Nim идет с батарейками в комплекте. Благодаря высокоуровневому модулю net код socket-сервера занимает меньше 10 строк.

Полный код игры можно посмотреть на github.

Вместо заключения

Nim красавчик! Писать код на нем приятно, а результат должен работать быстро. Компиляция Nim возможна не только в исполняемый файл, но и в JavaScript. Об этой интересной возможности можно почитать здесь, а поиграть в эмулятор NES, написанный на Nim и скомпилированный в JavaScript — здесь.

Хочется надеяться, что в будущем благодаря Nim написание быстрых и безопасных программ станет настолько же приятным процессом, как программирование на Python, и это благоприятно отразится на количестве часов, проводимых нами перед раличными прогресс-барами за нашими компьютерами.

ссылка на оригинал статьи http://habrahabr.ru/post/261801/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *