I’m looking for a way to execute code when creating an instance. For example from a module, this is to avoid having to call a method (init_my_module) in each initialize including this module. In the principle of #finalize (or GC.add_finalizer) but for the initialization.
Is there a way (std or not) to do this in Crystal?
initialize is a regular method for the most part, so normal inheritance rules apply for the most part: Carcin But this might a little less magic than you’re looking for…
I could imagine some more magic is achievable with macro included overriding initialize to call super and the original with previous_def, but not sure that’s worth the complexity.
The difficulty in overriding initialize is when there are several with different signatures Carcin. Of course there is a way to put a state in init_my_module to avoid initializing several times when an initialize call another initialize. Also, by using the macro finished into the macro included and overriding each initialize (+ take care of the hierarchy (child < parent). It goes into the too complex.
If there is only one input source (a common method or a hook like finalize), it would be more convenient and easier to debug.
Via GC you think it’s possible? Like a GC.add_initializer or other.
This is a module to handle configuration (from env, files, default values hard coded) and data validations. I need to do some processing on the values of instance variables.
Here is a simplified example:
struct Config
include Configurator
entry url : String = "http://localhost:3000", {required: true, url: "Should be a valid URL"}
# entry db_uri....
end
The Configurator module handle ENV hash and data validation. Example, if I provide a bad url to url entry, I get the error Should be a valid URL.
Currently, I have to add an initializer into each initialize methods.
struct Config
include Configurator
entry url : String = "http://localhost:3000", {required: true, url: "Should be a valid URL"}
# entry db_uri....
def initialize
# I want to avoid this constraint by detecting the instance creation.
# Or another way without having to do anything other than include the module.
init
end
end
Ideally, it would be convenient to include the module that works without setting a DSL. My example is for the config but this is the case for every class where I have to do some validation and some kind of ioc.
If this is the path you want to go down, couldn’t you just do like:
def self.load
instance = new
instance.init
instance
end
Have this method be added when the module is included then use it as the entrypoint versus the standard .new. Or could even make it def self.new if you want this to be the default behavior.
@asterite: You could always initialize those lazily.
EDIT: Nevermind, maybe not.
What do you mean?
@Blacksmoke16 Thanks. Indead, it’s a solution. However, that prevents from having a normal initialization (with new). If I define a new, like initialize it would be necessary to define several if there are several signatures.
def self.new
instance = allocate
instance.initialize if instance.responds_to?(:initialize)
instance.init
GC.add_finalizer(instance) if instance.responds_to?(:finalize)
instance
end
def self.new(*args)
instance = allocate
instance.initialize(*args) if instance.responds_to?(:initialize)
instance.init
GC.add_finalizer(instance) if instance.responds_to?(:finalize)
instance
end
I hope I didn’t forget anything in the `new’ so as not to compromise the normal operation?
That’s a dangerous approach. You really shouldn’t hack into the allocation logic. It’s completely unnecessary and can easily break.
As @jhass already mentioned in the first comment, simply redefining the constructor method and delegating to previous_def should work very well for your use case.
module InitializeHook
def initialize_hook
puts "#{self} initialized"
end
macro included
macro finished
\{% for method in @type.methods %}
\{% if method.name == "initialize" %}
def initialize(\{{ method.args.join(",").id }})
previous_def
initialize_hook
end
\{% end %}
\{% end %}
end
end
end
Personally I’m very against this pattern. While it makes for a nice DSL, the rest of the implementation is a bear. Is it not possible to decouple the instantiation of the type with the validation of the values? For example:
struct Config
include Configurator
entry url : String = "http://localhost:3000", {required: true, url: "Should be a valid URL"}
# entry db_uri....
def initialize(@url : String)
# Call the method added via the module
self.validate
end
end
Having the validation be separate from setting the state of the type would be much more flexible, as it would inherently support multiple constructors. E.g. from YAML or JSON, etc. It also makes for much simpler code as you’re able to better leverage existing language semantics, versus essentially reimplementing the initializer logic.
You’d also be able to do away with required as the compiler would know/enforce that it cannot be nil. Or if you still want it could use the nilability of the entry’s type to know if it is or not. Validator - Athena Framework Might be helpful as well if you want to go down this route.
@straight-shoota ah thank you, I didn’t understand it like this. It’s ok!
@Blacksmoke16: Is it not possible to decouple the instantiation of the type with the validation of the values?
Yes, of course. It’s not just for the validation. My goal is to call a function even with several constructors, without having to add it to each one.
@Blacksmoke16: You’d also be able to do away with required as the compiler would know/enforce that it cannot be nil. Or if you still want it could use the nilability of the entry’s type to know if it is or not.
It’s just an example. A validation rule to check from a Hash and generate the error message. Not related to the Crystal type.
But my question is not related to the validation and I know you validator because you posted it to me, forum and chat very frequently, but thanks again.