Хочется чего-то нового, быстрого, компилируемого, но при этом приятного на ощупь? Добро пожаловать под кат, где мы опробуем язык программирования Nim на реализации очередного клона игры 2048. Никаких браузеров, только хардкор, только командная строка!
В программе:
Объективно:
Nim — статически типизированный, императивный, компилируемый. Может быть использован в качестве системного ЯП, так как позволяет прямой доступ к адресам памяти и отключение сборщика мусора. Остальное — тут.
Субъективно
Многие из появляющихся сейчас языков программирования стремятся предоставить одну (или несколько) killer-feature, пытаясь с помощью них решить широкий класс задач (go routines в Go,
Код пишется в модулях (т.е. в файлах, 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 поддерживает создание новых типов на основе существующих, что-то вроде одиночного наследования, хотя предпочтение отдается композиции. Подробнее тут и тут.
Есть небольшая опасность — такая реализация ООП не подразумевает физическую группировку всех методов по работе с каким-либо типом в одном модуле. Таким образом, можно опрометчиво разбросать методы по работе с одним типом по всей программе, что, естественно, негативно скажется на поддержке кода.
Хотя 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)
Создавать объекты напрямую с использованием их типов (в обход функций-конструкторов) не принято.
Весь код игры уместился примерно в 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 http://ift.tt/1CVdRcd
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, и это благоприятно отразится на количестве часов, проводимых нами перед раличными прогресс-барами за нашими компьютерами.
This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.
Комментариев нет:
Отправить комментарий