self is one of those Ruby keywords that looks easy until you meet it in real code.
You see it in a setter:
self.name = normalized_name
...
For further actions, you may consider blocking this person and/or reporting abuse
The setter case is the one that actually bit me in a Rails model. I had a callback doing
status = "paid"and spent an embarrassing amount of time wondering why nothing persisted, because of course it just created a local variable. After that, I started treating every bare assignment inside an instance method with suspicion. The scope gate framing is nice too, since it explains why the block case feels inconsistent when it really is not.Yeah, that
status = "paid"case is a nasty one because it looks completely reasonable at a glance. Ruby is basically saying “sure, that’s a local variable” while you’re thinking “no, update the model.”A similar thing shows up when you have a setter and a method with the same name:
The interesting part is that the setter itself isn’t the weird bit. The missing receiver is. Ruby needs
selfthere to know you mean the assignment method rather than creating a local. That’s also why I like the scope-gate framing. Once you start looking for the receiver and the current scope instead of memorizing Ruby’s “exceptions,” these cases become much easier to reason about.That minimal repro is going straight into my notes. You put it better than I did: the setter isn't the weird bit, the missing receiver is. Once you read self as a message send instead of boilerplate, the behavior stops feeling arbitrary.
Exactly. And I think that distinction becomes even more useful once you hit Rails code that mixes implicit receivers with DSL-style APIs.
For example, in an ActiveRecord model,
updateandupdate!are explicit message sends, while something likestatus = "paid"is parsed as local assignment before Ruby ever gets to the question of which object should receive a message. That difference can become surprisingly important when a callback or service object is doing several things that look like ordinary Ruby expressions.Once you start reading
selfas the receiver of a message, these Rails “magic” moments become much less magical. You can usually trace the behavior back to ordinary Ruby parsing and method dispatch.Good point. The update(...) case is a nice anchor: it is clearly a message to the instance, so it steadies the mental model before you hit the ambiguous assignment. I still double-take on bare assignment inside callbacks now and then, even after years of this.
Yeah, and that’s probably why I think the “receiver” model holds up better than memorizing Ruby syntax rules.
What gets interesting in Rails is that DSLs can deliberately make the receiver less obvious. Take something like:
Inside
publish_event,statusis an implicit message to the currentself, whileOrderEvent.create!has an explicit receiver. Thensaved_changesis another implicit message, but its meaning depends entirely on what objectselfis at that point.That’s where Rails can make the same Ruby rule feel more complicated than it actually is. The framework adds a lot of DSL surface area, but the underlying question is still pretty mechanical: who is receiving this message right now?
Once that becomes the first question you ask, callbacks, scopes, validations, and model DSLs start feeling a lot less magical.