-
Notifications
You must be signed in to change notification settings - Fork 23
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
Object init shorthand #418
Comments
Rust also has this shorthand syntax and I'm not aware of any problems with it. |
Here's my counter-proposal, which I used in nim-lang/fusion#32 + other places (but the proposal below improves upon it to allow more flexibility) # in sugar.nim:
import std/macros
macro objInit*(obj: typed, a: varargs[untyped]): untyped =
##[
Generates an object constructor call from a list of fields.
]##
result = nnkObjConstr.newTree(obj)
for ai in a:
case ai.kind
of nnkExprEqExpr:
result.add nnkExprColonExpr.newTree(ai[0], ai[1])
of nnkIdent:
result.add nnkExprColonExpr.newTree(ai, ai)
else:
doAssert false, $ai.kind usagetype
User = object
name: string
age: int
last_online: float
proc init(T: typedesc[User], name: string, age: int, bar: int): T =
T.objInit(name, age, last_online = bar.float * 10.0)
doAssert User.init("ab", 3, 4) == User(name: "ab", age: 3, last_online: 40.0) benefits
proc baz(a: T) =
let b = T.init(...)
|
This can easily be done using macro, so I don't think there is a need for dedicated compiler support. Also, What nim really lacks is a standardized way to construct objects, and I don't think there is a need to add yet another one. We already have quite a few, all with their own limitations
If Also, how does this proposal intend to deal with inheritance, |
A couple of general remarks:
|
By in-place construction, do you mean aggregate initialization? struct S {
int x;
struct Foo {
int i;
int j;
int a[3];
} b;
};
int main()
{
S s1 = { 1, { 2, 3, {4, 5, 6} } }; |
No, mean "placement new": T* tptr = new(buf) T; // Construct a `T` object, placing it directly into your
// pre-allocated storage at memory address `buf`. It's useful for optimizing |
Just want to put this out here: https://docs.python.org/3/library/dataclasses.html It strikes me that a similar module would solve the "common initialization logic" problem, as well as some other common requests. |
I am not sure this belongs in the stdlib. |
I can see a "dataclasses"-like module being more effective as part of the standard library, because it would (likely) set a de facto standard. A problem with encapsulating interoperability mechanisms in an external module is its effectiveness relies on how ubiquitously it is used. If the community doesn't gather around a single module, then this has the potential to happen. (Please note the use of "likely", "potential", etc. in the paragraph above. The above is a possible outcome, but not a certainty) |
I'm surprised noone mentionned https://github.com/beef331/constructor#construct Creating new object sure could use some quality of life stuff; the question is whether or not syntactic sugar has its place in the stdlib. |
Just once, it'd be nice to see a thing added to stdlib because it's /annoyingly/ absent and /everyone/ has to depend upon it as a requirement which /rarely/ changes and works with /all/ supported compiler versions. Unlike, for example, this case, where neither the syntax nor the implementation will likely satisfy everyone, so it just becomes an immediate burden. But by all means, bike shed away. What if the burden was merely in adding it as a requirement to your package manager? Unthinkable? Really? |
The present object constructor syntax(aside from not inferring generics) is fine, if you something more sweet you can easily write your own or use a package(as disruptek alluded to). As such here is yet another way to construct objects in a DRY manner cause I apparently love volume. import constructor/constructor
type
User = object
name: string
age: int
lastOnline: float
proc initUser*(name: string, age: int): User {.constr.} = discard
proc init(T: typedesc[User], name: string, age: int) {.constr.} =
let lastOnline = 30f
assert initUser("hello", 10) == User(name: "hello", lastOnline: 0f, age: 10)
assert User.init("hello", 30) == User(name: "hello", lastOnline: 30f, age: 30) |
I also wrote a macro that create a object type definition from a constructor proc: But I'm not using it so much. Standard Nim way to define an object type and a constructor proc is fine. |
Seeing as @ringabout is already working on an implementation, I'd like to consider the potential issues with a simple positional syntax proposed. Here's the example @ringabout left on the forum: type
Vector = object
a: int = 1
b, c: int
block: # positional construction
var x = Vector(1, 2, 3)
echo x This example is bad at demonstrating the usefulness of shorthand constructs - why not use a tuple? - and good ad demonstrating its cons. If it's really an object with all fields of the same type, as soon as anyone touches the declaration and changes the order of fields, the code will break everywhere the object's used with no warnings. What you get is a weird tuple. If the fields are of mixed types, there's still a chance (albeit much smaller one.) the code will compile and break. The language should resist accommodating more syntax constructs with such qualities. Relying on order for tuples and procs is enough of a footgun. Named fields are an annoyance, but they are a correctness guarantee. This RFC proposes allowing for a short-version construction only if the field name matches the variable name. And this looks reasonable enough. #21559 doesn't implement this RFC and should be discussed separately, possibly with a voting/comparison with different approaches:
Personally, I think this is more an issue of tooling. Proper autocompletion and tooltips would reduce the crux of the problem to just a few tab presses for each construction. |
The bare minimum you could do is require Also both have a similar problem: type
Foo = object
x: int
converter toFoo(a: int): Foo =
Foo(x: -a)
echo Foo(3).x # 3 or -3?
import algorithm
type
Bar = object
x: string
converter toBar(b: string): Bar =
Bar(x: b.reversed)
let x = "abc"
echo Bar(x).x # abc or cba? Personally I have no idea what the big deal is with just typing the field name out. Maybe if you have giant types and you need to keep typing |
If you really need positional initializers, I have a painless solution. Just define a macro which generates the converter from an anonymous tuple, maybe with just a pragma. This way all you need to do is just add another pair of parentheses or just a single dot, but the resulting bit of syntax is visually distinct and requires zero changes to the language. type
Adj = enum
Mimsy, Galumphing, Slithy
Bandersnatch = object
name: string
age: int
kind: Adj
# This converter could be generated with a macro
converter toBandersnatch(x: (string, int, Adj)): Bandersnatch =
Bandersnatch(name: x[0], age: x[1], kind: x[2])
# Regular object init
let boojum = Bandersnatch(name: "Boojum", age: 147, kind: Slithy)
# Two ways to init an object positionally with a converter
let snark = ("Snark", 147, Galumphing).Bandersnatch
let jabberwocky = Bandersnatch(("Jabberwocky", 147, Galumphing)) Just add this to sugar and don't multiply rules with edge cases (like a single field in this case). |
The converter solution is way uglier and has unintented consequences like: let unrelatedTuple = ("Snark", 147, Galumphing)
proc takesBandersnatch(unrelatedTuple) # compiles
I've read all your concerns but I am not convinced positional values in object constructors will cause any problems in practice. The feature exists in Rust/C/C++ without many known downsides. A macro cannot accomplish the same as easily as it would need to do type introspection the syntax would look like |
What's wrong with it and why is it unintended? It does exactly the thing positional inits propose to solve: lets the initializer take a bunch of stuff and reason about it solely based on type matching. What is passing a bunch of arguments to a function/initializer/converter if not passing it an unnamed tuple? Moreover, positional inits muddy up the obviousness of the syntax. Why is
Well, Rust doesn't feature positional identifiers (see below) exactly because it's considered C's mistake worth fixing. Rust only has equal-name inits, like this RFC proposes. Even that shorthand was met with some well-argued resistance, even though the RFC is much more considerate and convincing. struct Foo {a: u8, b: u32, c: bool}
fn main() {
let a = 42u8;
let b = 90210u32;
let c = true;
let foo = Foo {a: 0, b: 1, c: false};
let bar = Foo {a, b, c};
// let xyz = Foo {a, b, false}; // Error!
// let baz = Foo {0, 1, false}; // Error!
} BTW, there's already a conceptual omission with type conversions and object initializers. From the syntax you expect it to be just a way to destructure tuples into type's fields, but it's really not, it's some special kind of syntax (why?). type Bandersnatch = object
s, t: string
# ↓type ↓tuple
let a = string("a") # Function `string` takes a tuple with anything "equivalent" to a string, returns a typed value
# ↓type ↓tuple
let b = string ("b") # type ← tuple ⇒ Ok, same "single field" destructuring as above
# ↓type ↓tuple
let c = Bandersnatch(t: "a", s: "b") # Naming fields allows not respecting the order for destructuring
# ↓type ↓tuple
let d = Bandersnatch (s: "a", t: "b") # Type mismatch error. Why? Because it's not really destructuring
# ↓type ↓tuple
let e = tuple[string, string]("a", "b") # Error. Yep, our intuition is wrong At the very least a serious reason should be given why this feature needs to be a part of the language if so many options are already available (bunch of third-party macros and the tuple conversion approach). Looks like it's just much easier to implement than, say, pattern-matching, so it's given a go, even though it multiplies ambiguity, potentially bug-prone and adds corner-cases like a single field init. |
type
Foo = object
x: int
converter toFoo(a: int): Foo =
Foo(x: -a)
echo Foo(3).x # 3 or -3?
import algorithm
type
Bar = object
x: string
converter toBar(b: string): Bar =
Bar(x: b.reversed)
let x = "abc"
echo Bar(x).x # abc or cba? Nope, the object with only one field should always use named field values. So the answer is -3 and cba. |
Here is my alternative RFC => #517 |
Functions with postional parameters have the same disadvantages, but are still used commonly. Probably it just means the project, which is broken, doesn't have enough tests. Though, on the other hand this RFC should be easier to implement. |
Consider the following object type:
It's very common to write a constructor for such types, some may have many more fields than the above, having to write the name of each field gets tedious:
This RFC proposes to implement a short-hand syntax for the object construction syntax to simplify this code:
The variable names have to match that of the field name, so
User(foo, age)
won't work even iffoo
is a string.The text was updated successfully, but these errors were encountered: