7 ms·
The Dynamic Def – abusing Ruby's def statement
- archimedespi 11y agoHa! An IRB-based interactive adventure is actually really, really cool and clever. It never fails to amaze me how much Rubyists abuse metaprogramming and langauge quirks. Somebody now shall goeth forth and implement Zork...
- jamis 11y agoThanks! I'm not the first to talk about using IRB for interactive fiction, but I think I might be the first to do so using nested defs. :) IRB-based Zork would be awesome! I hope someone does that.
- wdewind 11y agoCool article about the craziness of Ruby. Ruby is a frustrating language. The oauth gem, for instance, redefines '==(val)' on the AccessToken to 'Base64.encode(self.signature) == Base64.encode(val).' This stuff feels really dangerous and unnecessary. I spend a lot of time on code reviews pointing out bad features of Ruby (and Rails) that we shouldn't be using because they break application flow and make it significantly harder to reason about the code for the small benefit of decreasing a few lines. But it's certainly fun to talk about :)
- rylee 11y ago> The oauth gem, for instance, redefines '==(val)' on the AccessToken to 'Base64.encode(self.signature) == Base64.encode(val).' Is this just a complaint about the ability to overload operators in general?
- wdewind 11y agoIt's a complaint about the ability to redefine many things that shouldn't be redefine-able, operators included.
- JoeAltmaier 11y agoVery right. Operator overloading is a self-indulgent trick. The only one to benefit is the author. Subsequent readers are mostly confused. Instead of overloading, I've always wanted to define new operators e.g. <DotProduct> used as 'int x = v1 <DotProduct> v2;' If you're going to do something with operators, at least let me make descriptive ones.
- wdewind 11y ago> Operator overloading is a self-indulgent trick. The only one to benefit is the author. That's a great way to put it.
- icebraining 11y agoI disagree, operator overloading often makes the code much more readable. I don't see the problem unless you're trying to write C in Ruby. As long as the overloaded implementation keeps the expected semantics as described in the language documentation, it's fine.
- JoeAltmaier 11y agoIts not ever 'fine'. Its confusing at best. Imagine a component with 5 attributes. Does '==' match them all? Some of them? Loosely or tightly? I'm afraid just seeing '==' in the code is never going to be informative. Instead, maybe a method MatchAttr1And2(v1, v2) would certainly tell a subsequent reader a little more about what's going on.
- icebraining 11y agoSure, for some cases, == is not immediately clear. Don't use it in those cases.
- msbarnett 11y ago> Imagine a component with 5 attributes. Does '==' match them all? Some of them? Loosely or tightly? I'm afraid just seeing '==' in the code is never going to be informative. This is, fundamentally, a disagreement about the value of encapsulation. With an opaque, encapsulated type, '==' should mean whatever makes the most sense in the context of that type. For a pointer that might be "equality" means "same memory address", whereas for a vector that might mean "equal components". As a user, "equality" should match an intuitive understanding of what it means for two things of this type to "be the same". It's an art form. Like many things in programming, doing it well requires good taste. Operator overloading is a powerful technique for preserving encapsulation. It's the polar opposite of: > Instead, maybe a method MatchAttr1And2(v1, v2) would certainly tell a subsequent reader a little more about what's going on. This leaks implementation details like a sieve. It's a great recipe for encouraging dependency on a particular implementation detail across module boundaries, and rolling yourself a great heaping ball of mud.
- regularfry 11y agoIn its place, being able to redefine equality on value types really helps clarify code. Misused, it creates confusion. The problem is that it's easy to think you've got a case where it helps, when actually it's not well-defined. Usually that revolves around there being state you care about which is missed from the equality comparison. Another trap is redefining #== without also looking at #eql?, which means Hash doesn't behave like you expect. It's just another bit of mental trivia you've got to Just Know...
- wdewind 11y agoCan you give me an example of where redefining equality makes sense?
- JoeAltmaier 11y agoWithout explicit comments/documentation it is hard to imagine a good example. Its a longstanding issue. Even Lisp has 'equals' and 'equals?' which is an abomination. One looks for identity of object; the other for identity of value (if I understand it right). These kind of things are bug factories.
- icebraining 11y agoI think Python make the difference clear, with == vs "is".
- msbarnett 11y agoRuby provides object_id for the same purpose. Comparing those for equality provides the "is_" semantics.
- deckard1 11y agoKent Pitman has always been a good read, for the problems of equality in dynamic languages. The typical Ruby or JS programmer stumbles through the day just "getting by", where it comes to comparing objects. http://www.nhplace.com/kent/PS/EQUAL.html http://www.nhplace.com/kent/PS/EQUAL.html
- pdkl95 11y agoRuby is a wonderful (and wonderfully powerful) language. Unfortunately, some of the popular ruby libraries (gems) are... problematic. To put it nicely. The web/rails gems in particular can be nasty minefields. However, the language itself is great. The trick is to remember the usual advice that just because you can doesn't mean you should. Too many gems add "clever" metaprogramming (such as the def tricks in this article), when it isn't actually making the program simpler. The beauty of ruby is that while it can act almost like a LISP for the times when you want powerful metaprogramming features, while also allowing simple shell or C style imperative code when that is more appropriate. (Of course, some people fear that type of freedom... https://vimeo.com/17420638 https://vimeo.com/17420638 )
- TheAceOfHearts 11y agoRuby is really fun to program in, but debugging it can be hell. I would not be very enthusiastic about debugging this code. But still, this is a really cool trick that I didn't know you could pull off with Ruby, so thanks for sharing!
- jamis 11y agoAgree about debugging. To my thinking, though, there are two types of "programming": professional (for day jobs, where debugging, testing, and maintainability matter), and recreational (where the point is to just explore new things and try crazy stuff that no one in their right mind would ever "really" do). This "def" stuff falls into the latter category. Honestly, I wish there were more people doing posts about recreational programming topics. WhyTheLuckyStiff was one of the last great recreational Rubyists. I miss that kind of no-holds-barred exploration.
- TeMPOraL 11y agoI don't see anything hard to debug in this code. Honestly, if this kind of code makes debugging hard for one it means that one assumes too much about how code should behave instead of looking how it actually behaves. I hate this "dynamic programming[0] = hard to debug" meme. A truly hard to debug code is one that's nonlocal (you have to read 20 files to follow the execution flow (hello Scala trait abusers)) or displays random behaviour (e.g. threading, dependence on external resources). This one? Every "tricky" thing is contained in a block of 10-20 lines. Just isolate the endpoints and follow the execution until it does something you think it shouldn't. In a way, the biggest enemy of a bug hunter is their own assumptions. [0] - examples here aren't really metaprogramming, and even the latter isn't that hard to debug if you actually sit down and read the code.
- deckard1 11y agoDebugging Ruby, in general, is often a pain in the ass. It stems from an awful combination of terseness and metaprogramming. The terseness comes from using an identifier to both represent variable reference and method invocation. Contrast this to Lisp-like langauges, where function (or macro) invocation only happens in the first position of an S-expression. In Lisp, it's clear that an identifier is either a variable or a function depending on where it's located. In Ruby, no visual indication exists. Worse, things like attr_reader blend instance variables with local variables and method identifiers. Throw in a method_missing and inheritance, and you can easily lose weeks just tracking down where an identifier is even coming from. Throw in a gem or two, and all hope is lost.
- regularfry 11y agoSo, about that state machine definition and the method cache...
- Freaky 11y agoOr the JIT. We're not all using MRI.
- theyeti 11y agoI've somehow always found Ruby to be opposite to the Unix philosophy of doing one thing and doing it well. While Ruby may seem trivial and fun in the beginning, it tends to be cumbersome and maintainable as the size of the repository grows. Coming from a Python world, my first reaction to Ruby was that it was more like Perl where there are many ways to achieve the same thing, and no it was not really helpful if you inherited poorly written code.
- kazinator 11y ago> opposite to the Unix philosophy Bash: $ foo() { bar () { echo I am bar } } $ bar bar: not found $ foo $ bar I am bar Pretty much the same thing as the Ruby example. This is simply because function defining is a kind of statement or expression with a side effect which must be evaluated. The side effect is global (a name is globally associated with a function). So if the side effect is in a function body, its evaluation is delayed until the function is called, and then its effect is still global.
- adrianmacneil 11y agoTouché
- TeMPOraL 11y agoThis is indeed the basic feature of any language that's evaluated at runtime. When working with such a language, one needs to learn the program as a dynamically growing construct instead of a vision cast in stone when you press the "compile" button.
- kazinator 11y agoOr rather, one needs to learn which constructs destructively manipulate a global environment, and which perform lexical binding. In Python, an inner def will lexically bind a function, creating a closure. Python is not less dynamic than Ruby. In Common Lisp, a defun inside a defun will behave similarly to Ruby; but if you want lexically scoped local functions, you use a different operator, namely flet or labels. Scheme has a define which is lexical: it brings a lexical identifier into the scope for forms which follow. Lexically scoped items, even in a dynamic language, in fact can be "cast in stone when you press the compile button"; they are cast in that stone which is the entire compiled environment of the surrounding function.
- zwp 11y agoZork-alikes aside... I'm looking for practical applications :) I recently re-watched Yaron Minsky's "Effective ML" talk where (towards the end) he talks about making read-only and read-write types. That's where I thought Jamis was going with the state machine example: one could tell an object "yo, make yourself immutable!" (ie redefine your methods so that you can't change yourself). But in an OO world that seems more neatly achieved with subclassing. [To the extent of faking anything "immutable" in ruby"]. There's #freeze I suppose... and no #unfreeze. Which is perhaps sensible :) And #freeze only guards against new assignment to instance variables; it doesn't guard against an instance method mutating the content of an instance variable (def esquirify! ; @name << ' Esq.' ; end). So there's that. But I'm far from convinced... Python has this "inner-methods" capability (with saner scoping) which is a great antidote for python's limited lambdas. But that's not a problem with ruby. It's a neat trick (and nice to read something from Jamis again). Are there any sensible use cases?
- netghost 11y agoOne use might be to create simple singletons. That said, I'm not entirely sure how you would do it, maybe have `Object#initialize` redefine `Object#new` to always return the singleton.
- netghost 11y agoYup, here's how to use it to define a Singleton that is transparent to the caller: class S class << self attr_accessor :singleton end attr_accessor :value def initialize @value = "xxx" S.singleton = self def S.new S.singleton end end end a = S.new # => #<S:0x007ff4a41532e0 @value="xxx"> b = S.new #=> #<S:0x007ff4a41532e0 @value="xxx"> a.value = "zzz" b.value # "zzz" a.object_id == b.object_id #=> true That said... doing this may win you great sorrow.
- seivadmas 11y agoJust because you CAN do something doesn't mean you SHOULD. Metaprogramming has a place in Ruby but for the examples in the article there are far more readable ways to implement it. Readability > cleverness every single time.
- TeMPOraL 11y ago> Readability > cleverness every single time. Readability is in the eye of the beholder. For those unfamiliar with higher-order programming, maps are just a 'too clever' form of a for loop. A clean and understandable solution is one matching the problem being solved in a precise way. In search for simplicity one can't forget that programming is a trade, and one should be expected to actually learn some shit.
- braythwayt 11y agoTo that point, Jef Raskin famouly said that “intuitive == familiar,” and all-too-often, that’s exactly what people mean when they talk about “intuitive" code and/or user interfaces.
- TeMPOraL 11y agoIndeed. Looking over the article, the first example is sort of obvious if you ever worked more than few hours with a decent dynamically typed language, and the rest expose interesting functionality that could be papered over with a macro in order to build something useful. Like, you know, object-oriented programming can be built by hiding lexical closures under a macro or two, and was in fact built that way in the past. In other words - just because you don't understand something doesn't mean it's "clever code".
- adrianmacneil 11y ago> Just because you CAN do something doesn't mean you SHOULD This is hacker news, not "production code" news. Abusing things in unexpected ways is pretty much the definition of hacking. Many people seem to be jumping on this article as if the author was suggestion a new programming pattern we should start using, rather than an interesting look at some of the lesser-known quirks of ruby. I'm pretty sure even the author would agree that triple nested method definitions are not something we should use for production code.
- bascule 11y agoDefining instance-specific behavior of any kind is catastrophic to method caching. JRuby has a hierarchical method cache so it can clear only what's needed, but MRI does not: http://jamesgolick.com/2013/4/14/mris-method-caches.html http://jamesgolick.com/2013/4/14/mris-method-caches.html The late, great James Golick had a patch to add one once, but it never got merged upstream. If you care about performance even the tiniest bit at all whatsoever, please don't use the techniques discussed in the OP in production code or in your gems. It may make your memos 30% faster on a microbenchmark... while causing the rest of your program to run considerably slower.
- byroot 11y agoIt was merged for MRI 2.1: https://bugs.ruby-lang.org/issues/8426 https://bugs.ruby-lang.org/issues/8426
- bascule 11y agoThat change was reverted: https://bugs.ruby-lang.org/projects/ruby-trunk/repository/revisions/43027 https://bugs.ruby-lang.org/projects/ruby-trunk/repository/re... It remains an open issue: https://bugs.ruby-lang.org/issues/9262 https://bugs.ruby-lang.org/issues/9262
- byroot 11y agoI insist, it's mostly solved: http://tmm1.net/ruby21-method-cache/ http://tmm1.net/ruby21-method-cache/
- qewrffewqwfqew 11y agoFor a much deeper exploration of this kind of dynamic object behaviour in Tcl, Sean Woods' "Lifecycle Object Generators" is a fun read: http://www.tclcommunityassociation.org/wub/proceedings/Proceedings-2012/SeanWoods/Lifecycle-of-Objects.pdf http://www.tclcommunityassociation.org/wub/proceedings/Proce...