Dear All,
Why there are no constructors in NIM?
The reason I ask is that I see several instances where it would be very useful. Sure, one can resort to conventions like using a 'proc initTypename()' but that doesn't seem to be as convenient as using a constructor (or specific method like the __init__ in python).
As an example I will use a variation of a snippet of code is available somewhere in the NIM's documentation.
type
Dollar = distinct float
proc `+` (x, y: Dollar): Dollar {.borrow.}
...
proc `$` (x: Dollar): string = fmt"{float(x):.2f} $"
...
I my opinion this really shows the awesomeness of the type system of NIM, which can be used to enforce "dimensional correctness" of physical formulas in the code in a very clean way, by implementing just the right operators. However, let's imagine that I don't want to use floats to represent currency but I insist in using integer arithmetic. I could rewrite the above code as:
type
Dollar = distinct int
proc `+` (x, y: Dollar): Dollar {.borrow.}
...
But I also would like to represent cents, for that I could control how the Dollar is initialized, like so,
proc initDollar(x: float): Dollar = round(x * 100).Dollar
proc initDollar(x: int): Dollar = (x * 100).Dollar
And how is printed, like so,
proc `$` (x: Dollar): string = fmt"{float(x)/100:.2f} $"
This works just fine if I initialize my dollars as,
let d = 176.initDollar
The problem is that nothing prevents one from doing
let d = 176.Dollar
yielding an initialization different from what one might expect. What I would like to have is a mechanism to tailor how the Dollar is initialized using the latter form. For this example to be complete one also needed to control how Dollars would be converted back to integers and floats. Please understand that this is just an example to illustrate the idea and give context for the question in the beginning of this post. I don't really need to implement currency types.
Thanks in advance.
I'll assume for better or worse you are familiar with C++. Let's translate to C++ so it becomes obvious how Nim works differently.
let d = 176.Dollar
in c++ would be
auto d = static_cast<Dollar>(176); // maybe dragons!
in c++ what you wanted
auto d = Dollar(176);
in Nim convention what you wanted
let d = 176.toDollar
which actually reads better in my opinion because it conveys intent in more natural language.Here are multiple ways of initializing an object :
type
Dollar = distinct float
Transaction = object
amount: Dollar = Dollar(3.0)
proc `$`*(x: Dollar): string {.borrow.}
proc `$`*(x: Transaction): string =
$x.amount & "$"
proc test() =
var x = Transaction() # Default initialization based on type declaration
echo x # -> 3.0$
var y = Transaction(amount: Dollar(5.0)) # Explicit initialization
echo y # -> 5.0$
var z = Dollar(10.0) # Type converter
echo z
test()
Of course as you noted you can use initType proc :
# Use of proc initialization
proc initDollar*(val: float): Dollar = val.Dollar
proc initTransaction*(val: float = 3.0): Transaction =
result.amount = initDollar(val)
proc test2() =
var x = initTransaction() # Initialization based on default parameters of proc
echo x # -> 3.0$
var y = initTransaction(5.0) # Explicit proc initialization
echo y # -> 5.0$
test2()
You can also use this trick if you think it makes the code "cleaner" :
proc init(_: typedesc[Dollar], val: float) : Dollar =
Dollar(val)
proc init(_: typedesc[Transaction], val: float = 3.0) : Transaction =
result.amount = Dollar.init(val)
proc test3() =
var x = Transaction.init() # Initialization based on default parameters of proc
echo x # -> 3.0$
var y = Transaction.init(5.0) # Explicit proc initialization
echo y # -> 5.0$
test3()
I tried to study about constructors in C++ before and I found that there are many complex rules in C++ constructor. https://en.cppreference.com/w/cpp/language/constructor
I wonder if Nim can implement constructors that are simple and useful. I think calling a proc that initializes and returns the object is much simpler than constructors.
In your case, you can just stop constructing Dollar by calling the type name. Or define it as object type and 176.Dollar become compile error.
I tried to study about constructors in C++ before and I found that there are many complex rules in C++ constructor. https://en.cppreference.com/w/cpp/language/constructor
I used to not miss constructors as newObj or new[T] both work fine. However, recently I've seen a couple of bugs pop up lately on larger projects from code calling a default type constructor instead of the proper initObject. It can be tricky especially for less commonly used areas of a library.
It would be nice to be able to at least disable default constructors. We have various hooks now, so it seems it'd be a natural extension from there. There's just some cases when a default object constructor needs to be disabled or overridden.
It would be nice to be able to at least disable default constructors. We have various hooks now, so it seems it'd be a natural extension from there. There's just some cases when a default object constructor needs to be disabled or overridden.
what about {.requiresInit.}?
d = 176.toDollar
reads better. I'm fine with that. But the issue remains: since the language allows d = 176.Dollar
users of such module might be tempted to use it and get the unintended result. But I get it, this is how the language works. And probably there are good reasons for it. It just wasn't entirely clear to me whether I was missing something.terrible pattern as it does not compose with generics
So where can I find these generic procs that benefit from this design?
https://github.com/beef331/truss3d/blob/master/src/truss3D/atlasser.nim is a Rect agnostic guillotine square packer. Ideally this would work on any type but the concept needs expanded, as it is it should work with a Rect made of any builtin type.
My library Gooey constraints Vec s to the following, though ideally it just like the Rect would operate on any math type and not force a primitive numeric type.
type
Vec3* = concept vec, type V
vec.x is float32
vec.y is float32
vec.z is float32
not compiles(vec.w)
V.init(float32, float32, float32)
vec + vec is V
Vec2* = concept vec, type V
vec.x is float32
vec.y is float32
not compiles(vec.z)
V.init(float32, float32)
vec + vec is V
I mean just something as simple as:
proc something[T](x: T, y: int): T =
result = init(T)
result.setLen x.len
for i in result.mitems:
i = y
Demonstrates why init is better than initT. The proc is of dubious merit, but you can see how easily we've called the proper init function for our given type here. This is something which requires constructing a symbol via meta-programming to achieve with the initT pattern. In fact it's also highly relevant to this question because if you wrote this with a datatype that didn't require explicit initialisation you would likely skip the hassle of calling the proper initializer and all of a sudden you rely on the default value being correct.
The proc is of dubious merit, but you can see how easily we've called the proper init function for our given type here.
Yes but instead of init(T); setlen L it should have been init(T, L) for efficiency and then the real problem shows up which is "what is the generic all encompassing signature for a constructor". There isn't any.
Since the OP expressed a desire for "constructors", and I remember reading similar wishes in a number of posts a while back, perhaps the following tweak to the pattern would fill the need for those in need of "real" constructors:
proc construct(_: typedesc[YourType], ....): YourType = ...
This would enable code like:
var x = MyType.construct(arg1, arg2, ...)
That seems similar enough to the C++/Java/etc constructor pattern, and it uses the word "construct", so it may satisfy such requests (only half joking).
Snarkiness aside I really like the pattern, regardless of the proc name used, and intend to adopt it in my own work. Thanks for this helpful discussion.