Alifatisk 32 minutes ago

I believe the underlying behaviour of Ruby blocks is one of those mechanics that isn't talked about that much for newcomers, they just get used to how Ruby code look like when they see Rails, Cucumber or RSpec

Blocks and method_missing is one of those things in Ruby is what makes it so powerful! I remember watching a talk where someone was able to run JS snippets on pure Ruby just by recreating the syntax. That proves how powerful Ruby is for creating your own DSL

It's also a double edged sword and something you have to be careful with on collaborative codebases. Always prefer simplicity and readability over exotic and neat tricks, but I understand the difficulty when you have access to such a powerful tool

  • nasmorn 25 minutes ago

    IMO blocks are not something to be careful about in ruby. If you don’t use blocks you are the weirdo in this language.

    Method missing is a different beast altogether. I would probably avoid it nowadays.

inopinatus 2 hours ago

For this audience it may be worth noting that Ruby’s blocks are closures and are passed to methods either anonymously/implicitly or as a named parameter, may be subsequently passed around to any collaborator object, or otherwise deferred/ignored, have the same range of argument arity as methods and lambdas, can even be formed from (and treated similarly to) lambdas, and are thereby fundamental to Ruby’s claim to being a multiparadigm language even as they also betray the Smalltalk roots.

In addition they have nonlocal return semantics, somewhat like a simple continuation, making them ideal for inline iteration and folding, which is how most new Rubyists first encounter them, but also occasionally a source of surprise and confusion, most notably if one mistakenly conflates return with result. Ruby does separately have callcc for more precise control over stack unwinding, although it’s a little known feature.

kace91 2 hours ago

Coming from a language with functions as first class objects, blocks felt a bit limited to me, because it feels as if you almost have functions but not really, and they get inputted by a back door. Used for example to:

let isLarge = a => a>100;

numbers.filter(isLarge)

Blocks let you do the same but without extracting the body as cleanly. Maybe it’s a chronological issue, where Ruby was born at a time when the above wasn’t commonplace?

>When you write 5.times { puts “Hello” }, you don’t think “I’m calling the times method and passing it a block.” You think “I’m doing something 5 times.”

I’m of two minds about this.

On the one hand, I do agree that aesthetically Ruby looks very clean and pleasing. On the other, I always feel like the mental model I have about a language is usually “dirtied” to improve syntax.

The value 5 having a method, and that method being an iterator for its value, is kinda weird in any design sense and doesn’t seem to fix any architectural order you might expect, it’s just there because the “hack” results in pretty text when used.

These magical tricks are everywhere in the language with missing_method and the like, and I guess there’s a divide between programmers’ minds when some go “oh that’s nice” and don’t care how the magic is done, and others are naturally irked by the “clever twists”.

  • WJW 2 hours ago

    > The value 5 having a method, and that method being an iterator for its value, is kinda weird in any design sense and doesn’t seem to fix any architectural order you might expect, it’s just there because the “hack” results in pretty text when used.

    I don't think this is particularly weird, in Ruby at least. The language follows object orientation to its natural conclusion, which is that everything is an object, always. There is no such thing as "just data" in Ruby, because everything is an object. Even things that would just be an `int` in most other languages are actually objects, and so they have methods. The `times` method exists on the Integer classes because doing something exactly an integer number of times happens a lot in practice.

    • kace91 2 hours ago

      I don’t have an issue with the “everything’s an object” part, because it _is_ consistent, even though it gets a bit trippy when classes are objects as well and they are implementation of a Class class which is an implementation of itself (trickery again!).

      The issue is more with this part:

      >The `times` method exists on the Integer classes because doing something exactly an integer number of times happens a lot in practice.

      It is practical, but it breaks the conceptual model in that it is a hard sell that “times” is a property over the “5” object.

      The result is cleaner syntax, I know, but there is something in these choices that still feels continually “hacky/irky” to me.

      • WJW 2 hours ago

        Perhaps I've been doing Ruby for too long, but it's still not that weird to me. The quantity "5" is very abstract without anything to have "5" of. That is why "5.days" and "5.times" exist, among others. Mathematically it makes just as much sense to start with the amount and add the unit later than it does to start with the unit and add the amount later (ie like `time_to_wait = SECONDS_IN_A_DAY * 5` as you might do in some other languages).

        • kace91 an hour ago

          Maybe it is clearer if I explain it in syntactic terms? In my mental model objects are nouns (described entities) and methods are verbs - actions over the noun.

          process.start() is the action of starting done by the the noun that is the process.

          It's not exactly a matter of naming, as some methods are not explicitly verbs, but there is almost always an implicit action there: word.to_string() clearly has the convert/transform implication, even if ommitted for brevity.

          I see no path where 5 is a noun and times the verb, nor any verb I can put there that makes it make sense. If you try to stick a verb (iterate?) it becomes clear that 5 is not the noun, the thing performing the iteration, but a complement - X iterates (5 times). Perhaps the block itself having a times object with 5 as an input would make it more standard to me (?).

          But I do understand that if something is extremely practical a purist/conceptual argument doesn't go very far.

          • oezi 44 minutes ago

            I have been doing Ruby for so long that it feels very natural to apply a method in this way on the instance.

            false.not

            applies the not method on the false instance in the same way that

            car.start

            in every OO language calls the start method on car as the receiver.

            So filter(list) feels just wrong when you are clearly filtering the list itself.

            • nasmorn 21 minutes ago

              Although I prefer Elixir currently I agree that ruby at least goes all the way in on OO and not having to remember which feature is implemented as a language syntax and what is just a method invocation is a strength not a weakness. It is different in other languages for historical performance reasons really.

            • kace91 17 minutes ago

              list.filter is ok! Filtering is an action that applies to a list

              false.not is borderline but if read as false.negate it makes sense (negating is an action that applies to a Boolean value). That wording screws the chaining though.

              5.times is where the pattern breaks: times is not an action that applies to a number (nor an action at all). It’s the block the one that should repeat/iterate - but Ruby breaks the rule there and blocks are not an object (!). If they were you could block.repeat(5) which IMO is cleaner.

              • chao- 2 minutes ago

                There is a bit of personal preference in what "applies to a number", but I see what you mean.

                As a slight correction, a block is indeed an object! They are received by methods as an instance of the Proc class:

                  def inspects_block(&block)
                    puts block
                    puts block.class
                  end
                  inspects_block { "foo" }
                  # => #<Proc:0x0000000000000000>
                  # => Proc
                
                You can even add a 'repeat' method to these in the way that you specified, although you will need to add '->' to declare the block (as a lambda, which is also just an instance of Proc) before you call #repeat on it:

                  class Proc
                    def repeat(n)
                      n.times { self.call }
                    end
                  end
                  ->{ puts("foo") }.repeat(3)
                  # => foo
                  # => foo
                  # => foo
      • gray_-_wolf an hour ago

        > that “times” is a property over the “5” object

        Maybe here is the confusion, ruby is based on message passing, so the `times` is a message you are sending to 5, not a property of it.

  • judofyr 2 hours ago

    Blocks are fundamentally different from functions due to the control flow: `return` inside a block will return the outer method, not the block. `break` stops the whole method that was invoked.

    This adds some complexity in the language, but it means that it’s far more expressive. In Ruby you can with nothing but Array#each write idiomatic code which reads very similar to other traditional languages with loops and statements.

    • oezi 42 minutes ago

      You are right on return (use next in a block), but break uses block scope.

  • jhbadger 2 hours ago

    If you are familiar with a true object-oriented language like Smalltalk (rather than the watered-down form of OO in C++, Java, etc.), an integer like 5 having methods makes sense because it (like everything else) is an object. Objects in Ruby aren't just window dressing -- they are its core.

    • ck45 an hour ago

      But then Ruby only goes half way, not unlike the "watered-down form" in your term. Why is `#times` a method of Integer, but `#if` (or `#ifTrue`) not a method of booleans like in Smalltalk? Ruby does the same cherry picking from Smalltalk like everybody else, just different cherries. When looking at Ruby, it feels like the simple examples are all nice and clean but then the weird details start to appear and the language feels more hacky than others (like Ned Flander's house in Simpsons S08E08).

      • chao- an hour ago

        #if and #ifTrue are yours if you want them:

          class TrueClass
            def if = true
            def ifTrue = true
          end
        
          class FalseClass
            def if = false
            def ifTrue = false
          end
        
          true.if
          # => true
          false.if
          # => false
        • wild_egg an hour ago

          In Smalltalk those methods don't return `true`. They take a block and evaluate it if the boolean receiving the message

              (a > b) ifTrue: [ "do something" ]
          
          EDIT: to clarify what's happening there, `>` is a message sent to `a` that will result in a boolean. The True class and False class both understand the ifTrue: message and `True>>ifTrue:` executes the block whereas `False>>ifTrue:` just throws it away.

          There's no `if` keyword in the language. Control flow is done purely through polymorphism.

          • chao- 38 minutes ago

            I apologize for my lack of Smalltalk knowledge. As you can imagine, you can do similar in Ruby by defining ifTrue to accept a block, even adding ifTrue on other all objects and defining something similar:

              class TrueClass
                def ifTrue(&block) = block.call
              end
            
              class FalseClass
                def ifTrue(&block) = nil
              end
            
              class Object
                def ifTrue(&block) = block.call
              end
                  
              class NilClass
                def ifTrue(&block) = nil
              end
            
            If ck45's core complaint was that this is not baked into the language, I will agree that it is less convenient for lack of a default.
          • oezi 38 minutes ago

            Certainly possible: add ifTrue as a method to TrueClass and FalseClass.

            It just isn't very fast.

  • chao- 2 hours ago

    The "aesthetically pleasing" aspect of blocks is not mutually exclusive with real, first-class functions! Ruby is really more functional than that. Ruby has both lambas and method objects (pulled from instances). For example, you can write:

      let isLarge = a => a>100;
    
    as a lambda and call via #call or the shorthand syntax .():

      is_large = ->(a) { a > 100 }
      is_large.call(1000)
      # => true
      is_large.(1000)
      # => true
    
    I find the .() syntax a bit odd, so I prefer #call, but that's a personal choice. Either way, it mixes-and-matches nicely with any class that has a #call method, and so it allows nice polymorphic mixtures of lambdas and of objects/instances that have a method named 'call'. Also very useful for injecting behavior (and mocking behavior in tests).

    Additionally, you can even take a reference to a method off of an object, and pass them around as though they are a callable lambda/block:

      class Foo
        def bar = 'baz'
      end
    
      foo_instance = Foo.new
      callable_bar = foo_instance.method(:bar)
      callable_bar.call
      # => 'baz'
    
    This ability to pull a method off is useful because any method which receives block can also take a "method object" and be passed to any block-receiving method via the "block operator" of '&' (example here is passing an object's method to Array#map as a block):

      class UpcaseCertainLetters
        def initialize(letters_to_upcase)
          @letters_to_upcase = letters_to_upcase
        end
    
        def format(str)
          str.chars.map do |char| 
            @letters_to_upcase.include?(char) ? char.upcase : char
          end.join
        end
      end
    
      upcase_vowels = UpcaseCertainLetters.new("aeiuo").method(:format)
      ['foo', 'bar', 'baz'].map(&upcase_vowels)
      # => ['fOO', 'bAr', 'bAz']
    
    This '&' operator is the same as the one that lets you call instance methods by converting a symbol of a method name into a block for an instance method on an object:

      (0..10).map(&:even?)
      # => [true, false, true, false, true, false, true, false, true, false, true]
    
    And doing similar, but with a lambda:

      is_div_five = ->(num) { num % 5 == 0 }
      (0..10).map(&is_div_five)
      # => [true, false, false, false, false, true, false, false, false, false, true]
  • somewhereoutth 33 minutes ago

    Interestingly, in the Lambda Calculus, where everything is a function, a standard representation for a natural number n (i.e. a whole number >= 0), is indeed a function that 'iterates' (strictly, folds/recurses) n times.

    E.g. 3:

    (f, x) => f(f(f(x)))

politelemon an hour ago

It's only more readable if you already understand it. Otherwise it is not, it requires the same kind of hand waving that happened at the start.

Important to understand that readability doesn't mean it should be closer to natural language, in programming it means that a junior dev troubleshooting that code later down the line can easily understand what's happening.

The python examples are certainly more readable from a maintainability and comprehension standpoint. Verbosity is not a bad thing at all.

shevy-java 2 hours ago

Blocks yield a lot more flexibility to ruby. It was the primary reason why they are so well-appreciated.

stonecharioteer 3 days ago

I've been very taken by Ruby and how it uses blocks everywhere! This is an article I wrote just to emphasize that.

  • pjmlp 3 hours ago

    Have a look at Smalltalk blocks, or FP languages, to see where Ruby's inspiration comes from.

    • frou_dh 2 hours ago

      An interesting thing about Smalltalk and Ruby blocks is that they aren't just anonymous functions/lambdas, right? i.e. if you 'return' / '^' in a block, it's the context around the block that you return from, not just the block itself? That's what struck me about both of them when I was used to thinking in basic Lisp terms.

    • stonecharioteer 2 hours ago

      My next post, which is on loops, is about the common stuff with smalltalk as well!

  • PufPufPuf 3 hours ago

    Take a look at Kotlin, it perfected this idea

    • pjmlp 2 hours ago

      What Kotlin offers is already present in Scala or other languages from ML linage.

  • janfoeh 3 hours ago

    I discovered Ruby (through Rails) about twenty years ago on the dot. Coming from Perl and PHP it took me a while, but I remember the moment when I had the same realisation you did.

    I still love this language to bits, and it was fun to relive that moment vicariously through someone elses eyes. Thanks for writing it up!

    • stonecharioteer 2 hours ago

      I'm glad folks are having fun reading this. I want to write a few more articles, particularly dissecting the internals of Ruby and how amazing it feels.

dorianmariecom 29 minutes ago

blocks are just procs :)

  • Alifatisk 24 minutes ago

    It does look like it but procs encapsulates a block, but a block alone is not a proc

teaearlgraycold 2 hours ago

This level of cuteness and obsession with syntax is partly what drives me away from Ruby. A function should just be a function. We don't need to make programming languages look more like English. There are certainly issues with other languages and their verbosity (like with Java). But I don't want to have more ways to do the same thing and worry about how poetic my code reads as English - we should worry how poetic it reads as computer code.

That said, we don't need just one programming language. Perhaps Ruby is easier to learn for those new to programming and we should introduce it to students.

  • stonecharioteer 2 hours ago

    I think Ruby teaches a sense of style, but I'm not sure that style carries over to other languages. Python was my primary language for 12 years but I'm disappointed in the depth of Knowledge python Devs have. Most barely understand the language or try to. Ruby seems to coax people into coding into a ruby way. I like that.

hshdhdhehd 2 hours ago

Is a block basically a lambda or is there more to it?

  • masklinn 2 hours ago

    Assuming that by lambda you mean "an anonymous function"[1], for most intents and purposes they are, except their returns are non-local whereas functions usually have local returns.

    However blocks are special forms of the language, unless reified to procs they can only be passed as parameter (not returned), and a method can only take one block. They also have some oddities in how they interact with parameters (unless reified to lambda procs).

    [1] because Ruby has something called "lambda procs"

  • simonask 2 hours ago

    They are closures. But Ruby can do interesting and slightly reckless things, like transplanting a closure into a different evaluation scope. It’s very powerful, and also very dangerous in the wrong hands.

    • hshdhdhehd 2 hours ago

      Sounds a bit like a lisp macro? Or in JS using eval?

  • somewhereoutth 2 hours ago

    My understanding is that the 'extra thing' is control flow - blocks can force a return in their calling scope. For example a loop that calls a block may be terminated/skipped by a break/continue statement in the block itself. However I'm not a Ruby programmer, so please check my working.

varispeed an hour ago

With autocomplete being so good today, do we really need languages with cryptic syntax that obscure what is going on?

  • Alifatisk 27 minutes ago

    To an outsider watching Ruby, it's cryptic, esoteric and maybe magical. But when you actually use it and learn mechanism underneath it, things start to make sense. However, Rubys dynamic architecture have also made it difficult for DX. Things like autocomplete barely works in the ecosystem, because it's so unpredictable until runtime