Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] standardize constructors as: Foo.New(args) instead of newFoo(args) #34

Closed
timotheecour opened this issue Apr 2, 2018 · 18 comments

Comments

@timotheecour
Copy link
Member

motivation

this is the analog of https://github.com/nim-lang/Nim/issues/7430 (universal conversion operator (cf D's a.to!T) (std.conv.to)) but for constructors.

similar motivation as:
https://forum.nim-lang.org/t/703 Constructors (and tiny bit of destructors)
https://forum.nim-lang.org/t/1190/3 [RFC] Constructors proposition

in short: newFoo(args) is not generic (hard to use in generic code given a generic type), pollutes namespace, not standard (lots of languages don't need composite names).

options that have been considered as replacement for newFoo(args):

proc new Foo(x: int): Foo = Foo(x: x)
echo new Foo(123)

but that doesn't work as new Foo already has a meaning (GC allocation) and creates a ref Foo, not a Foo

My proposal is the following: Foo.New(args)

Here's an example:

type
  Foo=object
    age:int

#instead of: proc newBar(age:int): Bar = Bar(age:age)
proc New(T: typedesc[Foo], age:int): Foo = Foo(age:age)
let a=Foo.New(100)
assert a.type is Foo
assert a.age == 100

# eg showing generic use, which would be hard using newFoo syntax
let a2 = a5.type.New(101)
# eg showing it works if all we have is an alias
type FooAlias=Foo
let a3=FooAlias.New(100)

Advantages

  • no new language support needed
  • backward compatible (newFoo constructors can be changed gradually to Foo.New constructors, independently for each Foo)
  • can be used in generic code
  • no namespace pollution

another advantage is it allows to define constructors for multiple types, with just 1 definition, eg:

# for example
proc New(T: isSomeTypeConstraint, arg:typed): T = ...

note

as for name to use, this can be debated , eg: instead of New: make, create, ctor etc

@PMunch
Copy link

PMunch commented Apr 3, 2018

I like this idea of generic procedures, not sure why no-one has thought of this before. It solves the problem beautifully and is much more flexible than the current system. I'd love to see this, the to proposal, read from streams, and other similar cases get implemented across the stdlib before a 1.0 release. Would make everything much more consistent and nice and generic, plus it feels like much less of a hack.

@yglukhov
Copy link
Member

yglukhov commented Apr 3, 2018

I'm using somewhat similar approach in nimx, but with a twist that new does nothing except allocation and init. So View subclasses override their init. This is useful in case of class hierarchy when subclass.init needs to call super.init.

So to summarize what I'd want, is a new(t: typedesc[T], args: varargs[untyped]): (T or ref T) routine that would allocate + call init(result, args...).

@Araq
Copy link
Member

Araq commented Apr 3, 2018

Still has some of the disadvantages that I've described on the forum and these are not addressed in your proposal.

no new language support needed

Irrelevant without more justification of why that change is necessary in the first place.

backward compatible (newFoo constructors can be changed gradually to Foo.New constructors,

Irrelevant without more justification of why that change is necessary in the first place.

can be used in generic code

How so? You need to elaborate on this point with real use cases.

no namespace pollution

These new constructors would pollute the namespace in comparable ways.

@PMunch
Copy link

PMunch commented Apr 3, 2018

Simple example:

type
  Bar = object
    age: int
  Baz = object
    height: int

proc new(T: typedesc[Bar], age: int): Bar = Bar(age: age)
proc new(T: typedesc[Baz], height: int): Baz = Baz(height: height)

let a = Bar.new(100)

template newInst(old: untyped, ageOrHeight: int): untyped =
  old.type.new(ageOrHeight)

proc newInstProc(old: Bar | Baz, ageOrHeight: int): Bar | Baz =
  old.type.new(ageOrHeight)

let b = a.newInst(123)
let c = a.newInstProc(123)

Note that this trivial example is super dumb, but it's not impossible to imagine this being useful for lot's of other meta-programming cases, or just generic procedures as shown above. Doing what's done in the example today requires a macro that get's the type name and generates a call to new<typeName> which isn't very pretty.

As for namespace pollution I'd say having one name new reserved is better than having tons of new<whatever> procs floating around.

@Araq
Copy link
Member

Araq commented Apr 3, 2018

Note that this trivial example is super dumb, but it's not impossible to imagine this being useful for lot's of other meta-programming cases, or just generic procedures as shown above.

Well I asked for real use cases, not for toy examples.

@zah
Copy link
Member

zah commented Apr 3, 2018

Personally, I think it would be nicer if we introduce a way to use the name of the type as a callable proc.

type Foo = object ...

# some magic goes here (perhaps defining an `=init` proc)

# construct instances as in Python
var f = Foo(10, 20)

Besides this, we can benefit from a "placement new" operator that places the return value of a call into a raw slot of memory. Then we can have custom alloc helpers that work like this:

template myAlloc(x: typed) = 
  type T = type(x)
  var mem = myAllocImpl(sizeof(T))
  mem <-- x # this the special "placement new" operator
            # but one could argue that the good old `mem[] = x` syntax
            # could work here without problems

# the usage looks like this
var f = myAlloc Foo(10, 20)

@yglukhov
Copy link
Member

yglukhov commented Apr 3, 2018

@Araq, here's a real-life use case:
implementation: https://github.com/yglukhov/nimx/blob/master/nimx/layout.nim
usage: https://github.com/yglukhov/nimx/blob/master/test/sample14_layout.nim#L85-L118

myWindow.makeLayout:
  - SplitView:
    ...
    - TableView:
      ...
    - View:
      ...

The makeLayout generates code that has SplitView.new(...), TableView.new(...), View.new(...), etc.

@PMunch
Copy link

PMunch commented Apr 3, 2018

@Araq, the same thing that @yglukhovs example does is what is done in the various genui macros. If this syntax was adopted those macros would be a lot easier to create.

@dom96
Copy link
Contributor

dom96 commented Apr 3, 2018

The makeLayout generates code that has SplitView.new(...), TableView.new(...), View.new(...), etc.

You can easily generate newSplitView, newTableView, newView.

I also don't see a point in this, other than "it looks nicer". The convention for initFoo and newFoo has been with us for a while now, and I don't see a reason to change it.

Edit: Okay, I can see it composing better for generics. That is indeed an advantage. Not sure it's enough to warrant this sweeping change though.

@PMunch
Copy link

PMunch commented Apr 3, 2018

Yeah it's not particularly hard, it's just unnecessary. If you look at my above example you would be able to do things that was previously only achievable with a macro using only a simple template, or even a proc. This to me is enough of a benefit in it's own right.

As for if it's worth it for all the breakage it will cause I'd say so. None of the old code is going to break, it's not like "new" or "init" is going to stop working. So things can be updated at a leisurely pace. Whenever we get nim-format it could of course complain if you tried to name your proc initThis or newThat so to let people know not to use it anymore (as if deprecation warnings from the stdlib wasn't enough).

Nim is inching ever closer to that 1.0 release, and it should try to look as best as it can when it get's there. This solution definitely feels more thought through and "clean" as opposed to the old solution which always felt like a bit of a hack.

@yglukhov
Copy link
Member

yglukhov commented Apr 3, 2018

@dom96, there's also a problem with newFoo if you think of OOP.

type
  Foo = ref object of RootRef
  Bar = ref object of Foo

proc newFoo(): Foo =
  result.new()
  # ... initialize result

proc newBar(): Bar =
  result.new()
  # ... err, now how do I initialize Foo?

Surely, this is just a cosmetics question, and can be worked around. In my case I've just introduced new(v: typedesc[Foo]). I'm just saying that this newFoo convention is not as powerful/flexible as it could be.

@dom96
Copy link
Contributor

dom96 commented Apr 3, 2018

@yglukhov sorry, but I don't understand your example. Can you elaborate?

@PMunch Even though it might not break anything, it will make a lot of code obsolete. Changing something like this, so close to v1.0 will mean that a lot of code out there will start showing deprecation warnings. Including most of the code from my book.

@PMunch
Copy link

PMunch commented Apr 3, 2018

What's worse though, being stuck with a bad design forever, or having a period of deprecation warnings close to 1.0? It's a shame about the book, but if we create a comprehensive style/syntax guide for the release of 1.0 I'd say it's fine, there is a disclaimer in the book about Nim being pre 1.0 isn't it? Besides, look at breaking changes between Python 2 and Python 3, breaking is normal for crossing version boundaries. And better to have them break before 1.0 than after.. And again, this doesn't even a real break.

@yglukhov
Copy link
Member

yglukhov commented Apr 3, 2018

@dom96, my example is about constructors in terms of OOP. If we treat newFoo as a constructor, there is no possibility to call it from subclass constructor. Does that make sense?

@dom96
Copy link
Contributor

dom96 commented Apr 3, 2018

@yglukhov I understand now, it's not possible to do newFoo and then convert that to Bar I suppose. Makes sense.

@Bulat-Ziganshin
Copy link

Bulat-Ziganshin commented Jul 15, 2018

Foo(args) is already used for explicit conversion

in C++, object construction from a single argument and explicit conversion are just the same things, why it's the problem?

Foo(args) in languages with GC usually just does it - constructs object of type Foo using constructor with args. IMHO it's very important to keep this simple syntax for this task, rather than require to attach "new" or so on any side of it. This makes an entire program looking like we just dealing with values, without going into intrinsics where and what is constructed on the heap or stack:

type Tree[T] = ref tuple
    left: Tree[T]
    x: T
    right: Tree[T]

let t = Tree( nil, 1, Tree( nil, 2, nil))

@narimiran narimiran transferred this issue from nim-lang/Nim Jan 2, 2019
@bluenote10
Copy link

Should we close this RFC in favor of #48 ? As far as I can see after zah's edit they have become very similar. Differences on first glance:

  • different names: New(Foo, ...) vs init(Foo, ...)
  • other RFC proposes additional template to assign var T based on init.
  • other RFC contains discussion why zah has revised the original proposal.

@narimiran
Copy link
Member

Should we close this RFC in favor of #48 ?

We should.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants