Pros and Cons of this method vs restricting Object Constructors from being used outside of their type's module
- 'Object Constructor' refers to
TypeName(...)
, written shorter asT()
- 'Named Constructor' refers to
newT(...)
,initT(...)
, etc.. but alsoT.new(...)
-
T()
is the most direct syntactical way to describe the creation of a typeT
(even if that's type conversion). -
T()
is the only pattern where the return type is known "at a glance" because it is the type name. -
Due to it's direct relationship
T()
will be the most obvious and attractive syntax to the largest number of people. -
Many, if not most types need only one or few obvious and api dictated ways to construct a valid object.
-
It doesn't break existing code! Adding in the option to overload 'n lock object construction to a specific parameter interface could prevent code from using parameterless constructors, but only after those constructors are written. Simply adding the feature would not break existing code.
-
Extending object constructor syntax does not remove anything. Named constructors, in all there forms, would still be available and encouraged for some situations and as a way to distinguish between distinct constructors sharing parametric interface, or as just to be explict.
-
T()
syntax remains a useful pattern available for every type. When nocreate()
proc is define,T()
does exactly what it does today, eg, it gives you a 'default' object.create
makes this same pattern useful for the many situation where you need objects to be constructed procedurally in order to correctly setup their state [1]. This removes mental overhead required to properly construct any arbitrary type (plus, invalid construction is caught at compile-time, and constructor parameters can be assessed and suggested by the compiler when someone does not provide a valid set [2]). -
T()
is the only pattern in which generic code can construct an object given only the type name and a list of parameters. Named constructors either need to follow a specific idiom to work, eg,newT(): T
ornew(t:type T): T
. Types which do not follow this interface, or who's interface is unknown, are not as useful from generic code compared to my proposed constructor syntax. -
T()
's allocation is type-specific and largely decoupled from the user's concern. Eg, aref object
will be allocated on the heap while a regularobject
will be allocated on the stack. This behavior saves the end-user from having to both think about, or explicitly callnew
in order to instantiate a variable. By defaultcreate
would be triggered after default allocation, but combined with a{.allocator.}
pragmacreate
could be used as a mechanism to transparently customize a type's allocation [3]. Compare this to named constructors where allocation is idiomatically explicit, eg,newT()
andinitT()
both return a T only in different ways. Conflating allocation with construction (by default, which explicit ability to state otherwise) is more inline with Nim's "useful defaults" philosophy.
-
The
create
procedure is decoupled from theT()
call. This could arguably make tracing through code more difficult, though I don't see how this would be any worse than the existingdestroy
system. Nor do I think, given this would be a well documented and common place rule, thatcreate
would be a lasting source of confusion. -
The importing rules could be a source of confusion, or even a source of bugs (depending on how they're handled). Eg,
from module import Foo, someProc
imports the typeFoo
and a procsomeProc
but not the constructor. The solution is to either always importcreate
along with theFoo
or to simply make a warning/error for this scenario. More input is needed here.
type
Foo = object
Bar = object
Baz = object
proc create(f:var Foo) = # setup Foo properly
proc create(b:var Bar, s:string) = # setup Bar properly
let
foo = Foo() # invokes 'create(foo)' implicitly
bar = Bar("abc") # invokes 'create(bar, "abc")' implicitly
baz = Baz() # no 'create' provided, but still useful for allocation
type Foo = object
...
proc create(f:var Foo, path:string) =
...
let
f = Foo() # Error: invalid parameters, possible options: 'Foo(path:string)'
type
Foo = ref object
x, y: int
Bar = ref object
x, y: int
Baz = ref object
proc create(f:var Foo) =
## prepares a Foo on the heap for use
# note: 'f' is conveniently already allocated for this pattern, which helps
# since it's common we only want to focus on initialization, not allocation.
f.x = 123
f.y = 456
var barCache = MemoryCache[Bar]()
proc create(b:var Bar) {.allocator.} =
## prepares a Bar in a memory cache for use
b = barCache.borrowInstance()
b.x = 123
b.y = 456
proc destroy(b:var Bar) =
## returns a Bar to the memory cache for reuse later
barCache.returnInstance(b)
let
# These are syntactically identical as they conceptually do the same thing,
# eg, they introduce a new instance of a type into the program. However, they
# do somewhat different things 'under the hood'.
f = Foo() # default allocation + create
b = Bar() # create + cached allocation
z = Baz() # default allocation