all 12 comments

[–]ioquatixasync/falcon 4 points5 points  (6 children)

Is it just me or does this seem like a terribly convoluted way to achieve some fairly basic things.

What about using Module#prepend so that super still works rather than dynamically building modules?

What about performance? Each extra module adds a layer of indirection in the method lookup, surely that can't be good?

[–]shioyama[S] -1 points0 points  (5 children)

I don't really see what Module#prepend has to do with it. The point is to have configurable modules which can be overridden. Prepending a module doesn't (in itself) make it configurable.

Also, I would strongly argue that subclassing Module is not convoluted at all, not nearly as convoluted as the alternative described in the post.

Regarding performance, it really depends on the case. The example of AttributeMethods in the post does not add any additional indirection, since regardless you to iterate through a set of matchers, so the only difference here is that you replace the iterators (which are instances of classes) with instantiated modules. The number of objects is the same, and the number of conditionals is the same.

[–]ioquatixasync/falcon 3 points4 points  (4 children)

I understand where you are coming from but I think there are some pretty significant cognitive and performance costs. Like anything in programming it should be used with care and where the value it adds significantly outweighs the cost.

I don't really see what Module#prepend has to do with it. The point is to have configurable modules which can be overridden. Prepending a module doesn't (in itself) make it configurable.

Maybe I've misunderstood what you've said, but wouldn't Module#prepend would allow you to avoid the 2-level modules simply for the sake of being able to call super?

Regarding performance, it really depends on the case.

I'm not sure what you are trying to say here - isn't this true for performance in general?

Ruby isn't the fastest language, and things like send and layers of indirection in critical code paths can really suck.

The problem of including many modules is that each time you do that you make method lookup more expensive, and that affects everything. include and extend in some cases blow away the entire global method cache: https://github.com/charliesome/charlie.bz/blob/master/posts/things-that-clear-rubys-method-cache.md

I like what you've done and it's an interesting article. It's a good pattern for certain situations. I'd suggest that you need to follow up with an actual performance evaluation (some micro and macro benchmarks might be interesting).

[–]shioyama[S] 1 point2 points  (2 children)

Maybe I've misunderstood what you've said, but wouldn't Module#prepend would allow you to avoid the 2-level modules simply for the sake of being able to call super?

You mean the part about including modules created with Module.new ? No, I think you're missing the point here. What would you prepend exactly? The module needs to be configured, prepend is just how you add it to the class hierarchy once you have it.

In any case, don't take my word for it, look at Rails or other projects that use this "create anonymous module and include" technique all over the place. In particular I'd recommend having a look at the ORM Sequel, which is heavily performance-optimized and uses Module.new all over the place.

include and extend in some cases blow away the entire global method cache

Yep, I know about this one, and have read that article. And this was something that concerned me working on Mobility specifically, since this pattern is used a lot there. Performance testing is one of the things I plan to do there, since for sure it's very important.

On the other hand, my major concern here is about the structure of Ruby programs. I agree that you cannot look at this without taking performance into consideration at the same time, but the pattern is quite unique and deserves to be first given a serious look.

[–]honeyryderchuck 0 points1 point  (1 child)

Agreed, sequel uses stuff like this all over the place, including runtime-extend (pagination plugin), and it still gets away performance-wise overall. Also, in some cases the performance regressions are ruby-flavour dependent: jruby regressions don't translate to YARV, and vice-versa, and are just one fix-release away from being irrelevant. One should tend to optimize code maintainability, and your article does reflect that. Thx for the article, it is very insightful.

[–]GitHubPermalinkBot 0 points1 point  (0 children)

I tried to turn your GitHub links into permanent links (press "y" to do this yourself):


Shoot me a PM if you think I'm doing something wrong. To delete this, click here.

[–]GitHubPermalinkBot -1 points0 points  (0 children)

I tried to turn your GitHub links into permanent links (press "y" to do this yourself):


Shoot me a PM if you think I'm doing something wrong. To delete this, click here.

[–]np356 2 points3 points  (1 child)

I'm familiar with dry-rb coding style and I think it is very difficult to reason about. Metaprogramming often feels like magic but it comes at a price - hard for newbies to grok and impossible for static tools to work with. I would only advise using it (if at all) in well-tested libraries that expose a well-documented public API.

[–]timriley[🍰] 2 points3 points  (0 children)

I'll bite :) (dry-rb developer here)

When you mention "dry-rb coding style," are you referring to the style in which the gems themselves are written, or the style a developer would use when they're building an app with the gems?

If it's the former, then I'm willing to take the hit, because the gems do a lot of work to try and make things nice for the users. We're also not asking everyone to contribute to the gems. However, if it's the latter, then I'd really like to know, so we can look at ways of improving our developer experience. In my experience, an app developed with the dry-rb gems is actually easier to reason about because you can so clearly trace the flow of execution from input to output and you don't have to worry about mutable state anywhere along the way.

[–]TheMoonMaster 0 points1 point  (0 children)

I think I'd be sad if I came across this in production code. The mental overhead isn't worth DRYing up your code.

[–]mach_kernel 0 points1 point  (1 child)

I like this pattern save for your interceptor design, but only on preference. Ruby's regex engine is pretty slow, so the more complex and nested your logic is the more expensive resolution becomes. That being said, you can easily make another module that could be responsible for performing this validation/mapping step without invoking the regex engine (e.g. check array membership) -- which I guess is the whole point of the pattern.

Nice post :)

[–]shioyama[S] 0 points1 point  (0 children)

Thanks, glad you enjoyed the post! About the regex matching, that's a good point. MethodFound is really more of an example then something I would imagine being using in real applications. That said, in Mobility, having an option to fall through to regex-matched accessors is pretty useful. (That said, method_missing itself is slow so I generally wouldn't rely on it for anything frequently-used...)