Do you want something new, fast, compiled, but at the same time pleasant to the touch? Welcome to the cat, where we try out the Nim programming language on the implementation of the next clone of the game 2048. No browsers, only hardcore, only the command line!
In a programme:
Who is the Nim?
Objectively:
Nim - statically typed, imperative, compiled. It can be used as a system PL, as it allows direct access to memory addresses and disabling the garbage collector. The rest is
here .
Subjectively
Many of the programming languages that are emerging now tend to provide one (or several) killer-features, trying to solve a wide class of problems with them (go routines in Go,
hell memory management in Rust, etc.). Nim does not offer any special features. This is a simple programming language, syntax-like in Python. But Nim makes it easy to write programs. Practically as easy as at such a high-level Python. At the same time, the resulting output of the program should be comparable with the analogues in C, since the compilation does not occur to the level of any virtual machine, but to machine codes.
')
What does OOP look like in Nim
The code is written in modules (i.e., in files, Python-style). Modules can be imported into other modules. There are functions (
proc ), no classes. But it is possible to create custom types and call functions using Uniform Function Call Syntax (
UFCS ), taking into account their overload. Thus, the following 2 lines of code are equivalent:
foo(bar, baz) bar.foo(baz)
And the following code allows you to arrange OOP without classes in the usual sense of the word:
type Game = object foo: int bar: string Car = object baz: int
There are also methods. In fact, the same as
proc , the only difference is in the moment of binding. The
proc call is statically linked, i.e. Type information in runtime no longer has a special meaning. Using
method can be useful when you need to choose an implementation based on the exact type of object in the existing hierarchy at the time of execution. And yes, Nim supports the creation of new types based on existing ones, something like a single inheritance, although composition is preferred. More
here and
here .
There is a small danger - such an implementation of OOP does not imply the physical grouping of all methods for working with any type in a single module. Thus, you can recklessly scatter methods for working with one type throughout the program, which, naturally, will negatively affect the support of the code.
Little C under the hood
Although Nim is compiled to the limit, it does it through an intermediate compilation in C. And this is cool, because if you have a certain background, you can see what is actually happening in the code in Nim. Let's consider the following example.
Objects in Nim can be values (i.e. located on the stack) and references (i.e., be placed on the heap). Links are of two types -
ref and
ptr . References of the first type are tracked by the garbage collector and at zero ref count, objects are removed from the heap. References of the second type are unsafe and are needed to support all sorts of system pieces. In this example, we will consider only references of type
ref .
The typical way for Nim to create new types looks like this:
type Foo = ref FooObj FooObj = object bar: int baz: string
Those. the usual type FooObj and the type “link to FooObj” are created. Now let's see what happens when the following code is compiled:
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")
Compile:
nim c -d:release test.nim cat ./nimcache/test.c
Result in the nimcache folder (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); }
Conclusions can be made as follows. First, the code, if desired, is easy to understand and figure out what happens under the hood. Secondly, for the two types of
FooObj and
Foo , only one corresponding structure was created in C. At the same time, the variables
foo and
fooRef are an instance and a pointer to the structure instance, respectively. As stated in the documentation, foo is a stack-based variable, and fooRef is on the heap.
Creating instances
Creating copies in Nim is done in two ways. If a variable is created on the stack, it is created using the
initObjName function. If a variable is created on the heap,
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)
Creating objects directly using their types (bypassing constructor functions) is not customary.
2048
All the game code fit in about 300 lines of code. At the same time, without an explicit goal, write as short as possible. In my opinion, this indicates a fairly high level of language.
From a bird's eye view, the game looks like this:
The main code:
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
The field is drawn to the console using text graphics and color codes. Because of this, the game only works under Linux and Mac OS. Commands could not be
entered via
getch () due to the strange behavior of the console when using this function in Nim.
Curses for Nim is now in the process of porting and is not listed in the list of available packages (although the package already exists). So I had to use an I / O handler based on a blocking read from the socket and an additional python client.
The launch of this miracle is as follows:
What I would like to mention from the development process. The code is simply written and launched! This experience in compiled languages, not counting Java, I have not met before. In this case, the written code can be considered "safe" if the
ptr pointers are not used. The syntax and modular system is very much like Python, so getting used to it takes minimal time. I already had a ready implementation of 2048 in Python, and I was pleasantly surprised when it turned out that the code from it can literally be copied and pasted into the code on Nim with minimal fixes, and it starts working! Another nice thing is that Nim comes with batteries included. Thanks to the high-level
net module, the
socket server code takes less than 10 lines.
The full code of the game can be viewed on github .
Instead of conclusion
Nim handsome! Writing code on it is nice, and the result should work quickly. Compiling Nim is possible not only in an executable file, but also in JavaScript. You can read about this interesting feature
here , and play the NES emulator written in Nim and compiled into JavaScript
here .
Hopefully, in the future, thanks to Nim, writing fast and safe programs will become as enjoyable as programming in Python, and this will have a favorable effect on the number of hours we spend before our progress bars at our computers.