1. 17
  1. 2

    This is an announcement to a list of articles about ruby, currently one: https://timriley.info/writing/2022/03/24/let-the-shape-of-the-code-reflect-its-flow/

    I would rather do Message.new(message).tap(&:decode).process or decode in the initializer, not sure

    1. 2

      In Clojure, the thread-first macro would be used instead of a chain calls in a readable way.

      (-> message decode build-event process)

      I’m not familiar with Ruby, but I’m guessing the chain of then calls can’t be collapsed?


      I understand the author’s concern that not naming the intermediate values can make understanding the intermediate types unclear, but I think not having the names increases readability. In most cases, the intermediate structures are defined by or near the function definitions that use them.

      1. 1

        Unfortunately, Ruby doesn’t have first class support for functions - decode would be interpreted the same as decode() and would throw an ArgumentError. You would have to do something like message.then(&method(:decode)).then(&method(:process)).

        1. 7

          I’ve seen people say this a lot, that Ruby doesn’t have first-class functions, but I think that is wrong. It’s just not syntactically as convenient as other languages, but you absolutely can pass methods around as objects.

          operation = method(:decode) >> method(:build_event) >> method(:process)

          Method objects and procs/lambdas are interchangeable here.

          1. 1

            Here’s my response to whether Ruby has first-class functions: https://briankung.dev/2022/03/27/ruby-has-economy-class-functions/

            1. 1

              I disagree with your argument because the entire Higher-Order Functions section confuses the difference between methods and functions. All of the problems you demonstrate come from that misunderstanding.

              Next, let’s try to return functions from functions in Ruby:

              def returns_function
                def addition(a, b)
                  a + b

              You are not instantiating a function here at all, you are metaprogramming the receiver of returns_function to add a new method. There are times where you’d actually want to do this, but they have nothing to do with higher-order functions.

              add = returns_function
              #=> :addition
              (irb):10:in `<main>': undefined method `add' for main:Object (NoMethodError)

              You can do this pretty handily in Javascript:

              let returnsFunction = function() {
                return function(a,b) {return a + b };
              let addition = returnsFunction();
              // 3

              Yes, doing two entirely different things behaves differently! Allow me to flip this comparison on its head:

              dog = Object.new
              def dog.bark = "woof!"
              dog.bark # => "woof!"
              function addBark (obj) {
                obj.bark = function() { return "woof!" }
              let dog = {};
              let bark = addBark(dog);
              bark() // Uncaught TypeError: bark is not a function

              Huh, JavaScript sucks at methods doesn’t it? (No, this was intentionally wrong)

              An apples-to-apples comparison looks a lot closer:

              Currying in Ruby

              addition = ->(x, y) { x + y }
              add_two => addition.curry.(2)
              add_two.(4) # => 6

              Currying in JS

              let addition = (x, y) => x + y
              let addTwo = (y) => addition(2, y)
              addTwo(4) // 6

              Ruby makes different tradeoffs between methods and functions than JavaScript or other languages. JS is more comfortable for pure functions, but methods are janky. Ruby puts most of its ergonomic focus on methods, with implicit block arguments fullfilling the majority of first-class function use-cases, and native procs and lambdas filling in the blanks.

              This leads to more complexity in some cases, it’s true. But some of the interactions between them is much richer than JS APIs can provide. For instance:

              require 'markaby'
              mab = Markaby::Builder.new
              mab.html do
                head { title "Boats.com" }
                body do
                  h1 "Boats.com has great deals"
                  ul do
                    li "$49 for a canoe"
                    li "$39 for a raft"
                    li "$29 for a huge boot that floats and can fit 5 people"
              1. 1

                I think you’re 1) making a strawman argument, and 2) ignoring the ergonomics of what first-class functions means. The fact that you have to think about any of this at all in order to use Ruby’s “first class functions” means you can only say it has them with a lot of disclaimers. As I put it in the post, “economy-class.” Note that I don’t deny that it technically has them!

                JS is more comfortable for pure functions, but methods are janky. Ruby puts most of its ergonomic focus on methods

                To be equally pedantic, we weren’t talking about methods, were we? We’re talking about first-class functions. Not first-class methods. Yes, the syntax you call out defines a method on the caller, but do programmers used to first-class functions want to think about that? No, they do not. In fact, the fact that the def keyword defines methods and not functions makes it even more clear that Ruby does not treat functions as first-class.

                Yes, you can curry in Ruby. But it’s an incredibly uncommon pattern. Why? Because Ruby doesn’t treat functions as truly first class. You can do it - that was the whole point of my post - but it’s awkward. Wikipedia labels Ruby’s first-class functions support with the equivalent of asterisks:

                • The identifier of a regular “function” in Ruby (which is really a method) cannot be used as a value or passed. It must first be retrieved into a Method or Proc object to be used as first-class data. The syntax for calling such a function object differs from calling regular methods.
                • Nested method definitions do not actually nest the scope.
                • Explicit currying with Proc#curry.

                Also yes, you can do cool stuff with instance_eval, but that’s not what we’re talking about. We’re talking about language-level support for first-class functions.

                1. 0

                  I don’t understand how that could possibly be construed as a strawman, the entirety of my comments were responding to the substance of your blog post. Unless you are referring to the JS example, which I pointed out directly was intentionally wrong to make a point.

                  The fact that you have to think about any of this at all in order to use Ruby’s “first class functions” means you can only say it has them with a lot of disclaimers. As I put it in the post, “economy-class.” […] To be equally pedantic, we weren’t talking about methods, were we? We’re talking about first-class functions.

                  Ruby has first-class functions without any disclaimers. My critique of your argument is that you keep confusing method invocation for function execution and these are two entirely different language concepts.

                  To be explicitly clear: when I say Ruby has first-class functions, I am speaking of Procs and Lambdas. Those are functions. Methods are not. Two different things.

                  1. Assigning a function to a variable
                  greet = ->(name) { "Hello, #{name}!" }
                  greet.("John") # => "Hello, John!"
                  1. Passing a function as an argument
                  transform = ->(str, fn) { fn.(str) }
                  transform.("foo", -> { _1.reverse }) # => "oof"

                  It’s also worth mentioning that your example:

                  array.select {|number| number.even? }

                  You are demonstrating this property, which can also be written as

                  evens = proc { |number| number.even? }
                  array.send :select, &evens

                  Block arguments are Procs that are passed as arguments to methods. This is the most common way of using first-class functions in Ruby, and it’s one of the most unique language features.

                  1. Returned as a result of a function
                  curry = ->(fn, x) { ->(*args) { fn.(x, *args) } }
                  addition = ->(x, y) { x + y }
                  add_one = curry.(addition, 1)
                  add_one.(1) # => 2
                  1. Including a function in any data structure
                  transforms = {
                    reverse: -> { _1.reverse },
                    upcase: -> { _1.upcase }
                  transforms[:reverse].("foo") # => "oof"
                  transforms[:upcase].("foo") # => "FOO"
                  1. Composing multiple functions together
                  reverse = -> { _1.reverse }
                  upcase = -> { _1.upcase }
                  shout_in_reverse = reverse >> upcase
                  shout_in_reverse.("foo") # => "OOF"
                  1. Functions are closures

                  This isn’t strictly necessary to qualify as first-class, but I think it is the most common way to do it.

                  count = 0
                  incr = -> { count += 1 }
                  incr.() # => 1
                  count # => 1

                  Is that more clear? Functions in Ruby are invoked with call which has syntactic sugar .().

                  Second, methods are not functions.

                  1. Methods are not closures
                  count = 0
                  def incr = count += 1
                  incr() # NoMethodError: undefined method `+' for nil:NilClass

                  This happened because the execution binding of a method is the receiver, not the lexical scope. This is important because calling a method is essentially just syntactic sugar for send

                  This can be confusing in a REPL specifically, because it’s not clear from the above what you are actually doing. In a Ruby REPL, main is an instance of Object, and calling def is actually defining a new method on the Object class.

                  1. Method invocation is just send
                  dog = Object.new
                  def dog.bark = "woof!"
                  dog.bark # => "woof!"
                  dog.send :bark # => "woof!"

                  Method objects exist so that you can translate the method interface into a function interface, but you’re really just bundling up the receiver together with a message to send

                  bark = dog.method(:bark)
                  bark.() # => "woof!"
                  1. Methods can’t be detached from their receiver

                  A method is a type of message that an object can respond to. You can’t separate them (well, you can but UnboundMethod is not executable until it is bound to something)

                  A Ruby method is an implementation of the OOP idea of data and behavior existing together in a single object.

                  All methods have an implicit block argument (a Proc), which is a first-class function. The syntax of Ruby allows you to seamlessly combine methods and functions into a single interface, but there is no requirement that you do so.

    2. 2

      Is it considered bad form to make each of those methods return an object and define the methods on each of those objects? If you set up like so:

      class Message
        attr_accessor :message_string
        # returns a DecodedMessage object
        def decode

      …and so on, you’d be able to end up with message.decode.build_event.process. You have to create explicit objects for each of those intermediate steps so those methods have somewhere to live, so it’s more verbose and indirect overall, but you get some very readable code.

      1. 2

        A better way to do this generically is with dry-monads and the Result type. If each function returns a Result, you can easily compose them together:

        1. 2

          You might think so, because it embraces ruby’s OO spirit, but in practice this solution would be far worse: the overhead of three abstractions, the bulkiness of having to create 3 classes, the way it breaks locality of behavior – the cure would be far worse than the disease.

          1. 1

            My only concern would be whether those objects are good abstractions.