"Repetition gives absurdity a look of prudence." - Norman Wildberger
{. and .}
{. and .}
. They are not covered in this tutorial. For a list of available pragmas, refer to the manual or user manual .T()
object constructor is usually used only in internal development, and the programmer must provide a special procedure for initialization (it is called a constructor).of
, with which you can check the type of an object: type Person = ref object of RootObj name*: string # * , `name` age: int # Student = ref object of Person # Student Person id: int # id var student: Student person: Person assert(student of Student) # true # : student = Student(name: "Anton", age: 5, id: 2) echo student[]
*
). Unlike tuples, different object types are never equivalent. New object types can only be defined in the type section.object of
syntax. Multiple inheritance is not currently supported. If there is no suitable ancestor for the object type, then it can be made the RootObj
ancestor, but this is just an agreement. Objects without an ancestor are implicitly declared as final
. To enter a new object that is not inherited from system.RootObj
, you can use the inheritable
pragma (this is used, for example, in the GTK wrapper).let person: Person = Student(id: 123)
fields of the child class will be truncated.Note: for simple code reuse, composition ( “part of” relation ) is often preferable to inheritance ( “is” relation ). . Because objects in Nim are value types, composition is just as effective as inheritance.
type Node = ref NodeObj # NodeObj NodeObj = object le, ri: Node # sym: ref Sym # , Sym Sym = object # name: string # line: int # , code: PNode #
cast
operator and forces the compiler to interpret binary data as the specified type.destination_type(expression_to_convert)
(like a normal call). proc getID(x: Person): int = Student(x).id
x
not an instance of Student
, then an InvalidObjectConversionError
will be thrown. # , # Nim type NodeKind = enum # nkInt, # nkFloat, # nkString, # nkAdd, # nkSub, # nkIf # if Node = ref NodeObj NodeObj = object case kind: NodeKind # ``kind`` of nkInt: intVal: int of nkFloat: floatVal: float of nkString: strVal: string of nkAdd, nkSub: leftOp, rightOp: PNode of nkIf: condition, thenPart, elsePart: PNode var n = PNode(kind: nkFloat, floatVal: 1.0) # `FieldError`, # n.kind : n.strVal = ""
join
is a string or array method?obj.method(args)
means the same as method(obj, args)
. If there are no arguments, then you can skip parentheses: obj.len
instead of len(obj)
. echo("abc".len) # , echo(len("abc")) echo("abc".toUpper()) echo({'a', 'b', 'c'}.card) stdout.writeLine("Hallo") # , writeLine(stdout, "Hallo")
import strutils, sequtils stdout.writeLine("Give a list of numbers (separated by spaces): ") stdout.write(stdin.readLine.split.map(parseInt).max.`$`) stdout.writeLine(" is the maximum!")
type Socket* = ref object of RootObj host: int # , proc `host=`*(s: var Socket, value: int) {.inline.} = ## s.host = value proc host*(s: Socket): int {.inline.} = ## s.host var s: Socket new s s.host = 34 # , `host=`(s, 34)
inline
procedures.)[]
: type Vector* = object x, y, z: float proc `[]=`* (v: var Vector, i: int, value: float) = # setter case i of 0: vx = value of 1: vy = value of 2: vz = value else: assert(false) proc `[]`* (v: Vector, i: int): float = # getter case i of 0: result = vx of 1: result = vy of 2: result = vz else: assert(false)
v[]
.proc
keyword with method
: type PExpr = ref object of RootObj ## PLiteral = ref object of PExpr x: int PPlusExpr = ref object of PExpr a, b: PExpr # : 'eval' method eval(e: PExpr): int = # quit "to override!" method eval(e: PLiteral): int = ex method eval(e: PPlusExpr): int = eval(ea) + eval(eb) proc newLit(x: int): PLiteral = PLiteral(x: x) proc newPlus(a, b: PExpr): PPlusExpr = PPlusExpr(a: a, b: b) echo eval(newPlus(newPlus(newLit(1), newLit(2)), newLit(4)))
newLit
and newPlus
are procedures, since for them it is better to use static binding, and eval
already a method, because it needs dynamic binding. type Thing = ref object of RootObj Unit = ref object of Thing x: int method collide(a, b: Thing) {.inline.} = quit "to override!" method collide(a: Thing, b: Unit) {.inline.} = echo "1" method collide(a: Unit, b: Thing) {.inline.} = echo "2" var a, b: Unit new a new b collide(a, b) # : 2
collide
2 is preferable to collide
1, since the resolution works from left to right. Thus, Unit
, Thing
preferable to Thing
, Unit
.Performance note : Nim does not create a table of virtual methods, but generates dispatch trees. This avoids costly indirect branching on method calls and allows embedding. But other optimizations, such as computations at compile time or removing dead code, do not work with methods.
system
module defines an exception hierarchy to which you can bind. Exceptions occur from system.Exception
, which provides a generic interface.msg
field.raise
commandraise
command: var e: ref OSError new(e) e.msg = "the request to the OS failed" raise e
raise
keyword is not followed by an expression, the last exception is raised again . In order not to write the above code, you can use the newException
template from the system
module: raise newException(OSError, "the request to the OS failed")
try
commandtry
command handles exceptions: # , , # var f: File if open(f, "numbers.txt"): try: let a = readLine(f) let b = readLine(f) echo "sum: ", parseInt(a) + parseInt(b) except OverflowError: echo "overflow!" except ValueError: echo "could not convert string to integer" except IOError: echo "IO error!" except: echo "Unknown exception!" # reraise the unknown exception: raise finally: close(f)
try
are executed until an exception occurs. In this case, the corresponding except
branch will be executed.except
block is executed if the exception that is thrown is not included in the list of those explicitly listed. This is similar to the else
branch in the if
command.finally
branch is present, then it is always executed after executing exception handlers.except
branch. If an exception is not handled, it is distributed through the call stack. This means that if an exception occurs, the rest of the procedure, which is not inside the finally
block, will not be executed.except
branch, you can use the getCurrentException()
and getCurrentExceptionMsg()
procedures from the system
module. Example: try: doSomethingHere() except: let e = getCurrentException() msg = getCurrentExceptionMsg() echo "Got exception ", repr(e), " with message ", msg
{.raises.}
you can specify that the procedure can raise a specific set of exceptions or not raise exceptions at all. If the {.raises.}
Pragma is used, the compiler will verify that it is true. For example, if you specify that the procedure raises IOError
, and at some point it (or one of the called procedures) raises another exception, the compiler will refuse to compile it. Usage example: proc complexProc() {.raises: [IOError, ArithmeticError].} = ... proc simpleProc() {.raises: [].} = ...
{.raises.}
to existing code, the compiler can also help you. You can add the pragma command {.effects.}
To the procedure and the compiler will output all the effects that appear at this point (exception tracking is part of the Nim effect system). Another workaround for getting the list of exceptions thrown by the procedure is to use the Nim doc2
, which generates documentation for the entire module and decorates all the procedures with a list of the exceptions thrown. You can read more about the effects system and the corresponding pragmas in the manual . type BinaryTreeObj[T] = object # BinaryTree # ``T`` le, ri: BinaryTree[T] # ; nil data: T # BinaryTree*[T] = ref BinaryTreeObj[T] # , proc newNode*[T](data: T): BinaryTree[T] = # new(result) result.data = data proc add*[T](root: var BinaryTree[T], n: BinaryTree[T]) = # if root == nil: root = n else: var it = root while it != nil: # ; ``cmp`` # , ``==`` ``<`` var c = cmp(it.data, n.data) if c < 0: if it.le == nil: it.le = n return it = it.le else: if it.ri == nil: it.ri = n return it = it.ri proc add*[T](root: var BinaryTree[T], data: T) = # : add(root, newNode(data)) iterator preorder*[T](root: BinaryTree[T]): T = # . # , ( # ): var stack: seq[BinaryTree[T]] = @[root] while stack.len > 0: var n = stack.pop() while n != nil: yield n.data add(stack, n.ri) # n = n.le # var root: BinaryTree[string] # BinaryTree ``string`` add(root, newNode("hello")) # ``newNode`` add(root, "world") # for str in preorder(root): stdout.writeLine(str)
add
procedure for sequences is not hidden and is used in the preorder
iterator. template `!=` (a, b: expr): expr = # System not (a == b) assert(5 != 6) # : assert(not (5 == 6))
!=
, >
, >=
, in
, isnot
, isnot
are actually templates: as a result, if you overloaded the operator ==
, then the operator !=
Becomes available automatically and works correctly (except for floating-point numbers IEEE - NaN
breaks a strict boolean logic).a > b
turns into b < a
. a in b
transforms into contains(b, a)
. isnot
and isnot
get the obvious meaning. const debug = true proc log(msg: string) {.inline.} = if debug: stdout.writeLine(msg) var x = 4 log("x has the value: " & $x)
debug
once set to false
, then the rather costly operations $
and &
will still be executed! (The calculation of the arguments for the procedures is made "greedy.")log
procedure into a template solves this problem: const debug = true template log(msg: string) = if debug: stdout.writeLine(msg) var x = 4 log("x has the value: " & $x)
expr
(for expressions), stmt
(for commands) or typedesc
(for type typedesc
). If the template does not explicitly indicate the type of the return value, then stmt
used for compatibility with procedures and methods.stmt
parameter, then it must be the last in the template declaration. The reason is that the commands are passed to the template using a special syntax with a colon ( :
: template withFile(f: expr, filename: string, mode: FileMode, body: stmt): stmt {.immediate.} = let fn = filename var f: File if open(f, fn, mode): try: body finally: close(f) else: quit("cannot open: " & fn) withFile(txt, "ttempl3.txt", fmWrite): txt.writeLine("line 1") txt.writeLine("line 2")
writeLine
commands writeLine
bound to the body
parameter. The withFile
template contains service code and helps avoid a common problem: forget to close the file. Note that the let fn = filename
command ensures that the filename
will be evaluated only once.macros
module.macrostmt
syntax (command macro)debug
command that takes any number of arguments: # Nim API, # ``macros``: import macros macro debug(n: varargs[expr]): stmt = # `n` AST Nim, ; # : result = newNimNode(nnkStmtList, n) # , : for i in 0..n.len-1: # , ; # `toStrLit` AST : result.add(newCall("write", newIdentNode("stdout"), toStrLit(n[i]))) # , ": " result.add(newCall("write", newIdentNode("stdout"), newStrLitNode(": "))) # , : result.add(newCall("writeLine", newIdentNode("stdout"), n[i])) var a: array[0..10, int] x = "some string" a[0] = 42 a[1] = 45 debug(a[0], a[1], x)
write(stdout, "a[0]") write(stdout, ": ") writeLine(stdout, a[0]) write(stdout, "a[1]") write(stdout, ": ") writeLine(stdout, a[1]) write(stdout, "x") write(stdout, ": ") writeLine(stdout, x)
macro case_token(n: stmt): stmt = # # ... ( -- :-) discard case_token: # , of r"[A-Za-z_]+[A-Za-z_0-9]*": return tkIdentifier of r"0-9+": return tkInteger of r"[\+\-\*\?]+": return tkOperator else: return tkUnknown
import strutils, tables proc readCfgAtRuntime(cfgFilename: string): Table[string, string] = let inputString = readFile(cfgFilename) var source = "" result = initTable[string, string]() for line in inputString.splitLines: # if line.len < 1: continue var chunks = split(line, ',') if chunks.len != 2: quit("Input needs comma split values, got: " & line) result[chunks[0]] = chunks[1] if result.len < 1: quit("Input file empty!") let info = readCfgAtRuntime("data.cfg") when isMainModule: echo info["licenseOwner"] echo info["licenseKey"] echo info["version"]
version,1.1 licenseOwner,Hyori Lee licenseKey,M1Tl3PjBWO2CC48m
readCfgAtRuntime
will open the given file name and return Table
from the module tables
. File parsing is done (without error handling or boundary cases) using a procedure splitLines
from a module strutils
. There are many things that can go wrong; Remember that it explains how to run the code at compile time, and not how to implement copy protection correctly.data.cfg
that would otherwise need to be distributed along with the binary. Plus, if the information is really constant, then from the point of view of logic, there is no sense in keeping it changeable.global variable, better if it is constant. Finally, one of the most valuable pieces is that we can implement some checks at the compilation stage. You can take it as an improved unit test, which does not allow you to get a binary in which something is not working. This prevents users from delivering broken programs that do not start because of a failure in one small critical file.parseStmt
from the module macros
. Here is the modified source code that implements the macro: 1 import macros, strutils 2 3 macro readCfgAndBuildSource(cfgFilename: string): stmt = 4 let 5 inputString = slurp(cfgFilename.strVal) 6 var 7 source = "" 8 9 for line in inputString.splitLines: 10 # Ignore empty lines 11 if line.len < 1: continue 12 var chunks = split(line, ',') 13 if chunks.len != 2: 14 error("Input needs comma split values, got: " & line) 15 source &= "const cfg" & chunks[0] & "= \"" & chunks[1] & "\"\n" 16 17 if source.len < 1: error("Input file empty!") 18 result = parseStmt(source) 19 20 readCfgAndBuildSource("data.cfg") 21 22 when isMainModule: 23 echo cfglicenseOwner 24 echo cfglicenseKey 25 echo cfgversion
readCfgAtRuntime
gets a string parameter. However, in the macro version, although it is declared string, it is only the external interface of the macro. When a macro is run, it actually gets the object PNimNode
, not the string, and we need to call the procedure strVal
from the module macros
(line 5) to get the string passed to the macro.readFile
from the modulesystem
due to FFI limitations at compile time. If we try to use this procedure (or any other, depending on the FFI), the compiler will generate an error with the message that it cannot calculate the source code dump of the macro and add a stack listing to it showing where the compiler was at the time of the error. We can bypass this restriction by using the procedure slurp
from the module system
, which is made specifically for the compilation stage (there is also a similar procedure gorge
that executes the external program and intercepts its output).Table
. Instead, it generates the Nim source code in the source variable. For each line of the configuration file, a constant variable will be generated (line 15). To avoid conflicts, we prefix these variables cfg
. In general, all that the compiler does is replace the macro call line with the following code fragment: const cfgversion= "1.1" const cfglicenseOwner= "Hyori Lee" const cfglicenseKey= "M1Tl3PjBWO2CC48m"
quit
to exit (which we could call), this version calls the procedure error
(line 14). The procedure error
does the same as quit
but also displays the source code and the line number of the file where the error occurred, which helps the programmer to find the error during the compilation. In this situation, we would be pointed at the line that calls the macro, and not at the line data.cfg
that we process: we have to control this ourselves.macros
. At first glance, this seems like a daunting task. But we can use the macro dumpTree
, using it as a command macro, not an expression macro. Since we know that we want to generate a chunk of characters const
, we can create the following source file and compile it to see what the compiler expects from us : import macros dumpTree: const cfgversion: string = "1.1" const cfglicenseOwner= "Hyori Lee" const cfglicenseKey= "M1Tl3PjBWO2CC48m"
StmtList ConstSection ConstDef Ident !"cfgversion" Ident !"string" StrLit 1.1 ConstSection ConstDef Ident !"cfglicenseOwner" Empty StrLit Hyori Lee ConstSection ConstDef Ident !"cfglicenseKey" Empty StrLit M1Tl3PjBWO2CC48m
ConstSection
and ConstDef
. If we moved all these constants into a single block const
, we would see only one ConstSection
with three descendants.dumpTree
first constant, it explicitly defines the type of the constants. That is why, in the output tree, the two last constants have a second descendant Empty
, and the first has a string identifier. So, in general, a definition const
consists of an identifier, an optional type (which may be an empty node), and a value. Armed with this knowledge, let's look at the complete version of the AST macro: 1 import macros, strutils 2 3 macro readCfgAndBuildAST(cfgFilename: string): stmt = 4 let 5 inputString = slurp(cfgFilename.strVal) 6 7 result = newNimNode(nnkStmtList) 8 for line in inputString.splitLines: 9 # 10 if line.len < 1: continue 11 var chunks = split(line, ',') 12 if chunks.len != 2: 13 error("Input needs comma split values, got: " & line) 14 var 15 section = newNimNode(nnkConstSection) 16 constDef = newNimNode(nnkConstDef) 17 constDef.add(newIdentNode("cfg" & chunks[0])) 18 constDef.add(newEmptyNode()) 19 constDef.add(newStrLitNode(chunks[1])) 20 section.add(constDef) 21 result.add(section) 22 23 if result.len < 1: error("Input file empty!") 24 25 readCfgAndBuildAST("data.cfg") 26 27 when isMainModule: 28 echo cfglicenseOwner 29 echo cfglicenseKey 30 echo cfgversion
string
and writing the source code into it as if it were written manually, we use the variable directly result
and create a command list node ( nnkStmtList
) that will contain our descendants (line 7).nnkConstDef
) and wrap it with a section of constants (nnkConstSection
). Once these variables are created, we fill them hierarchically (line 17), as shown in the previous dump of the AST tree: the definition of a constant is a descendant of the section definition and contains an identifier node, an empty node (let the compiler itself guess what type it is) and the string literal by value.dumpTree
. But it cannot be used inside a macro that you write or debug. Instead, display the line generated treeRepr
. If at the end of this example you add echo treeRepr(result)
, you will see the same conclusion as when using the macro dumpTree
. Call it at the end Optionally, you can call it at any point in the macro with which you have problems.Source: https://habr.com/ru/post/271361/
All Articles