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

Support default values for object properties #126

Closed
ghost opened this issue Aug 25, 2015 · 21 comments · Fixed by nim-lang/Nim#20480
Closed

Support default values for object properties #126

ghost opened this issue Aug 25, 2015 · 21 comments · Fixed by nim-lang/Nim#20480

Comments

@ghost
Copy link

ghost commented Aug 25, 2015

Example:

type MyObj = object
    id: int = generateUniqueId()
    x: int = 42
    stuff: seq[string] = @[]

I can think at least the following benefits:

  • Faster prototyping (less typing when no need to write newMyObj or initMyObj procs)
  • Makes it harder to misuse objects (for example when using some library for the first time)
  • Easier generics (with a stupid example):
proc doSomethingGeneric[T](): T =
    assert result.x == 42
    assert result.stuff != nil

discard doSomethingGeneric[MyObj]()
@FedericoCeratto
Copy link
Member

FedericoCeratto commented Aug 25, 2015

Relevant: http://forum.nim-lang.org/t/703

EDIT: URL updated

@ConsoleTVs
Copy link

This still need to be a feature 3 years later

@CodeDoes
Copy link

CodeDoes commented Jul 1, 2018

@stefanos82
Copy link

@dom96 The reason @FedericoCeratto's link does not work is because it ends with a forward slash.

If I'm not mistaken, you mentioned this issue in IRC, correct?

@gitnik
Copy link

gitnik commented Nov 14, 2018

I'm having a hard time understand how something as fundamental as this is considered low priority

@krux02
Copy link
Contributor

krux02 commented Nov 15, 2018

It is by design that Nim does not need constructors. Objects are by default initialized with 0 memory and it is the programmers job to create the type in a way so that the 0 memory isn't an illegal state of the object. Since seq and string are now by default empty and not nil anymore, the biggest reason to have inititializers/constructors is now gone. Currently I don't see any good reason to add destructors that justifies the costs of having destructors.

Nim isn't the only language that works this way, for example Go works exactly the same way.

@timotheecour
Copy link
Member

Currently I don't see any good reason to add destructors that justifies the costs of having destructors.

what do you mean by costs? if you're talking about runtime cost overhead, I don't think this is the case; it would be a zero-cost abstraction when no custom constructor is defined for a type
if you mean implementation complexity, that's a different story but it'd be worth it IMO to simplify a number of patterns ala C++ RAII

@krux02
Copy link
Contributor

krux02 commented Nov 15, 2018

I am not talking about runtime costs. I am talking about costs to implement it, fixing the bugs and conflics that arise from it. Live with the fact that constructors are a thing and value types are no longer all zero anymore. Did you ever watch a lesson about generic exception safe initialization in c++? It's nuts and not worth it. memset to 0 can't raise an exception.

@timotheecour
Copy link
Member

timotheecour commented Nov 15, 2018

the alternative is to make initialization explicit, eg:
https://github.com/nim-lang/Nim/issues/8855
or
https://github.com/nim-lang/Nim/issues/7832

IIUC that would alleviate much of the complexity issues you're referring to.

@dom96
Copy link
Contributor

dom96 commented Nov 15, 2018

I'm having a hard time understand how something as fundamental as this is considered low priority

This isn't fundamental. There is plenty of languages that don't allow this. Plus this is yet another feature that is easily doable within the confines of existing language features: just use a constructor procedure.

@timotheecour
Copy link
Member

timotheecour commented Apr 17, 2020

this is fundamental.

@dom96
I think it is, for several reasons (beyond eliminating much boilerplate) including not to break the type system guarantees, see below.

I am not talking about runtime costs. I am talking about costs to implement it, fixing the bugs and conflics that arise from it

@krux02
you're simply shifting around the complexity to the user, it's never a good tradeoff.

Did you ever watch a lesson about generic exception safe initialization in c++? It's nuts and not worth it. memset to 0 can't raise an exception.

not relevant. We're talking (or at least in my version of this RFC) about default values known at CT, whereas C++ has to deal with RT initialization. Implementation is not complex (in fact we already have nim-lang/Nim#12378).
Implications are that for types that are not binary-zero-initialized, compiler will use memcpy instead of memset, and variable initializers in cgen binary could end up in .data or .rodata section instead of .bss, but that's to be expected.

without this feature, the type system is broken

There's another important use case not mentioned here: type safety guarantees.
Some Nim type system guarantees evaporate because of this, eg range objects, enum starting at some offset, or any type that (recursively) contains any such type. eg:

