New / Initialize hook?

hello,

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.

1 Like

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.

What do you need to initialize exactly?

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.

What does init do?

some processing on the values of instance variables

Like:

def init
   {% for var, entry in ENTRIES %}
      @{{var}} = check_{{var}}(@{{var}})
       # etc... on the instance variables
   {% end %}
end

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.

1 Like

You could always initialize those lazily.

EDIT: Nevermind, maybe not.

@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.

ok, this seems to work

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

https://carc.in/#/r/ax6z

3 Likes

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.

1 Like

@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.

Currently I use the validation like it:

validation = User.check(json_h)
return validation.errors unless validation.valid?

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. :relieved: