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.
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)).
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.
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
end
end
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.
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"
end
end
end
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.
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.
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.
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.
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.
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!"
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.
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
...
end
end
…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.
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.
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 sureIn Clojure, the thread-first macro would be used instead of a chain calls in a readable way.
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.
Unfortunately, Ruby doesn’t have first class support for functions -
decode
would be interpreted the same asdecode()
and would throw anArgumentError
. You would have to do something likemessage.then(&method(:decode)).then(&method(:process))
.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.
Method objects and procs/lambdas are interchangeable here.
[Comment removed by author]
Here’s my response to whether Ruby has first-class functions: https://briankung.dev/2022/03/27/ruby-has-economy-class-functions/
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.
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.Yes, doing two entirely different things behaves differently! Allow me to flip this comparison on its head:
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
Currying in JS
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:
[Comment removed by author]
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!
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:
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.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.
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.
It’s also worth mentioning that your example:
You are demonstrating this property, which can also be written as
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.
This isn’t strictly necessary to qualify as first-class, but I think it is the most common way to do it.
Is that more clear? Functions in Ruby are invoked with
call
which has syntactic sugar.()
.Second, methods are not functions.
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 ofObject
, and callingdef
is actually defining a new method on the Object class.send
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
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.
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:
…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.A better way to do this generically is with dry-monads and the
Result
type. If each function returns aResult
, you can easily compose them together: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.
My only concern would be whether those objects are good abstractions.