1. 21

According to Ruby tradition, the core team will release Ruby 3.4 on December 25—but the preview has been out since May. We did a quick writeup of what’s new in the upcoming release.

  1.  

    1. 5

      One thing that I found interesting is that many changes in each Ruby release would be considered a big no for any other language (for example the “Keyword splatting nil” change) since the possibility of breaking existing code is huge, but Ruby community seems to just embrace those changes.

      I always think about the transition between Python 2 and 3 that the major change was adopting UTF-8 and everyone lost their minds thanks to the breakage, but Ruby did a similar migration in the version 2.0 and I don’t remember anyone complaining.

      I am not sure if this is just because the community is smaller, if the developers of Ruby are just better in deprecating features, or something else. But I still find interesting.

      1. 7

        Ruby’s version of the Python 2 to 3 experience (by my memory) came years earlier, going from 1.8 to 1.9. It certainly still wasn’t as big of an issue as Python’s long-lingering legacy version, but it was (again, my perception at the time) the Ruby version that had the most lag in adoption.

        1. 6

          Yes, and it was very well managed. For example, some changes were deliberately chosen in a way that you had to take care, but you could relatively easy write Ruby 1.8/1.9 code that worked on both systems.

          The other part is that Ruby 1.8 got a final release that implemented as much as the stdlib of 1.9 as possible. Other breaking things, like the default file encoding and so on where gradually introduced. A new Ruby version is always some work, but not too terrible. It was always very user centric.

          It was still a chore, but the MRI team was pretty active at making it less of a chore and getting important community members on board to spread knowledge and calm the waves.

          Honestly, I think Ruby is not getting enough cred for its change management. I wish Python had learned from it, the mess of 2 vs 3 could have been averted.

          1. 3

            Yep, that’s my take too. IIRC 1.9 had a number of breaking API changes which were really low value. For instance, File.exists? -> File.exist?

            1. 3

              File.exists? started emitting deprecation warnings in Ruby 2.1 (2013) and was finally removed in Ruby 3.2 (2022)

              1. 1

                I guess IDRC!

          2. 6

            I feel like Python was pretty deeply ingrained in a bunch of operating systems and scripts that was excruciating to update.

            Ruby is mostly run as web apps

            1. 4

              Interesting POV. As a long-time Rubyist, I’ve often felt that Ruby-core was too concerned with backwards compatibility. For instance, I would have preferred a more aggressive attempt to minimize the C extension API in order to make more performance improvements via JIT. I’m happy to see them move down the path of frozen strings by default.

              1. 3

                One thing that I found interesting is that many changes in each Ruby release would be considered a big no for any other language (for example the “Keyword splatting nil” change) since the possibility of breaking existing code is huge, but Ruby community seems to just embrace those changes.

                Like others already said, the Ruby core team stance is almost exactly the opposite: it is extremely concerned with backward compatibility and not breaking existing code (to the extent that during discussion of many changes, some of the core team members run grep through the codebase of all existing gems to confirm or refute an assumption of the required change scale).

                As an example, the string literal freezing was discussed for many years, attempted before Ruby 3.0, was considered too big a change (despite the major version change); only pragma for opt-in was introduced, and now the deprecation is introduced in the assumption that the existence of pragma prepared most of the codebases for the future changes. This assumption was recently challenged, though, and the discussion is still ongoing.

                Keyword splatting nil change might break only the code that relies on the impossibility of the nil splatting, which is quite a stretch (and the one that is considered acceptable in order to make any progress).

                1. 1

                  Keyword splatting nil change might break only the code that relies on the impossibility of the nil splatting, which is quite a stretch (and the one that is considered acceptable in order to make any progress).

                  This seems like really easy code to write and accidentally rely on.

                  def does_stuff(argument):
                      output = do_it(argument)
                      run_output(output)  # now `output` might be `{}`
                      rescue StandardError => e
                          handle(e)
                  end
                  
                  def do_it(arg)
                      splats(arg)
                  end
                  

                  If nil was expected but was just rolled up into the general error handling, this code feels very easy to write.

                  1. 1

                    Well… it is relatively easy to write, yes, but in practice, this exact approach (blanket error catching as a normal flow instead of checking the argument) is relatively rare—and would rather be a part of an “unhappy” path, i.e., “something is broken here anyway” :)

                    But I see the point from which this change might be considered too brazen. It had never come out during the discussion of the feature. (And it was done in the most localized way: instead of defining nil.to_hash—which might’ve been behaving unexpectedly in some other contexts—it is just a support for **nil on its own.)

                    1. 1

                      is relatively rare

                      I have to doubt that. It’s extremely common in Python, for example, to catch ‘Exception’ and I know myself when writing Ruby I’ve caught StandardError.

                      I don’t have strong opinions.

                      1. 1

                        I don’t mean catching StandardError is rare, I mean the whole combination of circumstances that will lead to “nil was frequently splatted there and caught by rescue, and now it is not raising, and the resulting code is not producing an exception that would be caught by rescue anyway, but is broken in a different way”.

                        But we’ll see.

                  2. 1

                    Like others already said, the Ruby core team stance is almost exactly the opposite: it is extremely concerned with backward compatibility and not breaking existing code (to the extent that during discussion of many changes, some of the core team members run grep through the codebase of all existing gems to confirm or refute an assumption of the required change scale).

                    But this doesn’t really matter, because there are always huge proprietary codebases that are affected for every change and you can’t run grep on them for obvious purposes. And those are the people that generally complain the most about those breaking changes.

                    1. 2

                      Well, it matters in a way that the set of code from all existing gems covers a high doze of possible approaches and views on how Ruby code might be written. Though, of course, it doesn’t exclude some “fringe” approaches that never see the light outside the corporation dangeons.

                      So, well… From inside the community, the core team’s stance feels like pretty cautious/conservative, but I believe it might not seem so comparing to other communities.

                      1. 1

                        It doesn’t seem anything special really. Of course Python 2 to 3 was a much bigger change (since they decided “oh, we are going to do breaking changes anyway, let’s fix all those small things that were bothering us for a while”), but at the tail end of the migration most of the hold ups were random scripts written by a Ph. D. trying to run some experiments. I think if anything, it does seem to me that big corporations were one of the biggest pushers for Python 3 once it became clear that Python 2 was going to go EOL.

                  3. 2

                    I’d say that the keyword splatting nil change is probably not as breaking as the frozen string literal or even the it change (though I do not know the implementation details of the latter, so it might not be as breaking as I think). And for frozen string literals, they’ve been trying to make it happen for years now. It was scheduled to be the default in 3 and was put off for 4 whole years because they didn’t want to break existing code.

                    1. 2

                      Over the years I feel like Ruby shops have been dedicated to keeping the code tidy and up-to-date. Every Ruby shop I’ve been at has had linting fail the build. Rubocop (probably the main linter now) is often coming out with rule adjustments, and often they have an autocorrect as well making it very easy to update the code. These days I just write the code and rubocop formats and maybe adjusts a few lines, I don’t mind.

                      1. 2

                        I always think about the transition between Python 2 and 3 that the major change was adopting UTF-8 and everyone lost their minds thanks to the breakage, but Ruby did a similar migration in the version 2.0 and I don’t remember anyone complaining.

                        From what I remember, UTF-8 itself wasn’t the problem— most code was essentially compatible with it. The problem was that in Python 2 you marked unicode literals with u"a u prefix", and Python 3 made that a syntax error. This meant a lot of safe Python 2 code had to be made unsafe in Python 2 in order to run in Python 3. Python 3.3 added unicode literals just to make migrations possible.

                        On top of that, Python 3 had a lot of other breaking changes, like making print() a function and changing the type signatures of many of the list functions.

                        1. 4

                          As someone who was maintaining a python package and had to make it compatible with 2 and 3, it was a nightmare. For instance the try/except syntax changed.

                          Python 2

                          try:
                            something
                          except ErrorClass, error:
                            pass
                          

                          Python 3

                          try:
                            something
                          except ErrorClass as error:
                            pass
                          

                          Basically the same thing but both are syntax error in the other version, that was a nightmare to handle. You can argue the version 3 is more consistent with other construct but it’s hard to believe it would have been particularly hard to support both syntax for a while to ease the transition.

                          Ruby change way more things, but try its best to support old and new code for a while to allow a smooth transition. It’s still work to keep up, but it’s smoothed out over time making it acceptable to most users.

                          1. 2

                            It’s been a while, and I was just starting out with Python at the time, so take this with a grain of salt, but I think the problem was deeper than that. Python 2’s unicode handling worked differently to Python 3, so even when Python 3 added unicode literals, that didn’t solve the problem because the two string types would still behave differently enough that you’d run into compatibility issues. Certainly I remember reading lots of advice to just ignore the unicode literal prefix because it made things harder than before.

                            Googling a bit, I think this was because of encoding issues — in Python 2, you could just wrap things in unicode() and the right thing would probably happen, but in Python 3 you had to be more explicit about the encoding when using files and things. But it’s thankfully been a while since I needed to worry about any of this!

                            1. 1

                              My recollection at Dropbox was that UTF8 was the problem and the solution was basically to use mypy everywhere so that the code could differentiate between utf8 vs nonutf8 strings.

                              1. 1

                                In my experience the core issue was unicode strings and removing implicit encoding / decoding was, as well as updating a bunch of APIs to try and clean things up (not always successfully). This was full of runtime edge cases as it’s essentially all dynamic behaviour.

                                Properly doing external IO was on some concern but IME pretty minor.

                              2. 1

                                On top of that, Python 3 had a lot of other breaking changes, like making print() a function and changing the type signatures of many of the list functions.

                                This is why I said the “major” change was UTF-8. I remember lots of changes were trivial (like making print a function, you could run 2to3 and it would mostly fix it except for a few corner cases).

                                1. 2

                                  To me, the big problem wasn’t so much to convert code from 2 to 3, but to make code run of both. So many of the “trivial” syntax changes were actually very challenging to make work on both versions with the same codebase.

                                  1. 1

                                    It was a challenge early on, after ~3.3 it was mostly a question of having a few compatibility shims (some very cursed, e.g. if you used exec) and a bunch of lints to prevent incompatible constructs.

                                    The string model change and APIs moving around both physically and semantically were the big ticket which kept lingering, and 2to3 (and later modernize/ futurize) did basically nothing to help there.

                              3. 2

                                It wasn’t an easy transition. As others said, you’re referring to the 1.8-1.9 migration. It was a hard migration. It took around 6-7 years. An entirely.new VM was developed. It took several releases until.there was a safe 1.9 to migrate to, which was 1.9.3 . Before that, there were memory leaks, random segfaults, and one learned to avoid APIs which caused them. Because of this, a big chunk of the community didn’t even try 1.9 for years. It was going so poorly that github maintained a fork called “ruby enterprise edition”, 1.8 with a few GC enhancements.

                                In the end , the migration was successful. That’s because, once it stabilised, 1.9 was significantly faster than 1.8 , which offset the incompatibilities. That’s why python migration failed for so long: all work and no carrot. For years, python 3 was same order of performance or worse than python 2. That only changed around 3.5 or 3.6 .

                                Fwiw the ruby core team learned to never do that again, and ruby upgrades since 1.9 are fairly uneventful.

                                1. 2

                                  Minor correction: Ruby Enterprise Edition was maintained by Phusion (who did Passenger), not GitHub.

                                2. 1

                                  Ruby 2 was a serious pain for many large projects. Mainly with extensions behaving slightly differently with the encoding. I remember being stuck on custom builds of 1.9 for ages at work.