2

This is really hard to do a google search about because I have no idea if it's a Ruby thing or a Rails thing and google does not do a good job searching for "on"

In a file that looks like so

# app/models/concerns/searchable.rb
module Searchable
  extend ActiveSupport::Concern

  included do
    include Elasticsearch::Model
    include Elasticsearch::Model::Callbacks

    # Every time our entry is created, updated, or deleted, we update the index accordingly.
    after_commit on: %i[create update] do
      __elasticsearch__.index_document
    end

    after_commit on: %i[destroy] do
      __elasticsearch__.delete_document
    end

    # We serialize our model's attributes to JSON, including only the title and category fields.
    def as_indexed_json(_options = {})
      as_json(only: %i[title category])
    end

    # Here we define the index configuration
    settings settings_attributes do
      # We apply mappings to the title and category fields.
      mappings dynamic: false do
        # for the title we use our own autocomplete analyzer that we defined below in the settings_attributes method.
        indexes :title, type: :text, analyzer: :autocomplete
        # the category must be of the keyword type since we're only going to use it to filter articles.
        indexes :category, type: :keyword
      end
    end

    def self.search(query, filters)
      # lambda function adds conditions to the search definition.
      set_filters = lambda do |context_type, filter|
        @search_definition[:query][:bool][context_type] |= [filter]
      end

      @search_definition = {
        # we indicate that there should be no more than 5 documents to return.
        size: 5,
        # we define an empty query with the ability to dynamically change the definition
        # Query DSL https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html
        query: {
          bool: {
            must: [],
            should: [],
            filter: []
          }
        }
      }

      # match all documents if the query is blank.
      if query.blank?
        set_filters.call(:must, match_all: {})
      else
        set_filters.call(
          :must,
          match: {
            title: {
              query: query,
              # fuzziness means you can make one typo and still match your document.
              fuzziness: 1
            }
          }
        )
      end

      # the system will return only those documents that pass this filter
      set_filters.call(:filter, term: { category: filters[:category] }) if filters[:category].present?

      # and finally we pass the search query to Elasticsearch.
      __elasticsearch__.search(@search_definition)
    end
  end

  class_methods do
    def settings_attributes
      {
        index: {
          analysis: {
            analyzer: {
              # we define a custom analyzer with the name autocomplete.
              autocomplete: {
                # type should be custom for custom analyzers.
                type: :custom,
                # we use a standard tokenizer.
                tokenizer: :standard,
                # we apply two token filters.
                # autocomplete filter is a custom filter that we defined above.
                # and lowercase is a built-in filter.
                filter: %i[lowercase autocomplete]
              }
            },
            filter: {
              # we define a custom token filter with the name autocomplete.

              # Autocomplete filter is of edge_ngram type. The edge_ngram tokenizer divides the text into smaller parts (grams).
              # For example, the word “ruby” will be split into [“ru”, “rub”, “ruby”].

              # edge_ngram is useful when we need to implement autocomplete functionality. However, the so-called "completion suggester" - is another way to integrate the necessary options.
              autocomplete: {
                type: :edge_ngram,
                min_gram: 2,
                max_gram: 25
              }
            }
          }
        }
      }
    end
  end
end

I am not sure what after_commit on: %i[create update] do is supposed to mean. I managed to find this information https://apidock.com/rails/ActiveRecord/Transactions/ClassMethods/after_commit which gives me an idea of how to use this sytax. But I'm still not sure how this syntax "on:" is created. It doesn't seem like a Ruby thing. It seems like a Rails shorthand for something but what exactly is it?

On a separate note, is there any source that lists all the shorthands that Rails provides? It's such a pain to figure out if something is a Rails shorthand or if it's a Ruby syntax.

