Posts

  • Weird Ruby: Fun with String#split

    String#split is a pretty-well known and commonly used method. Still, its behaviour in some cases might surprise you:

    irb(main):001:0> 'foo.bar'.split('.')
    => ["foo", "bar"]
    irb(main):002:0> '...'.split('.')
    => []
    irb(main):003:0> 'foo...'.split('.')
    => ["foo"]
    irb(main):004:0> 'foo'.split('.')
    => ["foo"]
    irb(main):005:0> ''.split('.')
    => []
    

    No comment needed. Keep Ruby weird!

  • Weird Ruby: Nil Conversions

    Most Rubyists probably know that nil in Ruby is an instance of the singleton class NilClass. Because nil is a object like any other, we can actually invoke methods on it, and it turns out that NilClass provides a few - mostly conversion methods to other Ruby types. Let’s see some of those in action:

    irb(main):001:0> nil.to_h
    => {}
    irb(main):002:0> nil.to_a
    => []
    irb(main):003:0> nil.to_s
    => ""
    irb(main):004:0> nil.to_f
    => 0.0
    irb(main):005:0> nil.to_i
    => 0
    irb(main):006:0> nil.to_c
    => (0+0i)
    

    With those methods you can literally create something (e.g. an empty hash) out of nothing! Whether you should actually be doing this is a totally different matter, though. According to the class docs:

    The converter methods are carrying the concept of nullity to other classes.

    I’m not sure how this “concept of nullity” is defined, but I’ve always found it weird that you can equate the absence of value with some anchor value (e.g. an empty collection or 0). I know there are contexts in which this may be useful, but I’m fairly certain you’d be able to get similar results without resorting to such conversions.

    Keep in mind that you can do similar conversions in a different way:

    irb(main):001:0> Array(nil)
    => []
    irb(main):002:0> Hash(nil)
    => {}
    irb(main):003:0> Integer(nil)
    # `Integer': can't convert nil into Integer (TypeError)
    irb(main):004:0> Float(nil)
    # `Float': can't convert nil into Float (TypeError)
    

    Notice how the “safe” number conversion methods Integer and Float operate differently and raise an exception when passed nil. That’s part of the reason why it’s often suggested to avoid the use of to_i and to_f to avoid converting unexpected values.

    The Zen of Python famously says:

    Explicit is better than implicit.

    Errors should never pass silently.

    In the face of ambiguity, refuse the temptation to guess.

    But in the land of Ruby it seems we like to live dangerously!

    That’s all I have for you today. Keep Ruby weird!

  • nREPL 1.1.1: Improved Completion with compliment-lite

    Today I’ve released nREPL 1.1.1 with a couple of small bug-fixes and one more notable, if mostly invisible change.

    Historically nREPL’s completions op (introduced in nREPL 0.8) used internally a modified version of clojure-complete, that I eventually released as a library named incomplete. clojure-complete was pretty much abadonware at this point and I wanted a simple drop-in replacement that we could use in tools like REPLy (and by association - Leiningen).1

    Using the much better compliment library wasn’t an option, as we needed something that was quite literally a single file. (so it’d be easy to inline in nREPL and load in REPLy) Recently, however, compliment’s author Oleksandr Yakushev released exactly what we needed - a single-file, stripped-down version of compliment called compliment-lite. Here’s how it differs from the full-fledged compliment:

    • Context. Completion features that require context (e.g., filtering methods by the class of the receiver object) don’t work.
    • Local bindings and resources. Those two sources of completions completely rely on context, so they are disabled in Compliment-lite.
    • Documentation. The documentation function is absent from Compliment-lite.

    None of those were things supported by incomplete, so nothing lost. Naturally, it made sense for nREPL to adopt it and offer improved out-of-the-box completion experience to Clojure programmers. As this doesn’t affect the public API of nREPL2 I’ve opted to ship this change in a “bug-fix” release. A version like 1.1.1 is just too cool to pass up!

    In light of the creation of compliment-lite I now consider incomplete to be obsolete and I’d encourage everyone looking for a simple, but capable code-completion library to go for compliment-lite instead.

    That’s all I have for you today. Keep hacking!

  • Need for Speed: Using RuboCop with Prism

    By now probably most Rubists have heard of Prism (formerly know as YARP), the modern and (very) fast Ruby parser that’s aiming to replace all the existing Ruby parsers out there. Including RuboCop’s “own” parser (a.k.a whitequark/parser), which aimed to replace ripper and ruby_parser, back when it was created.

    I’ve been keeping an eye on Prism for while, as RuboCop has long been criticized for its choice of parser (and more specifically - for that parser being somewhat slow). That being said - parser migrations are always a pain, especially in a project as big as RuboCop. Early on I had to essentially rewrite RuboCop when I switched the codebase from ripper to parser, and back then RuboCop was a lot smaller. The good thing is that such rewrites can be done incrementally (I was migrating cops in batches), but it was still a lot of (boring, repetitive) work.

    That’s why I was super happy when I recently discovered parser-prism - a gem that provides a new backend for the parser gem’s syntax tree that uses the Prism parser. To make things better - it seemed that this library actually worked with RuboCop already. Yeah, the users still had to do some manual work, but it turned out that the migration could be a lot simpler than what I had expected. Did I also mention some mighty impressive benchmarks?


    As a whole, this parser should be significantly faster than the parser gem. The bin/bench script in this repository compares the performance of Parser::CurrentRuby and Parser::Prism. Running against a large file like lib/parser/prism/compiler.rb yields:

    Warming up --------------------------------------
     Parser::CurrentRuby     1.000  i/100ms
           Parser::Prism     6.000  i/100ms
    Calculating -------------------------------------
     Parser::CurrentRuby     16.642  (± 0.0%) i/s -     84.000  in   5.052021s
           Parser::Prism     64.951  (± 3.1%) i/s -    330.000  in   5.088147s
    
    Comparison:
           Parser::Prism:       65.0 i/s
     Parser::CurrentRuby:       16.6 i/s - 3.90x  slower
    

    When running with --yjit, the comparison is even more stark:

    Warming up --------------------------------------
     Parser::CurrentRuby     1.000  i/100ms
           Parser::Prism     9.000  i/100ms
    Calculating -------------------------------------
     Parser::CurrentRuby     20.062  (± 0.0%) i/s -    101.000  in   5.034389s
           Parser::Prism    112.823  (± 9.7%) i/s -    558.000  in   5.009460s
    
    Comparison:
           Parser::Prism:      112.8 i/s
     Parser::CurrentRuby:       20.1 i/s - 5.62x  slower
    

    These benchmarks were run on a single laptop without a lot of control for other processes, so take them with a grain of salt.


    Note: The results above were taken straight from parser-prism’s README. In a nutshell we’re looking into something like 4-6 times speedup!!! Now I was VERY excited!

    Immediately I created an issue to improve the support for Prism in RuboCop and we’ve started to collaborate with Prism’s author Kevin Newton, who has been very supportive and accommodating. I suggest to everyone interested in the topic to peruse the discussion in this issue (and issues references from it), as we’ve already managed to simplify the usage of RuboCop with Prism quite a bit. And we’re very close to addressing the main outstanding item - multi-versioning for the parser. Basically, the parser gem allows you to select the version of Ruby to parse code as (e.g. 3.1), independently from the version of Ruby runtime. This powers the TargetRubyVersion configuration option in RuboCop and was one of the many reasons for picking parser over ripper back in the day. In practical terms - this allowed us to keep supporting parsing Ruby 2.x code, long after one couldn’t run RuboCop on Ruby 2.x. To put it in different terms - the versions of Ruby that RuboCop understands (can parse) are completely orthogonal to the versions of Ruby on which RuboCop can be run.

    This and other items we need to address, are nicely summarized here. None of them seems like a particularly hard obstacle, so I’m quite optimistic about the future. A future in which one day you’ll be able to have this in your RuboCop config:

    ParserEngine: prism
    

    When is this going to happen exactly? No promises yet, but at the current pace it will likely happen sooner rather than later. I’d encourage adventurous people to play with RuboCop on Prism and to contribute to getting all required pieces in place faster.

    I recall that RuboCop’s adoption of whitequar/parser was instrumental in uncovering (many) weird bugs in it, and I have a feeling that things might be the same for parser-prism as well. (I assume a lot less projects use parser-prism compared to Prism) Exciting times ahead!

    That’s all I have for you today! Keep hacking!

  • Configuring fixed/tonsky indentation in clojure-mode

    A few years ago Nikita Tonsky made popular a certain style of formating Clojure code, that became known as “fixed” or “tonsky” indentation in the community.

    clojure-mode has long had some support for this via clojure-indent-style:

    (setq clojure-indent-style 'always-indent)
    

    However, it was kind of hard to get exactly the same indentation from Nikita’s article, as there was no way to suppress the usage of indent specs and forms starting with a keyword were indented by separate rules from what was set by clojure-indent-style. A recent upstream change made the indentation configuration more granular and now you can get fixed indentation with the following snippet:

    (setq clojure-indent-style 'always-indent
          clojure-indent-keyword-style 'always-indent
          clojure-enable-indent-specs nil)
    

    clojure-indent-keyword-style and clojure-enable-indent-specs are the new additions, that made this possible. Here’s how clojure-indent-keyword-style can be used:

    • always-align (default) - All args are vertically aligned with the first arg in case (A), and vertically aligned with the function name in case (B).

       (:require [foo.bar]
                 [bar.baz])
       (:require
        [foo.bar]
        [bar.baz])
      
    • always-indent - All args are indented like a macro body.

        (:require [foo.bar]
           [bar.baz])
        (:x
           location
           0)
      
    • align-arguments - Case (A) is indented like always-align, and case (B) is indented like a macro body.

        (:require [foo.bar]
                  [bar.baz])
        (:x
           location
           0)
      

    By the way, clojure-ts-mode also supports the fixed indentation style:

    (setq clojure-ts-indent-style 'fixed)
    

    The configuration here is a lot simpler, as this functionality existed since day 1.

    For the record, I still don’t endorse/like fixed indentation1 (and funny enough - neither does Arne, who implemented those changes). Still, I acknowledge that this style is somewhat popular in the Clojure community and I’m all for making it easier for people who like/need it to be able to use it.

    That’s all I have for you today. Keep hacking!

Subscribe via RSS | View Older Posts