An aspect of Nim that’s giving me trouble is ensuring that objects get initialized properly. I’ve gotten used to constructors in C++/Swift/TypeScript, which enforce that any instance of object type T has been properly initialized. Whereas in Nim, as in Go and Objective-C, objects get initialized as all zeros/nil and initialization is by convention, using procs like init(var T).
I’m finding bugs in my code where I forget to initialize an object, writing var t = T() but then forgetting to call t.init(a,b,c) afterwards. Is there a way to prevent T() from being used outside T’s module? IIRC, Nim 2 will have an annotation for object fields that requires an explicit initialization of that field; is that correct? Is there an experimental pragma for this in 1.6.x?
In the standard library I see a convention of defining a newT(...) proc that creates and initializes a T. Unfortunately this doesn’t support subclassing: if I define a type U = object of T, then to initialize a U I need to first initialize it as a T but there’s no separate function for that. Maybe there should be both a newT() and an initT() function, for subclassable types?
Anyway, I’m wondering if the Nim community has evolved some explicit best practices for object initialization. For instance, in Objective-C the universal convention is to have an init method, which must first call the superclass’s init method if it exists; and there can optionally be convenience methods that create and initialize an instance in one call.
objects get initialized as all zeros/nil
I guess you know that Nim devel and 2.0 has object field initialization like in type definition like
type
Vertex
dist: float = math.Inf
type
O {. requiresInit.} = object
And also use --experimental:strictDefs.
Thanks! But how do I handle a situation where I subclass a type marked requiresInit that has private fields? Here’s an example (Nim 1.6.10)
#### foo.nim
type Foo* {. requiresInit .} = object of RootObj
foo: int
proc newFoo*(f: int) : Foo = Foo(foo: f)
#### bar.nim
import foo
type FooBar* = object of Foo
bar: int
proc newFooBar(f: int, b: int) : FooBar = FooBar(foo: f, bar: b) # Error: the field 'foo' is not accessible.
Yeah, that's the downside of Nim's ultra minimal support for OO. You can use this idiom:
# foos.nim
type Foo* = object of RootObj
foo: int
proc newFoo*(f: int) : Foo = Foo(foo: f)
proc init*(f: var Foo, x: int) = f.foo = x
import foos
type FooBar* = object of Foo
bar: int
proc newFooBar(f: int, b: int) : FooBar =
result = FooBar(bar: b)
init result, f
I found a partial workaround, by redefining newFoo as:
proc newFoo* [F: Foo = Foo](f: int) : F = F(foo: f)
Then I can initialize a FooBar like so:
proc newFooBar(f: int, b: int) : FooBar =
result = newFoo[FooBar](f)
result.bar = b
The only drawback is I can’t make FooBar use requiresInit too — that produces an error in newFoo. But I think having a base class prevent default initialization is probably enough to ward off accidents in subclasses.
Yeah, that's the downside of Nim's ultra minimal support for OO.
:) I respect that you don’t want Nim to be a full-on OO language. But if there were a simple change that would make the constructor pattern work better, would you consider it? I don’t have one in mind but I’ll think about it.
You can also structure your code so that "initial all zeros" objects are valid objects.
Yup, I’m used to that from Go, and it’s often good enough. But in this case my actual code has something like an OwnedObject type that must have an owner, so nil is not a valid value for its owner field.
But if there were a simple change that would make the constructor pattern work better, would you consider it? I don’t have one in mind but I’ll think about it.
Definitely. I thought out parameters might offer something in this regard but right now they don't.
These might be workarounds and/or opportunities for compiler enhancement. A pure alias doesn't respect requiresInit itself, though maybe that could be changed. A distinct wrapper does work if you don't mind that. Unfortunately converting FooBarImpl to FooBarO doesn't seem to work, though that seems like it might be the cleanest solution if it did.
# foo.nim
type Foo* {.requiresInit.} = object of RootObj
foo: int
proc newFoo*[F:Foo=Foo](f: int): F = F(foo: f)
# bar.nim
import foo
type FooBarImpl = object of Foo
bar: int
type FooBar* {.requiresInit.} = FooBarImpl
type FooBarD* {.requiresInit.} = distinct FooBarImpl
type FooBarC* {.requiresInit.} = distinct FooBarImpl
converter toFooBarImpl*(x: FooBarC): FooBarImpl = FooBarImpl(x)
type FooBarO* {.requiresInit.} = object of FooBarImpl
proc newFooBar*(f: int, b: int): FooBar =
var r = newFoo[FooBarImpl](f)
r.bar = b
result = FooBar(r)
proc newFooBarD*(f: int, b: int): FooBarD =
var r = newFoo[FooBarImpl](f)
r.bar = b
result = FooBarD(r)
proc newFooBarC*(f: int, b: int): FooBarC =
var r = newFoo[FooBarImpl](f)
r.bar = b
result = FooBarC(r)
proc newFooBarO*(f: int, b: int): FooBarO =
var r = newFoo[FooBarImpl](f)
r.bar = b
result = FooBarO(r)
# baz.nim
import bar
block:
var x = newFooBar(1, 2)
echo x
#var y: FooBar # fails due to Foo not FooBar
block:
var x = newFooBarD(1, 2)
# echo x # need to borrow
# var y: FooBarD # not allowed
block:
var x = newFooBarC(1, 2)
echo x
# var y: FooBarC # not allowed
block: discard
# var x = newFooBarO(1, 2) # invalid object conversion
Anyway, I’m wondering if the Nim community has evolved some explicit best practices for object initialization.
We have some here: https://status-im.github.io/nim-style-guide/language.objconstr.html
Just to summarise this a bit and connect with object destruction. The construction guide (https://status-im.github.io/nim-style-guide/language.objconstr.html) suggests using:
proc init(newT: T, <public_args>): T = T(<initialize all members>)
It should be proc init and not a func init, if there are side effects. For example, if you log the time when the object is constructed.
You cannot really use newType() because it will automatically create a destructor and your custom proc `=destroy`(this: T) won't override it. You'll get an error "Error: cannot bind another '=destroy' to: T; previous declaration was constructed here implicitly: ..."
To define a destructor you use `=destroy`(this: T) from the guide on destructors, move semantics and object life time: https://nim-lang.org/docs/destructors.html
And you want to use {. requiresInit .} to ensure the users use the init() proc on these objects and all members get correctly initialized, when the default values don't work for you. One thing to notice when {. requiresInit .} is used and init() call is missing, the error message is a bit vague: "Error: The T type doesn't have a default value. The following fields must be initialized:..."
So, if you want a simple RAII-like object that logs the execution time of a scope:
import std/times
import std/strformat
type
Timer* {. requiresInit .} = object
name*: string
start_time: DateTime
# just to note
# accessing an uninitialized DateTime will crash at run time with:
# nim-2.2.0/lib/pure/times.nim(1157, 3) `dt.monthdayZero != 0` Uninitialized datetime [AssertionDefect]
proc init(T: type Timer, arg_name: string): T = T(
name: arg_name,
start_time: now()
)
proc time*(timer: Timer): Duration =
return now() - timer.start_time
proc `=destroy`(this: Timer) =
echo fmt"Timer {this.name} : {this.time()}"
Usage example:
import std/os # sleep proc
proc run_some_time() =
let timeit: Timer = Timer.init("run_some_time")
sleep(4)
run_some_time()
# prints:
# Timer run_some_time : 4 milliseconds, 202 microseconds, and 331 nanoseconds
I.e. 3 things make up basic object life-time: