I understand that if you like Ruby a whole lot and like being a web developer, but you don't like Rails for whatever reasons, it can be a frustrating experience over the long haul.
But as a counter-example to that, I'm constantly amazed that there are a ton of Ruby gems out there which pretty much work as advertised outside of Rails (even if Rails gets a privileged happy path during config). Not to mention you can use ecosystem stalwarts like Puma, Rack, Sidekiq, and many other subsystems outside of a Rails app entirely. Heck, you can pull just bits of Rails in if you really need them. Want Active Record for your ORM but nothing else? That's totally possible! (But of course then you get a bunch of Active Support stuff too which is what the author objects to.)
Personally I love Active Support and actively (heh) bring it into my Ruby projects if it's not there already. To a certain degree, everything people say is a "bug" about Ruby's metaprogramming/monkeypatching is a feature in my book. The fact that "Ruby core" is simply a substrate upon which you can build your own flavor of a Ruby-plus language—be that "The Rails Way" or something else entirely—is amazing. In that respect, I entirely disagree that Rails is not written in Ruby. Rails shows us how you _can_ (and probably should!) use Ruby to build DSLs which suit your specific application/framework purposes very well. It's not a bug. It's a feature. And it's why most other tools which claim "Rails but for Language X" fall short…they can try to replicate features of Rails, sure—but because it's not Ruby, it misses the whole point of using Rails, which is that you get Ruby "for free" to sweeten the deal! That's the (not so) secret sauce here.
> I'm constantly amazed that there are a ton of Ruby gems out there which pretty much work as advertised outside of Rails
I must come to Ruby from a very different angle to you - but I don't even really get what you mean. Why would the majority of gems have anything at all to do with Rails or need it to work as advertised?
For example the top-ten gems have got absolutely nothing at all to do with Rails have they?
Why would the majority of gems have anything at all to
do with Rails or need it to work as advertised?
If you put Rails-specific functionality in your gem, now you have a dependency on Rails, and given the seamless and transparent ways in which Rails (specifically, ActiveRecord and ActiveSupport) extend Ruby, it's easy to unintentionally wind up making the non- Rails specific bits of your gem depend on Rails somehow.
Additionally one could say that AS/AR serve as sort of a defacto standard library of sorts at this point. You copy some code from Stack Overflow, it's got a dependency on one or the other, etc.
I'm not endorsing that state of affairs.
I actually don't know how I even feel about that state of affairs. The mostly-transparent extensibility of Ruby is the best and the worst thing about it. It's great if you're writing a framework or gem; but it makes it really tough for beginners to know what is doing what.
Because the ruby community generally revolves around rails. Stack overflow often has generic ruby questions where answers assume you're using rails (by referencing activesupport, for example). Just to give you an idea, on stackoverflow, there are 222k ruby questions and 330k rails questions tagged.
> Because the ruby community generally revolves around rails.
Be careful about to which Ruby Community you believe you are speaking for. Keep in mind that a lot of development of the language itself is happening in Japan, in Japanese, where most people won't be regularly reading and posting on Stack Overflow. 5 of the core contributors live within 15 minutes of each other and meet and discuss issues in person.
If you've watched recent developments in the Ruby Community there, especially in the areas where the Ruby team has made large performance gains to improve the language, Embedded Ruby (as in Ruby running on IoT devices, or mruby, not ERB templating) is actually a very hot topic and isn't even in the orbit of Ruby on Rails.
Japan is still seeing growth in adoption of the language and has a huge number of Ruby conferences. There's multiple "regional" versions of RubyKaigi in Japan throughout the year.
Indeed. I subscribe to [ruby] and ignore [ruby-on-rails], it can get pretty lonely in there. Usually just basic questions. I wish someone would ask something that would require me to study the virtual machines in order to answer.
> Why would the majority of gems have anything at all to do with Rails or need it to work as advertised?
That's what your often hear from "Rails skeptics". According to them people don't even know what is Rails and what is Ruby, and that because of this, gems either have Active Support as explicit dependency, or use Active Support extensions without knowing it and then don't work outside of Rails.
I love python but I loathe django. I cringe everytime I have to deal with it or developers who believe that is the right way to write a web app. It's hard. Either people are using python to write scripts which are poorly written or they're using it to write django. Most of them don't understand python at all. It's sad. I think Ruby has that problem with Rails. The ones who learn only Rails don't understand Ruby and don't use it for anything beyond Rails.
I’ll admit to liking Django, but I don’t use it in every project. I don’t think Django has dominated the Python community to the extent to which rails has dominated the ruby community. For example Serverless applications, ML, the scientific community, and shell scripting all use Python without Django, and they’re pretty popular uses of the language. Also FastAPI appears to be doing pretty well
Sure, Django doesn't have that level of presence, but most of the tech companies I've interviewed with, especially startups, default to using Django alone, and being so rigid in their practices that they believe Django is everything. At my company, my colleagues only know Django or they write horrendous Python. I'm constantly doing my best to show them how to write production-grade python code without having to rely on Django - Airflow DAGs for example, but sadly, they seem uninterested - which is something I have realized I cannot fix. It's just a job, so at the end of the day I've to let go.
It's odd though, how unreadable Python code can end up being if you don't bother caring about how you write code.
So like, I guess the thing I don't understand is this: why do people think writing:
some_array.has_my_thing() is better than has_my_thing(some_array). ?
The former, the extension has so many issues, it messes up namespacing, it's unclear where it came from, etc. etc. The latter is just a function. It's not longer, or complex, it's just more straightforward. I really think things where you extend or override built in types is almost always just a bad idea, because the alternative is almost always just as concise and doesn't confuse everyone.
Note the first example could be an example of chaining - where each function returns "self" and we progressively transform the object. Or it could be simply each function returning different values which are operated on. Either way, the pattern is usually cleaner and easier to debug than the nested equivalent.
Even though Elixir is functional and aggressively not object oriented, if you squint at this pattern you get the readability of an OO-style method chaining API.
If I understand the Ruby pipeline operator it's essentially an alias of the dot. The example in the feature tracker is:
```
1.. |> take 10 |> map {|e| e2} |> (x)
```
which is equivalent to
```
(1...).take(10).map{|e| e2}
```
So in order to use it you still need extensions on every single object. The only reason that looks nice is because in this example you continuously call methods on lists that return a list. You can't add `my_custom_list_operation(list_input)` in between.
Going forward, the pipe operator is going to be showing up in many new languages. I've used elixir a lot, and the pipe operator is a genius piece of syntactic sugar.
It's worth noting that that version of pipeline operator was abandoned, while there are still (some newer) open feature requests for a more traditional functional-style pipeline operator.
The end result is superficially different. That's the point - we like the flow of the end result! But the implementation is completely different, as it's pure functional compared to object oriented.
"string".do_stuff() - do_stuff() is a method on the "string" class, with all of the potential gotchas that might come with an OO implementation. Does the method mutate the internal state of the object? Does the particular class override the default implementation? Is it a monkey-patched implementation (the issue raised by this article)?
"string" |> do_stuff() - do_stuff() is a standalone pure function. The data is immutable and does exactly what it says.
But this kind of functional composition often doesn't have first-class support in the language syntax. That is, it often requires you to rely on helper functions or libraries. As a result, it's not nearly as readable or intuitively obvious as the chaining example (and this is in contrast to mathematical notation, where functional composition is very clear, in my opinion).
I wasn't thinking specifically of Ruby (and indeed I don't know Ruby, nor was I aware that it has this kind of support).
Rather, I was thinking about programming languages in general, since the top-level comment seemed to be talking about the general principle.
Out of 5 languages I use on a regular basis, only one of them has _some_ support for function composition. But even then, not as flexible as what you're demonstrating here. So my point is that across programming languages, the chaining approach is generally more readable than the composition approach. And that in turn favours methods over global functions.
If I ever start using Ruby, I would probably be on the fence about which approach is better though.
You don't need to implement chaining for X.foo() to make sense. It's object-oriented syntax. If anything, languages like Python are the odd ducks here, mixing magical globals (len(X) => X.__len__()) with plain-old OO syntax conventions.
That Ruby also implements X.foo().bar().baz() is a weird thing about Ruby that can be very powerful, but is unrelated to the dot syntax.
I feel like roughly half of Ruby programmers treat it as "Lisp but better", which leads to confusion about the OO aspects of the language.
I'm pretty sure they're both equally composable as I understand the term for programming language usage, it's just that you prefer the first way of writing it.
Admittedly I don't think anyone really prefers the second way of writing it, but that's not the same as being ecstatic over the first one.
Programming language syntax matters. It's the interface in which the programmer writes the code, synthetic sugar can reduce the "mental overload" of a feature.
For example, in C, the array operator is mostly synthetic sugar for pointer addition, and could be removed. Should it be removed? Of course not, cause programmers are not thinking of pointer arithmetic when indexing an array.
And as much as I love lisp, it's one of the top reasons none of them ever had widespread adaption. Nobody likes having to do math like this (* (+ (sqrt 4) 2) 4).
A now-deleted (v_v) comment rightly called out that in many languages, method-style invocations are inextensible: a fixed set of methods exists when the class is created, and you can't add any more after the fact. So at some level you're forced to use direct-style invocations, and mixing the two can become ugly.
Some languages (like C# and Rust) support extension methods, so you can use method-style to invoke separately-defined procedures. But in other languages, method-style APIs become an inextensible privileged zone, and I personally prefer to minimize the number of inherent methods as much as possible. By preferring direct-style invocations, I avoid the temptation to stick a useful method on the class itself "for convenience".
As someone that used to have to program in Java, moved over to Kotlin, but now have to work with Java again... I miss extension methods so much. Util classes feel so hacky, and are so ugly to use.
Specially for cases like streams, where you are invoking a chain of operations, and then have to store the temporary result in a variable, or have worse readability.
Or for quick and dirty debugging, with print statements, being able to put a println in the middle of any method call, just by adding a .also{println(it)} to the chain.
One pattern I see sometimes is to define a util class, but rather than having a bunch of static methods, you wrap the underlying object in a util wrapper that adds all of the utility methods. Unfortunately, Java also has no good way to delegate methods without simply redefining all of them, so it's by no means a perfect solution. (And it's not really worthwhile unless you've got a fluent API that returns the same object every time; otherwise you're wrapping for just one call, which is arguably noisier.)
The first example is three characters longer and dispatches methods under the hood. There could easily be a dozen do_thing, do_another_thing and do_something_else methods.
In the second example, we have only function application. Put a breakpoint or print statement into do_another_thing and it is sure to go off.
(do_another_thing could support OOP dispatch as a generic function. A generic function is still something that is called first then dispatches methods based on its arguments. E.g. in Common Lisp we can (trace do-another-thing) to trace the calls.)
It's not just sugar; it also does things like ensuring that the value is actually a nonnegative integer.
And for other unary "magic methods", the disconnect can be even bigger. E.g. iter(x) is not just a synonym for x.__iter__() - it can also produce an iterator for something that only provides __len__ and __getitem__.
This is, I think, largely a result of Python not idiomatically using mixins as much as Ruby (since it has multiple inheritance, it supports them, and they are sometimes used, but Ruby leans into them hard in the core.)
In the Ruby way, with otherwise similar setup, you would have a mixin that provided __iter__() given __len__() and __getitem__(), and classes that had the last two but wanted the behavior of the first would just include that mixin class as a parent, rather than having a separate branch in iter() to handle the different cases. (Ironically, its one way that Ruby is less TMTOWTDI than Python is.)
I generally like Ruby better than Python, but Python's choice on
_join_ being a string method on the thing stuck between elements rather than an collection method on the things whose elements are getting something stuck between is better.
These method names feel like active form so 1 and 3 should be correct but it's 1 and 4 despite "a,b" obviously is not splitting "," but it's splitted by it
In Ruby it's 2 and 4.
I'm out of luck twice but at least Ruby has a and b always to the left and the comma always to the right so I always remember it.
The former's inherent namespacing is exactly why I prefer it. You know exactly where each method came from (it's a method that exists in a `some_array` instance namespace); inversely, I have no idea where `has_my_thing(foo)` comes from or what all it can be used for if it's just sitting in the global namespace.
Very often, when I'm learning new code, there's one line I'll write over an over in console bindings:
> some_object.methods
It provides a super-easy way to learn what methods are actually relevant to `some_object` without having to pour over every method available in the global namespace and figure out whether `some_object` is a valid argument to each. You can also apply set logic like `some_object.methods - {}.methods` to return only the methods available on `some_object` that AREN'T on typical objects, which is a very, very, very powerful learning/exploration tool.
Side note: you can also call `.source_location` on any method to see exactly what file/line it's defined in (even in core ruby/rails methods). There's a lot of other inspection methods available to learn more about objects and what you can do with them, which IMO is a benefit over your latter example.
Along these lines, I recommend also trying Pry's ls command, which breaks the methods down by which module or class implements them for a given object, and the show-source command.
It's this great ease of introspection that makes Ruby so fun.
Subjectively, the chief advantage is readability for long chains of function composition. Compare list.fft.map(square).sum.sqrt to sqrt(sum(map(fft(list), square))). This is all a matter of taste, but IMO the former reads more easily.
It's a pretty trivial matter, so I wouldn't go breaking encapsulation everywhere just to support it. D has a feature called Universal Function Call Syntax where a.f(b, c) is sugar for f(a, b, c) for any free function, rendering those issues moot. I just wish more languages would pick it up!
> I really don’t see what chaining via methods bring that simple function composition doesn’t.
Visual flow from input to output.
(Ruby can compose either way, so you can build a pipeline of procs in flow order, but if you aren’t chaining the input still goes at the end, not the beginning.)
That's not possible to write in most languages. If a language does support compose operator or custom operators then yes I think that style is good too. But there is no way to write that style in python/ruby.
My ideal preference is for a language to support universal function call syntax where x.f(y) is equivalent to f(x,y) always and also include support for custom operators (or at least include common functional operators).
> That’s not possible to write in most languages. If a language does support compose operator or custom operators then yes I think that style is good too. But there is no way to write that style in python/ruby.
Ruby has composition operators in both directions:
given:
x = 2
f = lambda {|a| a+1}
g = lambda {|a| a*2}
the following are all equivalent:
f[g[x]]
(f>>g)[x]
(g<<f)[x]
What’s inconvenient is that to use it with methods, even ones that can be invoked in ways that look like function calls in other languages, you have to realize a method object, since method names don’t name objects. Or, as Matz says (I don’t think of Ruby as a Lisp at all, but in this context it makes sense) Ruby is a Lisp-2, not a Lisp-1.
Those square parentheses are unfortunate because traditionally they are used to index arrays. I'd like to write one of
2.g.f
g(2).f
(g >> f)(2)
(f << g)(2)
with a strong preference for the first one. That's the Elixir pipeline with a dot instead of a |>
The second one is OK-ish but it turns weird with a long list of functions.
The last ones are not nice to read, too much syntactical overhead.
There is a ton of new syntax added to Ruby in the last years and they had to find a way to fit it into the space between what was already there. The results are not always nice IMHO.
> Those square parentheses are unfortunate because traditionally they are used to index arrays.
They’ve been among the methods for invoking procs since at least Ruby 1.8 (2003); there are several – all of these are equivalent.
f[x]
f.(x)
f.call(x)
f.yield(x)
> I’d like to write one of:
2.g.f
g(2).f
(g >> f)(2)
(f << g)(2)
Ruby is a pure OOP language, and “.” is the syntax for method invocation. So its really not available as a composition operator. So the first two don’t work for that reason. Further extending Ruby’s syntax to make unmodified parens both invoke methods and serve as syntax sugar for .call() on objects would probably be problematic (that’s why “.()” was adopted.)
> There is a ton of new syntax added to Ruby in the last years
None of the stuff related to this except the composition operators is added recently. And those aren’t really syntax, just normal operator methods added to the Proc class.
> So like, I guess the thing I don't understand is this: why do people think writing:
> some_array.has_my_thing() is better than has_my_thing(some_array). ?
The latter is in the global namespace (or you have to create a module/namespace for it). The former is encapsulated in a class. Encapsulation is nice.
One benefit is discoverability. I coded a ton of Ruby/rails back in the day and it was nice to be able to jump into a repl and see what methods were on an object. Also, you could often find the source of the method through the repl.
I think a lot of these debates come down to repl development versus ide development. I’ve done a lot of both, but many complaints come down to people on one side not understanding the way the other side works.
> Except here the encapsulation comes at the cost of extending a core library at runtime
No, it doesn't.
Pervasive OO style and monkey-patching core classes are orthogonal. Ruby favors the former strongly, and supports the latter, and Rails rather heavily leverages the latter (to the extent that it has been harshly criticized for it by much of the non-Rails Ruby community), but there is no necessary relationship between them.
In other languages (like Kotlin), that extension is actually not done on the object, but is a statically imported function and it's only syntactic sugar. This avoids all the pit falls (no collision, knows where the function comes from etc)
has_my_thing(some_array) can be encapsulated too, using modules. For example, in elixir you have functions like List.to_string(some_list) and Map.values(some_map). This way, you get encapsulation and don't have to have a class.
Both styles can utilize that kind of intellisense on parameters. What about code completions before you've specified the parameters? Not really possible in the functional style unless your functions are namespaced and then you're still limited to what was in that module. Only with object-verb method passing can you get code completions related to the data before you've started specifying (the rest of) the parameters.
When declaring a variable, in a typed language you could use the variable type to filter down the possible function completions based on return type; in practice, I don't think any LSPs do that.
In your example, `has_my_thing` then has to be able to handle any possible input (even if it's just to say "I can't handle this input). Example: length of objects/hashes, and length of arrays.
And, sure, I guess you could `Array.has_my_thing(some_array)`... but that's just re-arranging the mixin structure behind `Array`. It only looks different until you start peaking under the hood.
IMHO, sometimes you want a function because in your problem domain there's a universal (or universal enough) act/question/etc, and sometimes you want a member function because in your problem domain the act/question/etc only makes sense given some particular context.
I think this might be hosting complexity which reminds me a little of Rich Hickey's Simple Made Easy talk. The Rails way seems more readable because it's hiding complexity. The other approach reveals the complexity, but actually has less complexity than the Rails style solution. This might only become apparent when something goes wrong.
> The former, the extension has so many issues, it messes up namespacing, it's unclear where it came from, etc. etc
That depends on the implementation. A monkey-patch is definitely problematic, but when it's properly scoped, like C# extensions, Rust Traits or Ruby Refinements, it's quite elegant.
Because it is baked into the class itself. Ruby is global variables "fiesta", because all classes and modules (and functions that begin with first uppercase letter) and became global once loaded.
Because of that, you can not have 2 parallel versions on the same lib, in the sane app, not possible. You load classes once, and they are accessible everywhere, magic! No need for loader headers as in JS, Python, Java, Go, ...
That said, ruby devs like to have "sane" extensions like to_json. People proffer to write "@object.to_h.to_json" instead of "JSON.parse(@object.to_h)" and not to think about loading json lib. I agree that Rails is abusing this too much.
I think you are right that the solution where you pass the array in is clearer and probably more maintainable. It doesn't seem object-oriented at all though. It also leads to a totally different look and feel between built-in and extensions. It's not idiomatic at all.
So, I think it's fair to say that the advantages you are lay out are more tangible than the other side of the trade-off.
Having written too much Ruby code I'd still cringe every time I have to pass the array.
Edit: To make it actually clean it should be 'MyArrayLib.has_my_things(array)'. Still really dislike it despite all the logical arguments for it.
>some_array.has_my_thing() is better than has_my_thing(some_array). ?
one of the reasons i prefer python to ruby. sorted(my_thing) will sort anything that can be sorted. no need for monkey patching -- you just call it and, if it can be sorted, it will be sorted.
In Ruby, anything that can be sorted will respond to sort. So thing.sort will also sort anything that can be sorted.
Python does the exact same thing, it just hides it from you using ugly __methods__ that the language itself will call by convention. What determines sortability? "Anything that can be sorted" just means anything that implements the __lt__ method. In Ruby, this method is called <=> and the Comparable module builds upon it.
__lt__ is just the way to overload the comparison operator, which of course is going to be called by the sort. What OP is saying is that there's no __sort__ or anything like that.
Sorted mandates the type you give it implements __iter__, aka that it is iterable. Ruby defines sort as a method on Enumerable.
The difference is that most likely that CPython and CRuby started with different capabilities to map native implementations into the language.
The biggest difference between the two is python adds more functions into the global namespace, while Ruby pushes pretty hard to encapsulate logic within types.
The discussion is about how to provide additional functionality.
To be sortable, you collection has to be a collection, and it has to have comparable elements. That has to be provided by the type itself.
The difference between Python and Ruby is how to add more functions. If you make a library to sort collections (or format phone numbers), the Ruby way is to monkey-patch collections (or numbers). Whereas the Python way is to offer a free function that takes an iterable (or number). This is what we are discussing.
No, pretty much everything in the language is hidden from you behind methods. This hidden magic is in many cases literally baked into the language's syntax.
Iterables use functions like __len__ and __next__. The with keyword is just a thing that implicitly calls __enter__ and __exit__ on a context management object.
Yes, sorted() will use methods like __iter__ and __lt__ provided by the object. But sorting is not provided by the object and is therefore not (quoting GP) "the exact same thing, just hidden from you in an ugly __method__".
There is nothing that sorts hidden in dunder methods on objects, even if they can be sorted. `sorted()` will use intrinsic methods like `__iter__` and `__lt__` to sort a collection, but the op is claiming that even though the syntax is different (`sorted(obj)` rather than `obj.sort!`), it's "the exact same thing, it just hides it from you using ugly __methods__".
To that I reply that no, there is no method to sort hidden in __methods__. This is not how functionality is added to classes in Python. If you want to add something that works on all iterables, you don't insert new methods in iterable classes, like you do in Ruby. That free-standing sorted() function really is a free-standing function, it is not actually a method "hidden in ugly __methods__".
> If you want to add something that works on all iterables, you don't insert new methods in iterable classes, like you do in Ruby.
You don't in Ruby, either.
If you want something that works on all Enumerables, you normally write an (instance or class/module) method on some other class or module that takes an argument that supports the methods provided by the Enumerable API.
(In Python, you might do this or write a bare function, but Ruby doesn't have bare functions; it's possible to make proc objects in a library which is the nearest equivalent, but you’d almost never do that, because module methods are much more convenient.)
In either Python or Ruby you might also monkey-patch existing classes to add functionality to the., and this is slightly more common in Ruby than Python, but because Python iterable, unlike Ruby Enumerables, don't have a common superclass that isn't shared with non-iterables, you couldn't easily do a “for all iterables” thing in Python [0] by monkey-patching because you don't have a good target to monkey-patch, whereas in Ruby you might do something for (nearly, because duck typing is a thing and it's possible someone could build an enumerable by implementing the whole API without mixing in the module) all enumerables, not by adding methods directly to individual enumerable classes, but by monkey-patching the Enumerable module itself.
The main difference here is that Python (despite having support for mixins because it uses multiple inheritance) tends in it's core to use a mix of OO public APIs and procedural public APIs built on top of OO protocols using (often dunder) methods, rather than either consistently-OO or consistently procedural APIs, whereas Ruby is consistently OO and uses mixins to provide shared functionality building on a narrower protocol where Python would use a suite of functions. (Python tends to be more OO in its stdlib and third party libs than in its core, except specifically when supporting protocols defined in the core and consumed through built-in functions.)
[0] except by monkey-patching object with code that tries to use the dunder methods that underlie the iteration protocol and fails if they don't exist, but while theoretically possible I don't think anyone would do that.
> one of the reasons i prefer python to ruby. sorted(my_thing) will sort anything that can be sorted. no need for monkey patching
sort() in ruby is declared on Enumerable. Sorted() in python is declared to take an iterable first parameter. They are pretty much equivalent, except python has built-in functionality to reverse the order of the output array, while ruby you would have to negate your sort function.
Ruby however allows types to override sort, such as when the underlying type is a red black tree which is already sorted. For duck typing, you can also declare a compatible sort method on types which do not implement Enumerable - doing so in python would require a separate method.
> Ruby convention is to add a ! when methods directly modify the object they're called on
No, Ruby convention is to add a “!” when there is a “less safe” version of an operation, for which a “more safe” version exists without a “!”.
Mutating versions of methods where a nonmutating version also exists are one common example, but not the only one, and mutating methods without a nonmutating counterpart don't get “!”.
Did they ever fix the arbitrary nature of monkey patching where last in wins? I've not touched ruby in years (and never seriously after I read up on how prevalent MP was).
Yes, they added refinements several years ago. That said I've been programming rails for probably 10 years, and I never see anyone use refinements, and I can count on one hand the number of times monkey patching has actually been a problem for me personally. I'm sure there are cases where it bit someone hard, but this is one of those things that a theoretically bad, but never seem to really have any impact in practice.
I have to maintain code that was written by a developer who has long since left the space. He monkeypatched in stuff like Hash#dig years before it landed in ruby with a slightly different API and blew up the codebase. Since he was a pretty clever developer and had some wide experience in other languages he would monkeypatch in useful stuff from other languages -- but by doing that collisions with the core language are actually pretty frequent. Many people have the same clever idea when its really just copying some pattern that was successful in some other language.
Awhile back I gutted the code and ripped out 60% of it, including all the monkeypatching and the maintanence burden on every ruby release dropped sharply.
(And I'm assuming we're talking about the more polite kind of Monkeypatching where you're adding methods to global classes that are net-new, not the practice of opening up a class and scribbling over methods that you didn't write and replacing behavior, where should you should always mental picture your SemVer contract being lit on fire)
Sort of. They introduced "refinements" as the thing you're supposed to do instead of monkeypatching, but AFAIK it doesn't get wide-spread usage.
IME you don't usually do monkey-patching, although there are circumstances where it makes A LOT of sense (usually test suites). It's one of those sharp knives - safe if you plan out using it, dangerous if you don't.
Rails is a DSL for building web apps. Ruby is a great language for building DSLs. Building up a DSL to meet your domain needs is a nice way to go. I don't think this is a problem at all, and very much in the spirit of both Ruby and Lisp (from which ruby takes a lot of inspiration).
I believe Ruby's open classes were also a reaction to the locked down libraries that were common to Java. They were impossible to extend and resulted in a rats nest of adapters, visitors, and every other pattern in the book. In Ruby the programmer is in charge, not the library authors. The ability to do 1.day.ago was not just cute, but revolutionary, and huge productivity boost for anyone coming from C, or Java, or C# - the predominate languages of the time.
ActiveSupport is one of my very favorite things in the Ruby ecosystem.
I can understand why it might not be to someone's taste, but I find the idioms to be generally delightful to use and, in most cases, extraordinarily convenient.
But, as always in the Ruby ecosystem, if you don't like it, find the thing that brings you joy!
> RSpec is our primary example here - a limited and problematic DSL based on monkey-patching was turned into a beautiful, composable DSL which we still have in RSpec.
One of my most consistent experiences with Ruby is doing something the "wrong way" - but being able to do, due the language flexibility - and then later coming back to the problem and realizing the better solution, which ends up being more satisfactory and happier in every way.
Ruby lets you do whatever you want, however you want, but it makes the right way the most satisfying. Very handy for iterative development.
Those are similar to C#'s Extension methods. That way consumers of the Refinement can opt-in to the extended functionality without injecting their methods globally into the class for every bit of code running on the VM.
There's a good reason that ActiveSupport as a "Monopoly on Monkeypatching" which is that nobody should be doing it any more. ActiveSupport largely gets a pass because it is pretty well defined and engineered, bits of it get pulled into core ruby, and its the 800# gorilla out there because it is rails.
You shouldn't copy that pattern, and you shouldn't want to. Use refinements instead.
I like the refinements mechanism too and it is most certainly underutilized, but the lexical scope makes it an incomplete substitute, and somewhat problematic during reflection.
I'm not an expert but I believe the lexical scoping was a last-minute save by Charlie Nutter to preserve performance! Dynamic scoping of method definitions is seriously hard to optimise, and I'm not sure anyone knows how to do it. Do you really need dynamic? Why isn't lexical enough?
To clarify, I don't need it, and I'd agree that any library interface using monkeypatching could in principle be redesigned to use refinements. What I'm suggesting is in practice, I suspect it's doubtful anyone would undertake the exercise of reframing ActiveSupport (and thereby the entire Rails ecosystem) to use refinements instead of core extensions.
Nobody should rewrite rails and activesupport and the entire ecosystem using refinements. It would be a massively breaking change for little purpose because activesupport is so well known to the core ruby team.
Nobody who is greenfielding code should copy the activesupport pattern, though, and you aren't losing anything by being told not do that, you're being shown a better way.
"and behind every framework, there's a langauge trying to get out, to protect semantic ideas, then syntax ones, and then support the logical ideas that we have" -- Lambda World 2019 - Language-Oriented Programming with Racket - Matthias Felleisen
You can make that argument if you consider a language is more than just syntax. It's an complicated ecosystem, of tooling (compilers, runtimes, repls, IDEs, debuggers), reusable code (libraries, frameworks), conventions (coding styles, idioms, design patterns), and community.
I'm not really fussed about dictionary definition debates anyway. I generally use ActiveSupport in personal Ruby projects. It just adds too much value to ignore.
As a former Rails dev, I've always enjoyed Piotr Solnica's writing. He does a great job of explaining what's wrong with the framework, in particular the flaws that aren't immediately obvious when you're starting out.
But these days the main thing I think of when I hear his name is that his blog used to be on a different domain, and he apparently let the old registration expire because someone else has bought it and now it points to a porn site. And tragically, there are still many other sites online which still link to Solnica's outdated, now-pornographic URLs.
And that's how I ended up accidentally sharing a porn link in a professional setting.
welp :( I'm very sorry about this. I didn't manage to pay for my old domain because I was recovering after an accident (true story, not making any silly excuses). I was aware that it's about to expire but wasn't in a good physical and mental shape and I just forgot to do it. People keep reporting it to me and we're updating URLs every time somebody finds an old link somewhere. I should probably just search through GitHub and send some PRs...
I totally agree with this. This situation where a library has a de facto monopoly on monkey patching is just weird an unfair.
It reminds me a bit of MS's good old EEE strategy. People get comfortable with ActiveSupport to a degree where they start disliking to not have it's small benefits. But the consequence of this behaviour, that most are oblivious to, is that it to a large degree puts Rails in control of the language.
ActiveSupport feels like a leftover from the old days of prototype and jQuery. You search for a solution and half the answers on SO will assume that you are using it (and many of the answer authors don't even know it)
I'm sure it's not going to happen, but I think Rails should stop using ActiveSupport internally. It would allow devs to decide for themselves if they want to pollute the standard library, and also open the door for competition.
While the syntactic sugar of "1.day.ago" is hard to match, I don't think an alternative have to be that bad "ActiveTime.days_ago(1)"
Back in the first era of the JavaScript library was this was one of the big differences between Prototype and jQuery.
Prototype added a whole bunch of new methods to the built-in JavaScript objects. It also defined $ as an alias for document.getElementById.
jQuery added nothing to the built-in objects, but did also use $ as an alias for the jQuery function.
Something I really liked about jQuery was that it included a noConflict() function, which you could use to avoid it interfering with Prototype. Doing this:
jQuery.noConflict();
Reset the $ back to whatever it was before jQuery was loaded.
If you wanted to keep using $ in your jQuery code you could use this elegant idiom, which registers your code to run when the page content has loaded:
jQuery(function($) {
// Within this block $ is an alias of jQuery
});
I think the author's example of time_calc compared to ActiveSupport's 1.days.ago syntax speaks to why ActiveSupport is used so much and that sometimes it's OK to do the wrong thing to make everything else easier. The time_calc API looks just awful to use I'd rather deal with the implications of potential Ruby "magic".
I think this is the beauty of Ruby. The core philosophy of Ruby is that it was designed to make "1.day.ago" possible. That's why DHH fell in love with it and used it to build Rails. You can't do that with many other languages.
"1.day.ago" is not really a piece of code, it's a sensibly named shortcut to existing code that already solves easy problems. It's is not designed to be built upon; not all code has to be designed that way.
For example:
// How to get last day of month?
// Javascript
var month = 0; // January
var d = new Date(2008, month + 1, 0);
console.log(d.toString()); // last day in January
// monkey patching in ActiveSupport
Date.today.end_of_month
This is the definition of a great DSL. It abstracts away easy problems so you don't have to solve them hundreds of times and allows you to focus on the more unique problems of a domain.
Monkey patching is a powerful tool and just like any powerful tool it should be used carefully. Random monkey patching everywhere is bad, just like polluting the global namespace in javascript is bad. But used well and you can create extremely expressive, powerful, and concise coding experiences. Isn't that the goal of any DSL?
This is no longer a testimony of Ruby's strength, or rather this highlights Ruby's age and stagnation.
D, Go, C#, Rust, Swift, Kotlin, Scala, Haskell, and (more I'm forgetting) already have this without the hack of monkey patching.
If you really find beauty in DSL's you should look into tagless final encoding in Haskell.
From what I recall Ruby still doesn't have first-class ADT's. Meanwhile Java will be getting sealed classes (the OO orthogonal to ADTs) with pattern matching.
Not to overstate the point, but what many Rubyists want in programming is to mirror their code to a subset of English as much as possible, avoiding the more ambiguous parts of English. This is because if it makes sense in English, it is self documenting, clear, and intuitive.
By DSL, I meant an abstract language on top of an existing programming language that serves a domain well and Ruby's monkey patching makes that easy to implement.
Haskell's language, with it's custom non English symbols, and functional language with verb object or even operator at the end style... it's like dvorak vs qwerty. Even if it is superior, it's not the preferred one.
Think that should be possible in Kotlin as well, fwiw. Extension functions and extension properties are nice. And function with receivers make for some nice DSLs. All without monkey patching, which I consider a better way.
I love Ruby and until I started to earn money with it as in a normal job I refused to touch rails. It felt wrong as someone used to pure ruby, Sinatra, and the whole syntax philosophy behind it.
I had the luck to work with one of the Rails Devs in my work place and really learned to love it, the thing that makes rails great really is the structure.
Taking over a 10 year old rails project written by a newbie? Well not to big of a problem. Taking over the same project by someone who knows rails is always a pleasure. Like you know exactly where a bit of code would be found, it doesn't feel like taking over legacy depts but like a fun new adventure to explore.
As web dev who used to work with PHP before that was mind blowing. In the PHP world every bit of code was different enough that everything 'legacy' was something nobody wanted to work with.
It's less about the dialect but the smart structure and MVC
Rust's traits let you add methods like this to any type without creating a risk of conflicts, because they aren't accessible within a particular file unless you import the trait. (Also I think there's some syntax for disambiguating?)
The last time I offered a Rails PR that extended core classes, it got a thumbs down review on that basis alone.
Solnic's complaint is often voiced, and has merit for all the reasons given (and more besides IMO), and Rails is certainly stuck with ActiveSupport, but the core team does not seem keen on exacerbating the problem.
This type of shenanigans always rubbed me the wrong way. Like, are we saying that "1" has a property, "day"? Conceptually that makes no sense, right? And "day" having a property "ago" is bogus as well.
So then, is it just a cute "let's make it read like a sentence" trick?
One time a colleague was trying to tell me about this trick, and I opened up a ruby interpreter to see what he was talking about. I typed in "1.day.ago" and pressed enter and it gave some error message. A day later he came back and told me how he was surprised to learn that the trick only worked if you had ActiveRecord in the mix.
To me, that's all downside. Sneaky and confusing that loading an ORM adds magic properties to your _numbers_. And, the properties don't even make sense.
The worst part of it all is: how in God's name are you supposed to find out where these magic properties and methods sprinkled all up into everywhere -- where did they come from, so you might read their docs, or read their source? It's madness!
Sure, the terseness is nice, and it is possible to learn this interface and make sense of it, but to me it still seems like abusing OOP paradigm in a way that conflicts with reality.
Abstract of any programming language, the concept of "2" has the properties that it is an integer, that it is a natural number, that it follows "1", that it is prime, etc. But it doesn't have a property "years".
From another perspective, back to programming, if "years" and "step" make sense as properties of "2" then does _any noun_ make sense? Is it cool for every library to monkey patch its own domain's worth of nouns to be properties of every number? 2.centimeters, 2.users, 2.widgets?
I can get with natural expressions for date math, but how about something more like
Time("2 years from now")
Then I can read that to mean we are constructing a time, described by the parameter.
This is because in Ruby everything* is an object. I agree that from outside Ruby this might look weird, but knowing Ruby this is very simple to understand.
Knowing this you can also expect the following things to work in Rails I think:
1.day.ago + 2.hours
This is because:
+ is an instance method defined on the Integer class. See Integer.instance_methods. So it is more like `1.day.ago.+(2.hours)`
and I expect then that in Rails (or some gem) they added `hours` as a method to the Integer or a compatible object
I understand how it all works mechanically, I just don't think it models the real world conceptually. OOP works well when you make classes to represent fundamental nouns, those nouns with properties describing their existential makeup. But day.ago is not that -- it's just abusing the syntax to fake a natural looking expression.
On finding out where a method came from, I appreciate that I can look to see if it's core or else must be from a gem, but I want to know _which_ gem did it come from.
I don't try to convince you to like Ruby, but I don't really understand how you can know in other languages if something comes from a plugin or not by simply looking at it and not knowing the std library or using an IDE.
Maybe in the case of a language with a very small standard library, this could be easy: as the best assumption could be that it is in a plugin/module. But I think looking at a piece of code and direct knowing from _which_ plugin some methods comes from has more to do with naming conventions than with the language itself, especially in languages where either overriding or overloading or a form of them is possible.
In the case of Ruby, I recommend RubyMine and just to go to definition. 99% of cases it will correctly open the definition from the gem of std library.
I agree that in Ruby - maybe more than in other languages - one can really make some behaviors hard to discover. But most of the libraries are straight forward and Rails is very well documented in code. I usually prefer to just go to the definition and read the method than browse the API docs.
Why there always have to be somebody telling you arbitrarily what's right or wrong even for such a harmless and creative things such are a programming languages or concepts and why i always open articles like that even that i know how bad it makes me feel.
I really wish i will finally remember this next time and i will spend the time coding hapily instead, sometimes with ActiveSupport, sometimes not, maybe even with Dry-whatever, if i'm able to overcome Solnic's negative agendas.
What makes you think I have negative agendas? My "agenda" is quite positive, I want a Ruby ecosystem where we play by the same rules and Rails is no longer a special snowflake. This way we can evolve faster.
Your articles are very negative. Basically everything involving Rails is bad and wrong by them. That's what i mean.
I want Ruby ecosystem, where everybody is free to do their things the way they want, by the rules they want, for whatever motivations they have. I want to have my special Rails snowflake and i don't want it to ever change because it constantly enables me to build amazing things AND enjoy the process also. And i'm willing to let you have your way and i will respect it but only if you can do the same without being negative about it in every other statement.
If somebody chooses to support Rails and Rails only in their library i think it's very fine and i think the same if there is a library doing the same thing for only Hanami. Or a library supporting both. Or a library not really supporting anything in a special way. I also think it's fine when there is a library which sole function is to monkey patch Object with #forty_two method. And fine it is because there are no rules from you or anybody else forcing me to actually use this thing. So, i will just not use it, right?
So for me this open and free "we" is the only "we" there is and also the only one i want to evolve anyhow.
> Your articles are very negative. Basically everything involving Rails is bad and wrong by them. That's what i mean.
I provide critique that some (probably many) Ruby developers don't like and disagree with but my intention isn't to bash something and make it "all bad". I don't recall saying that everything involving Rails is bad and wrong. I had conference talks sharing my appreciation for Rails and various parts of its philosophy that I agree with and try to incorporate in my own projects. There are also things I don't like that are very problematic and I share this with the community as well.
> I want Ruby ecosystem, where everybody is free to do their things the way they want, by the rules they want, for whatever motivations they have.
Imagine a ruby gem that becomes super popular because it provides some neat features that plenty of companies can benefit from. Then, for whatever reason, this project introduces some great performance optimizations but they are achieved through monkeypatches. Unfortunately it turns out it breaks Rails in some weird, subtle ways, that are hard to debug. Rails core teams starts getting lots of bug reports about some seemingly unrelated problems, just to realize that it's not a problem in Rails, it's a problem in another library.
Does this make any sense? Nope. That's why people don't monkeypatch in their libraries.
From what I gathered, there's some initiative to move Rails to use Refinements via ActiveSupport, if this happens, then the problem is solved.
> And i'm willing to let you have your way and i will respect it but only if you can do the same without being negative about it in every other statement.
It's interesting that you mention respect here - ActiveSupport doesn't respect the rest of the ecosystem. By simply adding new methods to Kernel, Object, NilClass, it reserves them and you really want to avoid having same methods in your library. If you want to tell me "but nothing prevents you from doing this" then you're right, except that I have to think about this because if my lib doesn't work with Rails well, I might as well not build it in the first place because adoption would be non-existant.
> I provide critique that some (probably many) Ruby developers don't like
Yes, maybe that is the thing, i don't like critics. The approach i prefer is to put your money where your mouth is. Why don't you to build this version of ActiveRecord using refinements? Then let others see how good it is
> Imagine a ruby gem that ... Does this make any sense? Nope. That's why people don't monkeypatch in their libraries.
This can happen any day because of infinite types of mistakes a programmer can make. Monkey patching is just one technique you have at your disposal and as with everything else, you have to know when to use it, how and why.
> It's interesting that you mention respect here - ActiveSupport doesn't respect ...
What do you mean doesn't respect? ActiveSupport is a tool without feelings and stances and believes. You can either use it or not use it. And actually i want ActiveSupport to do exactly what it does, patch all the things and give me tons of nice methods i need.
Yeah, so if you want your library to work with Rails, because *you want* Rails users adopt your lib, you have to make some effort right? Well that's how it works with anything. If you don't like it for whatever reason why don't you find some place where they do it exactly your way, or why don't you build it? I mean i like the way how Rails and the other things around work and that's why i use it. So if you want adoptions for your library from me, you have to kind of a do it the same way.
Prototype modification of global objects in JS provides similar monkey patching capability. Interestingly evolution is very different though
The instances of abuse are generally less in mainstream JS libraries, and in many of them such features are usually opt-in or at-least it is possible to still use the lib even when opting out , there is also no major dialect that has privileged rights so to speak.
I've felt a lot of the same frustrations on iOS projects with swift. Lots of people add tons of extensions and helpers, and folks learning on these codebases don't learn iOS, they learn the playground that the developers of the project made, it's not obvious what's built in and what's custom. The extensions are less composable, harder to navigate, and often end up really ramping up complexity when you end up with constraints and generics and protocols and all kinds of really fancy "nerd stuff" that really isn't needed in an iOS codebase. I used to have a theory that folks did this because, well, Apple has basically "solved" what make an app is. Everything you need is there, it's all pretty easy to use, it's "boring" in a good way. Maybe this is how developers scratch the itch of needing to do a lot of "real programming"? Maybe I'm just getting old!
Rather than rails not being written in ruby, I would say that rails extends ruby. Rails is written in ruby, but rails isn't used by ruby - your rails (-using) project is implemented in the language of rails.
And it's mostly fine, but the cavalier attitude to meta programming and lack of namespacing is a problem. And not just convenient (but ultimately insane) stuff like 1.second.ago - but also in core rails. On a current project interfacing with a legacy database, I've encountered tables with columns named "table_name" and "changes" - both of which are shadowed by ActiveRecord itself.
But even with the crazy, it does make starting a project right, quite easy. Tests are set up, and with rails 7 you can finally opt out of most of the crazy js/css tooling and still have a nice front and backend stack.
Although in principle the author is probably correct, what I don't understand is the value of his argument. Instead of saying ActiveSupport is bad, use native Ruby, why not take all of that time, energy, and more importantly understanding of the ecosystem and make AS better.
When giving directions, we don't say: "please proceed forward for 17.84 ft, while reducing your speed, come to a complete stop, and then gently accelerate while turning 90 degree." We just say: "go to the corner and turn right."
Certain abstractions and improvements to the way we communicate make a lot of sense. Why fight it? Sounds like the author has the perfect mix of skills to improve the core of Ruby to a point where it's useful by the rest of us who don't know and don't have the time.
p.s. Thanks to the author for pointing out how much non-core stuff there's in AS. Forced me to go read the docs, to find out how much awesome stuff I am not using, but should!
JavaScript supports monkey patching by modifying prototype objects too. But it is considered to be bad practice because it modify the behavior globally, and some method name conflict may cause trouble. An example is Array.prototype.contains couldn't be added because it will break another library, so it is renamed to Array.prototype.includes. (Source: https://github.com/tc39/proposal-Array.prototype.includes)
Rails itself depending on ActiveSupport, that's the problem. If users were to be able to use ActiveSupport or not, depending on their preferences all would be well.
Imho there isn't a good reason for Rails to depend on the monkey patched ActiveSupport parts and it could be factored out into less magicky functions inside Rails.
There's a third option: explicit opt-in of extensions, with better support for finding where .whatever() is defined. Gets you ergonomic helpers where they're beneficial, scoped to just the modules you pull them into. Too late for Ruby but this is how Rust traits work right?
Just wanted to say that the authors libraries, especially ROM [0], are incredible and have played a huge influence on me as a developer. I learned a lot using and diving through the code.
It would be one thing if Rails was the only gem that depended on ActiveSupport. The problem is that other authors use it because they initially expect their library to be used with rails. And then, if someone chooses to use the library without rails, all of ActiveSupport must be brought in as a dependency (which is ugly).
I think DHH's aesthetic intention with ActiveSupport was for it to be used by Rails, not used by a random assortment of gems throughout the Ruby ecosystem and misunderstood by a lot of people.
FWIW, ActiveSupport is one of the main reasons why I stopped using Rails a while ago. The other was that version updates typically have a LOT of API changes that make staying up to date quite a drag (or source of tech debt).
Not sure this is what you're getting at. I agree with the author though, this aspect of Rails bothered me quite a bit. Knowing that working on a Rails project meant I could magically generate ranges from dates because of ActiveSupport was unsettling - it made me wonder what else was non-native behaviour and waiting to trip me.
I just can’t bring myself to get worked up about it - I’ve done a lot of Rails, and I’ve done a lot of pure Ruby. You quite quickly learn which bits come from ActiveSupport when you step outside Rails because… it doesn’t work. It’s not going to trip you up in 8 month’s time after shipping your code, you’re just going to get an exception when you first try to run it, and then either implement it the pure way or add a dependency on ActiveSupport.
Those are good points. I should say it's not really ActiveSupport that concerned me the most so much as the possibility of other people on my team monkey patching stuff in a similar way, but with much lower quality code. It's almost like all the common dangers of bad programming exist, plus a bunch more at a lower level.
I totally recognize that it's also a strength of ruby. I really enjoyed working with it. It's super expressive and clear to read. ActiveSupport is all really well documented and designed too, and I'm not trying to criticize it.
After re-reading my comment I'm noticing I was specifically targeting Rails as well. I think if anything Rails does a great job and shows why this is a cool thing about ruby. I think what I should have said is that this aspect of ruby is what I found off-putting and specifically in a context where I'm relying on a vast number of dependencies, coworkers of various skill levels, and shipping code to customers. Does that make more sense? I didn't express what I was thinking very well before.
There's nothing magic about it. It is just ruby. And no need to wonder what else is there. It is all documented. All the methods are well named. It is hard to see how they could trip you up. I've been programming for a long time now. I remember when people would read the documentation for the libraries and frameworks they were using. Seems to have fallen out of fashion.
I read the docs, and I thought it was all well designed too – the issue I took with it is that Ruby is so incredibly dynamic that it looked like a giant foot gun to me. Well, potentially at least. I know it's powerful in the right hands.
I'm a lot happier with something like Rust, and it arguably comes down to personal preference. Ruby is awesome and fun to write. I'm not a good enough programmer to make it as productive as others in the long term though, especially when working on a large team.
But as a counter-example to that, I'm constantly amazed that there are a ton of Ruby gems out there which pretty much work as advertised outside of Rails (even if Rails gets a privileged happy path during config). Not to mention you can use ecosystem stalwarts like Puma, Rack, Sidekiq, and many other subsystems outside of a Rails app entirely. Heck, you can pull just bits of Rails in if you really need them. Want Active Record for your ORM but nothing else? That's totally possible! (But of course then you get a bunch of Active Support stuff too which is what the author objects to.)
Personally I love Active Support and actively (heh) bring it into my Ruby projects if it's not there already. To a certain degree, everything people say is a "bug" about Ruby's metaprogramming/monkeypatching is a feature in my book. The fact that "Ruby core" is simply a substrate upon which you can build your own flavor of a Ruby-plus language—be that "The Rails Way" or something else entirely—is amazing. In that respect, I entirely disagree that Rails is not written in Ruby. Rails shows us how you _can_ (and probably should!) use Ruby to build DSLs which suit your specific application/framework purposes very well. It's not a bug. It's a feature. And it's why most other tools which claim "Rails but for Language X" fall short…they can try to replicate features of Rails, sure—but because it's not Ruby, it misses the whole point of using Rails, which is that you get Ruby "for free" to sweeten the deal! That's the (not so) secret sauce here.