itsmarziparzi
  • 699
  • 7
  • 21
  • 1
    _"It doesn't seem like a Ruby thing"_ – syntax-wise, `on` is just a [keyword argument](https://ruby-doc.org/3.1.2/syntax/calling_methods_rdoc.html#label-Keyword+Arguments): `after_commit(on: [:create, :update]) { ... }` – what it does is up to the method (just like with any other argument). – Stefan Dec 22 '22 at 10:12
  • 1
    "It's such a pain to figure out if something is a Rails shorthand or if it's a Ruby syntax." - thats a common problem when you don't bother to learn Ruby properly before gettting into Rails. This is basic Ruby syntax and Rails doesn't actually change how the language behaves on a basic level. – max Dec 22 '22 at 10:19
  • BTW, gems can't alter or extend the [Ruby syntax](https://ruby-doc.org/3.1.2/syntax_rdoc.html) – it's solely defined by the language. However, Ruby has a very light-weight and flexible syntax (parentheses can be omitted, arrays are created implicitly) which allows for so-called [DSLs](https://en.wikipedia.org/wiki/Domain-specific_language) that may _look like_ a custom syntax or language. (think of Rake files) – Stefan Dec 22 '22 at 10:25
  • It took me a while to find out the point of the question. I don't think ALL that code is needed to exemplify the issue. Just use a part of it that is illustrative and remove the rest. – Lomefin Dec 26 '22 at 04:31

5 Answers5

5

What does this syntax using "on:" mean in Ruby on Rails?

Since you are specifically asking about syntax, not semantics, I will answer your question about syntax.

This is really hard to do a google search about because I have no idea if it's a Ruby thing or a Rails thing

That is easy to answer: Ruby does not allow to modify the syntax, so it cannot possibly be a Rails thing. Anything related to syntax must be a "Ruby thing" since neither Rails nor any other user code can change the syntax of Ruby.

What you are asking about is just basic Ruby syntax, nothing more, and nothing to do with Rails.

I am not sure what after_commit on: %i[create update] do is supposed to mean.

What you see here, is called a message send. (In other programming languages like Java or C# and in some parts of the Ruby documentation, it might be called a method call and in programming languages like C++, it might be called a virtual member function call.) More precisely, it is a message send with an implicit receiver.

A message is always sent to a specific receiver (just like a message in the real world). The general syntax of a message send looks like this:

foo.bar(baz, quux: 23) {|garple| glorp(garple) }

Here,

  • foo is the receiver, i.e. the object that receives the message. Note that foo can of course be any arbitrary Ruby expression, e.g. in (2 + 3).to_s, the message to_s is sent to the result of evaluating the expression 2 + 3, which in turn is actually just the message + sent to the result of evaluating the expression 2, passing the result of evaluating the expression 3 as the single positional argument.
  • bar is the message selector, or simply message. It tells the receiver object what to do.
  • The parentheses after the message selector contain the argument list. Here, we have one positional argument, which is the expression baz (which could be either a local variable or another message send, more on that later), and one keyword argument which is the keyword quux with the value 23. (Again, the value can be any arbitrary Ruby expression.) Note: it is actually not necessarily true that this is a keyword argument. It could also be a Hash. More on that later.
  • After the argument list comes the literal block argument. Every message send in Ruby can have a literal block argument … it is up to the method that gets invoked to ignore it, use it, or do whatever it wants with it.
  • A block is a lightweight piece of executable code, and so, just like methods, it has a parameter list and a body. The parameter list is delimited by | pipe symbols – in this case, there is only one positional parameter named garple, but it can have all the same kinds of parameters methods can have, plus block-local variables. And the body, of course, can contain arbitrary Ruby expressions.

Now, the important thing here is that a lot of those elements are optional:

  • You can leave out the parentheses: foo.bar(baz, quux: 23) is the same as foo.bar baz, quux: 23, which also implies that foo.bar() is the same as foo.bar.
  • You can leave out the explicit receiver, in which case the implicit receiver is self, i.e. self.foo(bar, baz: 23) is the same as foo(bar, baz: 23), which is of course then the same as foo bar, baz: 23.
  • If you put the two together, that means that e.g. self.foo() is the same as foo, which I was alluding to earlier: if you just write foo on its own without context, you don't actually know whether it is a local variable or a message send. Only if you see either a receiver or an argument (or both), can you be sure that it is a message send, and only if you see an assignment in the same scope can you be sure that it is a variable. If you see neither of those things it could be either.
  • You can leave out the block parameter list of you're not using it, and you can leave out the block altogether as well.
  • If the last argument of the argument list (before the block, obviously, which is passed after the closing parenthesis of the argument list) is a Hash literal, you can leave off the curly braces, i.e. foo.bar(baz, { quux: 23, garple: 42 }) can also be written as foo.bar(baz, quux: 23, garple: 42) which can also be written as foo.bar baz, quux: 23, garple: 42. That's what I alluded to earlier: the syntax for passing a new-style Hash literal and the syntax for passing a keyword argument are actually the same. You have to look at the parameter list of the method definition to figure out which of the two it is, and there are some corner cases that have changed how, exactly, it is interpreted a couple of times in between Ruby 2.0 when keyword parameters and arguments were first introduced and Ruby 3.2.

So let's dissect the syntax of what you are seeing here:

after_commit on: %i[create update] do
  __elasticsearch__.index_document
end

The first layer is

after_commit … some stuff … do
  … some stuff …
end

We know that this is a message send and not a local variable, because there is a literal block argument, and variables don't take arguments, only message sends do.

So, this is sending the message after_commit to the implicit receiver self (which in a module definition body is just the module itself), and passes some arguments, including a literal block.

If we add the optional elements back in, we can see that

after_commit … some stuff … do
  … some stuff …
end

is equivalent to

self.after_commit(… some stuff …) do
  … some stuff …
end

The block has no parameter list, only a body. The content of the body is

__elasticsearch__.index_document

Again, we know that index_document is a message send because it has a receiver. Whenever you see either an argument or a receiver or both, you know that you have a message send. So, this is sending the message index_document to the receiver expression __elasticsearch__.

Now, what is __elasticsearch__? As I mentioned above, we can't actually know without context what it is: it could be either a receiver-less message send with no argument list, i.e. a message send to the implicit receiver self, roughly equivalent to self.__elasticsearch__(). Or, it could be a local variable. The way this ambiguity is resolved is by looking at the preceding context: if there has been an assignment to __elasticsearch__ parsed (not necessarily executed) before this point, it will be treated as a local variable, otherwise, as a message send.

In this particular case, there is no assignment to __elasticsearch__, therefore, it must be a message send, i.e. it is sending the message __elasticsearch__ to the implicit receiver self (which is here still the module itself, because blocks lexically capture self, although that is part of the language semantics and you asked strictly about syntax).

If we add the optional elements back in, we can see that

__elasticsearch__.index_document

is equivalent to

self.__elasticsearch__().index_document()

So far, we have dissected the body of the block as well as the outermost message send. If we put together what we have found so far and add all the optional syntax elements back in, we see that

after_commit on: %i[create update] do
  __elasticsearch__.index_document
end

is equivalent to

self.after_commit(on: %i[create update]) do
  self.__elasticsearch__().index_document()
end

Now, let's look at the argument list:

(on: %i[create update])

And specifically, let's first focus on the expression %i[create update].

This is a percent literal, more precisely, a Symbol Array percent literal. It has the form

  • % character
  • i character
  • opening-delimiter
  • Symbols separated by whitespace
  • closing-delimiter

If the opening-delimiter is one of <, [, (, or {, then the closing-delimiter must be the corresponding >, ], ), or }. Otherwise, the opening-delimiter can be any arbitrary character and the closing-delimiter must be that same character.

These percent Array literals allow you to concisely create Arrays from whitespace separated bare words.

In this case,

%i[create update]

is equivalent to

[:create, :update]

As mentioned above, there is an ambiguity related to the interpretation of the argument list here: this could either be a keyword argument on whose value is the result of evaluating the expression [:create, :update] or it could be a Hash literal equivalent to { :on => [:create, :update] }.

We can't know which is which without knowing what the definition of after_update looks like.

So, there you have it:

If after_update is defined with a keyword parameter something like this:

def after_update(on:); end

Then the whole thing will be interpreted like this:

self.after_commit(on: [:create, :update]) do
  self.__elasticsearch__().index_document()
end

Whereas, if after_update is defined with a positional parameter something like this:

def after_update(condition); end

Then the whole thing will be interpreted like this:

self.after_commit({ :on => [:create, :update] }) do
  self.__elasticsearch__().index_document()
end

This involves the following syntax elements:

  • message sends
  • arguments
    • either keyword arguments
    • or positional arguments
      • with a trailing Hash literal
  • block literals
  • percent literals

But I'm still not sure how this syntax "on:" is created.

It's not quite clear what you mean by "how this syntax is created". The way all Ruby syntax is created (and in fact, all syntax for any programming language is created), is by writing down the rules for the syntax in the programming language specification. Now, unfortunately, Ruby does not have a single unified specification document, but for example, you can find parts of the syntax specification in the ISO/IEC 30170:2012 Information technology — Programming languages — Ruby specification. You can also find bits and pieces in the ruby/spec, for example on Symbol Array percent literals. Other sources are the RDoc documentation generated from the YARV sourcecode, the Ruby issue tracker, and the mailing lists, in particular the ruby-core (English) and ruby-dev (Japanese) mailing lists.

Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
  • Thank you. Your explanation on the differences between "keyword argument" and argument being a hash was actually really helpful in understanding other responses – itsmarziparzi Dec 24 '22 at 08:13
3

Let's dissect the various parts of the syntax in

after_commit on: %i[create update] do
  # ...
end

At first, about the array at the end. It uses Ruby's %-syntax to create an object, in this case an array of Symbols. %i[create update] is thus equivalent to [:create, :update]

There are various options to use this %-syntax to create various objects, e.g. %[foo] is equivalent to "foo" and %w[foo bar] is equivalent to ["foo", "bar"]. The delimiter characters are arbitrary here. Instead of [ and ], you can also use { and }, or even something like %i|foo bar| or %i.foo bar`. See the syntax documentation for details.

Second, the on:. Here, you are passing the keyword argument on to the after_commit method and pass the array of Symbols to it. Keyword arguments are kind-of similar to regular positional arguments you can pass to methods, you just pass the argument values along with the names, similar to a Hash.

Historically, Ruby supported passing a Hash as the last argument to a method and allowed omitting the braces there (so that you could use after_commit(on: [:create, :update]) rather than after_commit({:on => [:create, :update]}). Ruby's keyword arguments (which were first introduced in Ruby 2.0) build upon this syntax and refined the semantics a bit along the way. For the most part, it still works the same as when passing a Hash with Symbol keys to a method though. Note that different to e.g. Python. positional arguments and keyword arguments can not be arbitrarily mixed.

You can learn more about keyword arguments (and regular positional arguments) at the respective documentation.

The method call is thus equivalent to:

after_commit(on: [:create, :update]) do
  # ...
end

The on ... end part is a block which is passed to the after_commit method, another Ruby syntax. This allows to pass a block of code to a method which can do something with this, similar to how you can pass anonymous functions around in Javascript. Blocks are used extensively in Ruby so it's important to learn about them.

Finally, the after_commit method and its associated behavior is defined by Rails. It is used to register callbacks which are run on certain events (in this case, to run some code after the database transaction successfully was committed in which the current ActiveRecord model was created or updated).

This is described in documentation about ActiveRecord callbacks.

Holger Just
  • 52,918
  • 14
  • 115
  • 123
  • Thank you. So I didn't know what a "keyword argument" was. So basically this is like saying you have a variable "on" and "on = [:create, :update]" – itsmarziparzi Dec 22 '22 at 11:16
  • On a totally separate note that I also had trouble googling. Is there an easy way to tell in Ruby if something is a function call or a variable? For practically every other language if it's not followed by a parenthesis, it's a variable, but in Ruby you don't need parenthesis for function calls. Is it the "do" that symbolizes it's a function call? – itsmarziparzi Dec 22 '22 at 11:18
  • The difference between using a variable and calling a method is intentionally blurred in Ruby as part of the language. In the end, if you pass any arguments, it is definitely a method call. Without arguments, Ruby first tries to read a local variable if it is defined; if not, it tries to call a method with that name on `self`. – Holger Just Dec 22 '22 at 11:53
  • Technically these aren't real keyword arguments as [the method](https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#method-i-after_commit) is defined as `after_commit(*args, &block)`. In that case Ruby will imply that the last argument is a hash. So its actually equivilent to `after_commit({ on: [:create, :update] })`. – max Dec 23 '22 at 09:29
  • Its thus a regular positional argument just that Ruby will imply the brackets around the hash. The method would actually have to be defined as `after_commit(*args, **kwargs, &block)` in order for it to actually use keyword arguments. Rails is a quite old codebase at this point and much of it predates the induction of keyword arguments in Ruby 2.0. – max Dec 23 '22 at 09:35
1

Try the following code in the console if you can:

def fn_args(*args, &block)
  puts 'args: ' + args.inspect
  puts 'block: ' + block.inspect
  block.call
end

fn_args on: %i[create update] do
  puts 'Heya from passed block'
end

I get:

args: [{:on=>[:create, :update]}]
block: #<Proc:0x0000020bc91c6a98@(irb):21>
Heya from passed block
=> nil

What does this mean?

fn_args is created with the same function signature as after_commit, we can see (I think) that on: %i[create update] is getting treated as a generic hash as part of the function arguments. It's not a special language feature (Ruby or Rails), though the interpretation of on: %i[create update] as a hash (or part of a hash) is a special feature of Ruby and the way it handles function arguments.

We could rewrite our call to:

fn_args({ on: %i[create update] }) do
  puts 'Heya from passed block'
end

and it should work the same way.

Ben Stephens
  • 3,303
  • 1
  • 4
  • 8
0

after_commit is triggered on create, update, destroy

If you want to run the callback only on specific actions you can make use of :on

Example:

after_commit :calculate_total, on: [:create, :update]

private 

def calculate_total
  update(total: subtotal + tax)
end

Given the above snippet, it makes sense to calculate the total only on create and update but not on destroy

Read more here - https://apidock.com/rails/ActiveRecord/Transactions/ClassMethods/after_commit

Deepak Mahakale
  • 22,834
  • 10
  • 68
  • 88
0

With "on:" you can specify the ActiveRecord actions which will trigger this callback method.

Callbacks can also be registered to only fire on certain life cycle events

class User < ApplicationRecord
  before_validation :normalize_name, on: :create

  # :on takes an array as well
  after_validation :set_location, on: [ :create, :update ]

  private
    def normalize_name
      self.name = name.downcase.titleize
    end

    def set_location
      self.location = LocationService.query(self)
    end
end

You can check out more from here