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.
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.
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...
"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
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.
>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.
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).
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.
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)
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.
> 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 “!”.
> 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.
> 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.
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)
> 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.)
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.
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!
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.
> 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.)
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.
> 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.
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.
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!
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.
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?)
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?
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.
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.
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".
> 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.
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?
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.
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.
> 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.
> 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.
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.
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.
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.
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).
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)"
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.
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.
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
});
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)
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.
184 comments
[ 2.2 ms ] story [ 160 ms ] threadNot 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 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.
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.
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.
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.
https://youtu.be/z8Pz4bJV3Tk?t=180
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.
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.
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.
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)
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.
You can add iterators with __iter__, reversed() calls __reversed__, etc.
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.
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.
Of course Python classes have methods.
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.
The discussion has mostly been about instance methods defined in classes, and how do you define “like sort” in this claim?
list.sort() is a method in Python; it's the equivalent of Array#sort! in Ruby.
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__".
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.
Your point about Ruby syntax not really being meant for free functions is well-taken though.
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 “!”.
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.
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.
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.
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.
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.
I just don’t understand why Python thinks it necessary to define global functions for something that naturally should exist as a method.
And, of course, there’s no way to know up front when to use a global function vs when to use a method.
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__.
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.)
Python does have such a mixing, it's collections.abc.Sequence https://docs.python.org/3/library/collections.abc.html#colle...
I don't use either Ruby or Python but I think that the first way is more natural.
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.
some_array.do_thing().do_another_thing().do_something_else()
versus
do_something_else(do_another_thing(do_thing(some_array)))
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.
(do_thing . do_another_thing . do_something_else) some_array
Are we talking about Ruby? Because it does.
It has both:
And: (Where the things in the composition are procs/lambdas, the closest thing Ruby has to functions as distinct from methods.)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.
Which, believe it or not, they still teach function composition as good old g(f(x)) in high school algebra...
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.
some_array |> do_thing() |> do_another_thing() |> do_something_else()
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.
See also:
https://mamememo.blogspot.com/2019/06/a-brief-history-of-pip...
https://mametter.hatenablog.com/entry/2019/06/15/192311
``` 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.
a.this().that() requires that a.this() returns an object on which .that() can be invoked.
(that (this a)) requires that (this a) returns an object that is a suitable argument to that.
"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.
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.
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).
And because of easy macros (another advantage of the syntax), you can easily add syntactic sugar as you see fit where you care.
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".
Isn't the D solution actually that:
is syntactic sugar for: if A.B() is not specifically defined.(Yours both flips the elements and reverse the direction of sugaring, as I understand it.)
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.
Ruby syntax apparently is especially nice if you know Japanese: https://flatironschool.com/blog/ruby-and-the-japanese-langua...
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.)
How do code completion work in languages where the predicate goes before the subject?
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.
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.
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.
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).
Ruby has composition operators in both directions:
given:
the following are all equivalent: 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.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.
They’ve been among the methods for invoking procs since at least Ruby 1.8 (2003); there are several – all of these are equivalent.
> I’d like to write one of: 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.
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.)
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.
with the very latest ruby version, you actually can:
https://rubyreferences.github.io/rubychanges/3.1.html#kernel...
Well, you always could, it was just a bit more involved:
https://github.com/lloeki/pak
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.
Very often, when I'm learning new code, there's one line I'll write over an over in console bindings:
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.For example,
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.It's this great ease of introspection that makes Ruby so fun.
For some
is not better than If you're an "artist" type that care about the aesthetics of your code you tend to prefer the second.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!
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.
Or you can cast &object to &Trait.
"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?
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.
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.
``` using Dates julia> lastdayofmonth(today()) 2022-02-28 ```
But that's function oriented programming for you.
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.
"Dialect" implies that it's a sub-language, but Ruby is still Ruby no matter how many methods you add or modify.
Whereas a "Jargon" is a specialized vocabulary, and even sometimes some word change meaning as part of a jargon.
But I suppose that when you title your post like this, controversy is actually what you are looking for.
It's that which (the author argues) makes it a dialect, rather than a specific jargon.
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 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?
https://rubygems.org/stats
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.
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.
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.
Do a lot of gems do that? It's never something I've encountered when looking for gems to use in my Sinatra, command-line, etc applications.
It's odd though, how unreadable Python code can end up being if you don't bother caring about how you write code.
I don't think it's a coincidence that we got _why, DHH, pragmatic programming and the like as soon as Rails put Ruby on the map back in the mid 2000s.
It has a lot of Smalltalk in it in a way that is easy to step into from another language.
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.
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).
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)"
https://docs.ruby-lang.org/en/3.0/doc/syntax/refinements_rdo...
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.
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.
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.
Additions to ext modules are, consequently, rare, and in fact there's a trend going the other way, e.g. https://github.com/rails/rails/commit/33ca3923170a50da7f1731...
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:
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:
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.
In the US, it’s like trying to talk to someone from the deep south versus Maine.
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.