Primary and secondary constructors

Recently, I discovered Yegor Bugayenko’s books (Elegant objects vol 1 & 2) and his blog about object-oriented programming.
His statements are often described as controversial, but at the same time, they are thought-provoking!

One of his tips poses the problem of class constructors and he distinguishes between the primary constructor and the secondary constructors, the latter always ending up calling the primary constructor (which could for example centralize the validity check of instantiation parameters).

Here is an example in a first version, where initialize calls initialize, and reading the Crystal documentation, I’m afraid it’s not very satisfactory, even if it works.

class Test
  def initialize(s : String) # Secondary constructor
    initialize(s.to_i)
  end
  def initialize(f : Float64) # Secondary constructor
    initialize(f.to_i)
  end
  def initialize(@value : Int32) # primary constructor, at end of list
    raise "parameter error" if @value > 33
  end
end
 
x = Test.new("33")
p! x
x = Test.new(3.14)
p! x
x = Test.new(42)
p! x

and here is a modified example, in which the initialize method is called only once.

class Test
  def initialize(s : String) # Secondary constructor
	primary_initialize(s.to_i)
  end
  def initialize(f : Float64) # Secondary constructor
	primary_initialize(f.to_i)
  end
  def initialize(i : Int32) # secondary constructor
    primary_initialize(i)
  end
  def primary_initialize(i : Int32) # primary constructor
    raise "parameter error" if i > 33
    @value = i
  end
end

x = Test.new("33")
p! x
x = Test.new(3.14)
p! x
x = Test.new(42)
p! x

Would there be another more efficient and/or idiomatic version to comply with this advice?
What do you think about it?

In Crystal this is handled via the differentiation between def initialize and def self.new. Usually the rule of thumb is use self.new overloads to transform the arguments to fit a smaller amount of initialize methods. E.g. your example could be written as:

class Test
  def self.new(f_or_s : Float64 | String) : self
    new f_or_s.to_i
  end
 
  def initialize(@value : Int32)
    raise ArgumentError.new "parameter error" if @value > 50
  end
end

Also checkout Constructor variants and inheritance, as it has some more detailed information on how the two relate, especially when inheritance is introduced.

1 Like

In my examples (especially the first one), the question was whether there were several memory allocations for one object.
I guess in your example this is not the problem anymore.
Elegant syntax anyway, I didn’t know the use of self.new.
Thanks

2 Likes

Each object is only allocated once, no matter how often you call methods named #initialize on it. In fact, #initialize is really just a normal method. There are some special rules in the compiler to validate it assigns all instance variable. But it has no extra sauce at run time. You can call #initialize on an already initialized object (its visibility is protected, so that only works from the class or instance scope).

Regarding primary constructors, I agree that it’s a good idea. In Crystal (and Ruby) you’re encouraged to differentiate #initialize and .new which fit very well with the description of primary and secondary constructors.

There are however some limitations with inheritance which require to use #initialize as a secondary constructor (an example is stdlib’s Socket class tree).