block:
  type R = range[10..13]
  doAssert R.default == 0 # broken
  var a: R
  doAssert a == 0  # broken

  type Foo = enum k1 = 10, k2 # not even enum with holes
  var b: Foo
  doAssert b.ord == 0  # broken
  doAssert $b == "0 (invalid data!)"  # broken

  ## affects any type that has a (sub-) member with non-valid-default
  type Bar = object
    a1: Foo
    a2: R

  # var c: Bar # this doesn't even compile
  var c = default(Bar) # that does compile, but still broken
  doAssert $c == "(a1: 0 (invalid data!), a2: 0)"

note that some of these generate a warning (UnsafeDefault) but preventing these from working is not an option (breaks generic code including serialization/deserialization, among other use case)

real life example

complications arising from DateTime, which contains both range[1..31] and enum with offset (see #211)

@Araq
Copy link
Member

Araq commented Apr 19, 2020

without this feature, the type system is broken

Well yeah, but there are two conflicting designs here: "Just make the default value sane" vs "Just make initialization explicit" and only the latter can fix our "not nil" issues.

@timotheecour
Copy link
Member

timotheecour commented Apr 19, 2020

Just make initialization explicit

this opens a can of worm similar to C++ static initialization order fiasco, eg http://www.cs.technion.ac.il/users/yechiel/c++-faq/static-init-order.html

it also doesn't play well with compile time code and generic code

  • not nil is a separate issue (because the not nil value is runtime initialized instead of known at CT) and we can at least make the type system not broken for types with a well known valid CT default value.

That being said, I think we can also make it work for not nil types.

Here's my proposal

  • var a: T is always equivalent to var a = default(T)
  • default(T) is defined recursively in the obvious way, taking into account default intializers for object types, eg
type
  Normal = ref object
  TBar = object
    b1 = "abc"
    b2: range[10..12]
  Bar = ref TBar not nil
  Foo = object
    x1: Bar
    x2: Bar
    x3: seq[Bar]
    x4: Normal

static:
  assert default(Normal) == nil
  const b0 = default(Bar)
  assert b0 != nil
  assert b0[] == TBar(b1: "abc", b2: 10)
  assert default(Foo) == Foo(x1: b0, x2: b0, x3: @[], x4: nil)

var a: Foo
let b = default(Bar)
assert b != nil

# b is a (singleton-like) instance intialized in module constructors
# no C++ static initialization order fiasco in our case since the default values
# are known at CT and there are no cycles

assert b[] == TBar(b1: "abc", b2: 10)
assert a == Foo(x1: b, x2: b, x3: @[], x4: nil)


## note: b0 and b's addressed are unrelated, eg:
const b0Adrr = cast[int](b0)
assert b0Adrr ==  cast[int](b) # would fail

this is all possible by re-opening nim-lang/Nim#13082

The only thing needed in that PR is either disallowing var y = x (if x is a CT ref type with x != nil), or mapping it to a statically (in the C++ sense) initialized variable initialized as above during module constructors, so that var y = x; var y2 = x; assert y2 == y.

const b0 = default(Bar)
#var a = b0 # either CT error or map it to a statically initialized variable
var b2 = b0.b2  # ok
assert b2 == 10
assert b0.b1 == "abc"

@Araq
Copy link
Member

Araq commented Apr 19, 2020

This is a totally unproven design though, complex to implement, much "magic" going on, unknown pitfalls are awaiting us. All the recent developments in other programming languages focus on "not nil" via some form of explicit constructors/construction. And it works, it's a painful transition for Nim, but worth it.

@timotheecour
Copy link
Member

timotheecour commented Apr 20, 2020

This is a totally unproven design though

this is pretty close to how it works in D; I encourage you to read this short article for more details: https://p0nce.github.io/d-idioms/#Precomputed-tables-at-compile-time-through-CTFE ; it explains the difference between D's enum vs static const

  • enum a = expr;: enum creates a compile-time only construct; it has no address
    pretty close to nim's const a = expr
  • static const a = new Foo(): this computes at CT as well but puts in the static data segment, and has an address you can access at runtime.
    in nim, that'd be the analog, for example, of let a = "foo".cstring, which puts "foo" in the data segment as readonly memory

try it out: here's more or less my example i gave in proposal above ported to D

rdmd -of/tmp/z01 -L-no_pie t10584b.d
/tmp/z01 # you'll see same address if re-running thanks to -L-no_pie to disable ASLR

import std.stdio;

class A {
  int ma1 = 10;
  int[] ma2 = [11,12];
  string ma3 = "abc";
  this(int ma1){this.ma1=ma1;}
}

class B {
  A a1 = new A(11); // A could be a "not nil" ref object type in nim's case
  A a2;
  bool isCT;
  this(){
    this.a2 = this.a1;
    this.isCT = __ctfe;
  }
}

void main(string[]args){
  auto bRT = new B(); // alloc'd at RT
  static const bCT = new B(); // alloc'd at CT
  assert(bCT.isCT); // checks it was alloc'd at CT
  assert(bCT.a1 == bCT.a2); // checks this is using reference semantics
  enum ma3 = bCT.a2.ma3; // checks we can access in CTFE
  assert(ma3 == "abc");

  writeln(&bRT); // 7FFEEFBFB188 => heap
  writeln(&bCT); // 10006F478 => static data segment (at a fixed address if compiled with `-L-Wl,-no_pie` to disable ASLR)
  enum bCT2 = &bCT; // checks that address is available at CT
}

note

this would make not nil not only a CT guarantee, but it would also make it efficient, since the same ("allocated" in data segment) is reused, effectively acting as a singleton known at CT with an address available at RT.

it would also provide RT guarantees to prevent writes to that object, eg could cause a SIGBUG error if attempting to write to ROM.
Ideally this would be a CT error but nim doesn't (yet?) have a notion of immutable. For example:

var a = "foo".cstring
a[0] = 'x'

nim gives no warning / CT error but at runtime crashes with: SIGBUS: Illegal storage access because a[0] is in ROM.

wheres in D you have to explicitly circumvent type system to get that SIGBUS:
*cast(char*)(&bCT.a1.ma3[0]) = 'x'; // bus error

This is yet another use case for introducing immutable type qualifier, which would have lots of safety and performance benefits while also making analysis easier or even possible (at the price of having to implement that feature).

In particular for multithreading, where immutable values could be safely shared between threads.

@Araq
Copy link
Member

Araq commented Apr 20, 2020

Yeah well, not every single bug is the language's domain and deterministic crashes are pretty tame to begin with. And enough people gave up on D's way of doing immutability (while still using D).

@timotheecour
Copy link
Member

timotheecour commented Apr 27, 2020

@Araq

[...] focus on "not nil" via some form of explicit constructors/construction

I thought about this more in the context of NRVO (eg nim-lang/Nim#14126 and this old forum question does using result guarantee NRVO? - Nim forum )

IMO forcing explicit constructors would not only complicate user code, but would also prevent NRVO in many cases and would likely be more expensive than simply inserting nil checks (combining static analysis + runtime checks when static analysis can't tell).

  • in C++, NRVO is not always guaranteed in particular in presence of non-default constructors(+friends) and exceptions.
  • in nim, the fact that objects are binary-zero initialized (or initialized to a fixed CT value, it's irrelevant), makes NRVO a lot easier to implement.

Note that current nim implementation doesn't always do NVRVO (eg if an object contains an array, see timotheecour/Nim#137 which seems like a bug), but could.

And then there's this classic example: https://github.com/nim-lang/Nim/pull/13808/files/4f6cdd797cc0db0410644e7f1216e5264968deeb#diff-f48932f809aa3e1ed2576a4fdd754e26 (seq initialization with not-nil elements) which I really don't see how you can overcome with explicit constructors/construction without a serious performance cost because of things like nim-lang/Nim#13448.

My suggestion for the short term is this:

  • allow CT default initializers for non-pointer-like type kinds, eg where const a = fun() is legal currently (objects, floats, ordinals, ranges etc and its transitive closure)
    this will solve already many practical problems including the old DateTime one.
  • then in a 2nd phase, consider extending that to other type kinds

@alapini
Copy link

alapini commented May 22, 2020

Optional range
For what it's worth, I get stuck in the following scenario :

import options
type
  Foo = object
    bar: range[1001..2000]

proc myproc(a: int, b: Option[Foo] = none(Foo)) =
  discard

# or just
proc myproc(a: Option[Positive] = none(Positive)) =
  discard

The compiler warns me : Cannot prove that 'result' is initialized. But I have no way to provide a default initializer or at least default attribute values for Foo

Edit : No more warning since I upgraded to Nim 1.2.0 . I was using 1.0.6

@timotheecour
Copy link
Member

timotheecour commented Oct 13, 2020

should we close as duplicate of #252 which was marked as accepted in the form written here: #252 (comment) ? (although #126 (comment) has more details)

@al6x
Copy link

al6x commented Mar 9, 2023

Is there some switch to enable defaults in 1.6.10? I tried defaults but it failed

@Araq
Copy link
Member

Araq commented Mar 9, 2023

It's only for 2.0. Sorry, backports are for bugfixes.

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