"Useless Ruby sugar": Endless (one-line) methods
zverok.space
zverok.space
“how much of it fits in one page” matters. This doesn’t mean that cramming everything into tight subsequent paragraphs, like a serious book, is a good idea: code isn’t supposed to be primarily read paragraph-by-paragraph. On the other hand, a two-words-per-line, twenty-words-per-page nursery rhyme-like layout means that one might need to scroll through dozens of pages to get “what’s this all about.”
I disagree that code is "supposed to" be any particular way, but in general I agree with this point. And I think it's gradually being forgotten. Automatic code formatters and mandatory style guides are becoming more common, and while they do raise the floor (by preventing some interpretation of "bad" style) they also lower the ceiling. Uniformity isn't bad, but sometimes you really need certain thingsto
stand
OUT!
const foo = useMemo(() => {
// code
}, [
alfa,
bravo,
charlie,
]);
const bar = useMemo(() => {
// code
}, [delta, echo]);
const quux = useCallback(
(arg0: string, arg1: number): SomeType => {
// code
},
[foxtrot, golf, hotel]
);
Which is infurating because it's not visually consistent so the code starts to look like syntactic soup. It's the same thing with components, really.They're free to do it in their personal trees all they want. But if they commit changes that make the code less readable because some tool is being opinionated and can't be configured properly, their change won't pass code review.
You can't code taste into every tool that might want to help you edit your source code.
That being said, almost all formatters have ways to tell them to go away in especially important stretches of code, with the tradeoff being ugly generated code fixups for that need hand massaging every time.
I mean, I agree that that's true at least in relative terms, but in my experience that's because it raises the floor from "bottomless pit" (eg IOCCC entry quality) to "much worse than we usually do", whereas lowering the ceiling a proverbial meter or two still means hitting my head on it (there's a upper bound on how readable code can be when you need to actually read it).
My problem is the fact Ruby practitioners have a tendency ABUSE the usage of one-line-methods with LOTS of side-effects.
So it's not like Haskell one-line-methods.
Things that could be a function with 5 or 6 lines can become a class with as many methods. And instead of local variables, you now have to use instance variables (class fields).
For example, this is common in lots of codebases:
class UserCreator
def initialize(email)
@email = email
end
def create_user
create_user_object
assign_admin_role
assign_invite_permission
call_invite_template
end
private
attr_reader :email, :user
def create_user_object
@user = User.create(email)
end
def assign_admin_role
user.roles << find_admin_role
end
def assign_invite_permission
user.permissions << find_invite_permission
end
def call_invite_template
find_invite_template.call(user: user)
end
def find_admin_role
Roles.find_by(name: 'admin')
end
def find_invite_permission
Permissions.find_by(name: 'invite')
end
def find_invite_template
Templates.find_by(name: 'invite')
end
end
Sorry but this is not readable nor reasonable.I encountered this in about 4 companies so far. Only one Ruby company I worked didn't do it. That was the company that actually had a very maintanable backend.
So dreadful.
Oftentimes when I suggest to change a rubocop default it leads to epic arguments as if you’re desecrating the bible. In fact, it’s just silly to not apply critical thinking to the quality of your toolchain, and that includes questioning the decisions of your linter’s maintainers.
(Edit to add: another way of justifying the above service design pattern is to suggest that the code is DRY because you’ve extracted common sections to methods. This is taking DRY to an illogical extreme. You _should_ repeat yourself if your method calls are simple enough, and if you actually do need to repeat some logic in multiple places requiring a private method, you might find a better place to put it - for example, a filter method.)
The amount of code I see where people preemptively extract methods and even add methods they don't use to prepare for a hypothetical future situation that never comes is staggering.
I think some of these methods might be ok in a public helper class ("go here if you want to know the correct way of finding these kinds of objects so we have a single extension point"), but not as private methods...
The irony here is they try to let you read the main codepath top down, but because they unnecessarily obscure details they force you to jump back and forth between a ton of methods instead, completely ruining any benefit.
I love code that lets you actually read it sequentially, but then it needs to actually let you do that.
What does YAGNI even mean if it can stand in opposition of DRY? If it just means "you don't need to do that," and "that" can mean whatever you want, then it's not a useful guiding principle.
Example of a YAGNI violation: You're writing a piece of code whose purpose is to download some kind of asset for whatever thingamajigga you're working on. It just needs to HTTP GET the resource from your official server. To future-proof it, you also add support for using any of the HTTP verbs as well as adding custom request headers. Not only have you written useless code that will likely never be needed, you've leaked implementation details (all these HTTP details don't make sense if a future version wants to use a different protocol).
Example of what is not a YAGNI violation: Your asset downloader also needs to parse some sidechannel information that the server returns as custom response headers. You just assume that header names are capitalized the way your server sends them and don't bother with case-insensitivity. That isn't following "YAGNI," that's leaving a ticking time bomb behind.
YAGNI is about functionality, DRY is about structure.
> YAGNI is about functionality, DRY is about structure.
I absolutely see where you’re coming from and I agree, the reason DRY caught on and YAGNI hasn’t is indeed the vagueness of YAGNI. But I would equally argue that the simplicity of DRY is what gets you in trouble if you over-optimize for DRY regardless of context.
DRY can be about functionality: if you’ve already implemented a feature, even if it’s a library maintained by another team, as long as the tests encourage your use case, you should re-use existing functionality over creating yet another abandoned half-baked attempt at something.
YAGNI, by opposition, might suggest that the effort spent learning the other team’s library and maintaining a connection to what might be brittle code is itself overhead. A similar debate can be found in using an npm library. DRY would argue if you already use the library, keep using it. YAGNI would argue if the library is only used in one place, rip it out for simplicity and replace it, especially if the code is old.
Taken to extremes, DRY is how we end up with a left-pad module on npm while YAGNI is using padStart built-in to modern JS. Likewise taken to extremes, YAGNI may result in missing functionality and unmaintainable spaghetti code, while DRY is one possible approach to taming complexity in codebases. Both are essential principles, neither actually more important than the other.
I agree with JUCS, but it’s even less defined than YAGNI ;-)
An example of DRY would be: You're using libraries "foo" and "baz," and whenever foo::get_bar() returns 1, 2, or 3, you need to call (for your use-case) baz::qux(9), baz::qux(31), or baz::qux(-5), respectively. So you create one function that either handles both calls completely, or at least maps the return value from get_bar to the argument required by qux. The violation of DRY would have switch statements littered throughout the code that all do the same mapping manually.
A build-or-"buy" decision isn't about being DRY or not.
def find_admin_role
Roles.find_by(name: 'admin')
end
Assuming your application consistently uses `name`, and is consistent about class names, `find_admin_role` is not saving you complexity over Roles.find_by(name: 'admin'). People familiar with Ruby ORMs and tasked with finding an admin role will go looking for finders in the Role class. They're not likely to find your method, and so are likely to write their own.The former introduces complexity that is only warranted to me if 1) the method of finding the admin role grows more complex so it's non-obvious, 2) the method you DRY it up into is actually being used at least twice, 3) it's placed in the most appropriate class.
And it's that second where YAGNI comes in. By all means, DRY up logic. But don't do it before it actually stops you repeating yourself in a way that is actually significant.
To #3, that "find_admin_role" method really should be `Roles.find_admin` if it's allowed to exist at all - there it's at least somewhat defensible even if it is only a single line method, because others are likely to find it there, and even if it seems relatively redundant, at least there it serves to document that this is indeed the canonical name for an admin role.
And here’s a bonus: the testability is exactly the same in both cases too (unless you’re breaking encapsulation in your tests — another thing that’s awfully common in Rails).
The only difference is the readability. With a 6-line function you have perfectly readable code without jumping around.
If you want to test all paths on a multi-line function, you have to do a lot of duplication and sometimes a lot more setup for each variation you're trying to test.
But another source of complexity is OOP itself. Mixing code and data is a horrible affliction we have accepted. All we need is some data structure (which can be a rigid class/struct, and then a module of appropriate operations which work on that data structure (or even elements of that structure).
We can have mostly pure functions that are 100% covered by tests... and if we choose appropriate names for these functions it allows us to think in terms of higher level "steps" (each of which we really don't care about how they are implemented).
All that said, we're debating over relatively small differences of approach in Ruby. For a really good time (not), go look at Python codebases. Then some of the people here who are arguing for a single function with 10 lines that just does the full job will find themselves staring at 100 line Python functions and thinking, "we should break this into individual functions" :).
Therefore, there's no difference in terms of "implementation hiding (encapsulation)" between the original 4-line function [0] and the class I presented.
Also, there is no testability differences, unless you break encapsulation in your tests and test private methods directly with things like send(:find_admin_role). As much as this is seen in Ruby, this is not something that would pass a code review.
This class is just 9 methods in a trenchcoat pretending to be that 4-line function.
And in case you think this not a strawman example. I provide a real-world case in another message. [2]
In fact, in Ruby it hides less, because bypassing "private" is trivial, and manipulating an object's instance variables is trivial.
If you care about encapsulation, that makes this approach worse than a single method. If you need to maintain state across invocations (this example doesn't), then capturing local variables with a closure (lambda/proc) provides far tighter encapsulation than a regular object in Ruby.
Whether the contents of do_this_specific_thing() are just one line or 100 lines, that point is that we don't have to know or care. We just know that do_this_specific_thing presumably does what its name says, and we accept that and don't worry about HOW.
When you inline those tiny functions, as the author argues for, now the reader has to recognize what each implementation line means. Of course we might say it's obvious, but it's less consistent and clear than a series of calls to functions which are well-named and take arguments which are well-named.
And as software engineers of any experience, we know that eventually one of those "simple" actions will become more complex due to some new requirements. Then that one line becomes more lines, and then of course it will end up as its own function. Over time, many of those simple steps will become more complex out of necessity, and eventually we'll end up with all those individual private methods (with bigger bodies).
As for maintaining state, I personally argue against any OOP style. All of those action functions can just be functions in an appropriately-named module. They should all take input arguments (the data), and ideally not even mutate the input but rather return a value. That value could be a new version of the input data (can be memory/time expensive, so not always the best choice), or they can be just a data structure of changes.
I'm not a fan of lambdas, however, as they require more human memory to keep track of what's going on in a system.
And let’s not touch the maintainer nightmare of having to keep method names up to date with a complete description of what the method does. You might end up with twenty words or needing multiple methods that are no-ops if you ever call a shared library with any amount of complicated functionality. Alternatively, your description of what is done is so high level that it doesn’t actually describe what’s done in a detail sufficient for non-programmers to understand.
I guess where I’m going with this is that it’s like comments that describe what the method does. We’ve all been there when the comments don’t accurately reflect reality, because someone, maybe through automation, changed how the system works and didn’t update the comment.
It’s hard for me to see as much value in this presentation over an IDE that could auto-inline the function bodies and leave a comment above each with the method name.
To me, if someone tried committing that, I'd sit down with them to have a very long discussion about 1) why it will not be accepted into the code base ever, 2) which training steps to go through with them to level them up, because if they keep writing code like that, it'll be a negative effect on the whole team - at a minimum by forcing us to review and reject their code on a regular basis - and you can be 100% certain I would ding a developer on their performance reviews if they kept writing code like this (I would give them a chance to learn not to first, but if they kept it up?)
> When you inline those tiny functions, as the author argues for, now the reader has to recognize what each implementation line means. Of course we might say it's obvious, but it's less consistent and clear than a series of calls to functions which are well-named and take arguments which are well-named.
Each of those are obvious to anyone familiar with Ruby ORMs, while the methods calls are not for the very reason you go on to give:
> And as software engineers of any experience, we know that eventually one of those "simple" actions will become more complex due to some new requirements.
This is exactly why hiding the simple calls in a method is an awful code stink, because it signals there may be more to it than just a single method call, and so to be sure I know what is going on I will need to chase down each method call.
That's fine if the method calls are actually encapsulating complexity. It's not fine when they're hiding simplicity.
When the complexity grows, then you've justified a separate method.
> As for maintaining state, I personally argue against any OOP style. All of those action functions can just be functions in an appropriately-named module. They should all take input arguments (the data), and ideally not even mutate the input but rather return a value. That value could be a new version of the input data (can be memory/time expensive, so not always the best choice), or they can be just a data structure of changes.
Ruby doesn't have functions, only methods of an object. You can pretend a method in a module isn't a method of the module object and doesn't carry state, but it does, and ignoring that means throwing away what makes Ruby powerful. E.g. making use of those abilities makes implementing monads, composition/currying etc. trivial to achieve, and sometimes having that ability is valuable and improves the ability to instrument code for example.
Put another way: If I call SomeNamespace.foo, I don't know from the fact that it looks like a module or class whether it carries state or not, because SomeNamespace is just another object, and as long as you're disciplined about how you use it, that's a good thing.
But whether or not you prefer that is besides the point. The point was that if you need or want your "user creator" to maintain state, in Ruby an object with instance variables leaves the instance variables accessible no matter what you try to do. If you're fine with that (or want that), use a regular class. If you want to prevent any tampering, use a lambda. Either way, if a class has only a single reasonable action to take on it, the method name by convention ought to be `call`. Those were the points I was making about state. You don't need to use it, but it's something to be aware of.
> I'm not a fan of lambdas, however, as they require more human memory to keep track of what's going on in a system.
A lambda in Ruby is just an object of the class Proc representing a closure. It does not inherently contain any more or less state than any other object. However it gives the benefit of being possible to pass to anything that either expects a Proc or expects to be able to `.call` an object, and when used properly that allows for a powerful level of composeability.
ActiveRecord is MUCH better at "telling what's going on without telling the how" this than micro-functions.
-
> When you inline those tiny functions, as the author argues for, now the reader has to recognize what each implementation line means.
As a software maintainer, I can understand a simple ActiveRecord query better than find_user_role.
> Of course we might say it's obvious, but it's less consistent and clear than a series of calls to functions which are well-named and take arguments which are well-named.
If the action is obvious, then simply don't turn it into a micro-function. If it's not, then put it into a function of appropriate size.
Consistency is a piss-poor argument for treating everything like a hammer. If you don't start this practice in the first place, you can't even resort to "consistency" to defend it.
-
> And as software engineers of any experience, we know that eventually one of those "simple" actions will become more complex due to some new requirements. Then that one line becomes more lines, and then of course it will end up as its own function. Over time, many of those simple steps will become more complex out of necessity, and eventually we'll end up with all those individual private methods (with bigger bodies).
Nope. That is not true for the general case. In 99% of cases, micro-functions won't get more complicated.
A simple 4-line function instead of the 6-7 methods allows for the same functionality, the same level of encapsulation and for the same testability, without the “jumping around”.
Here's the original text I inputted into ChatGPT that generated the code above:
def create_user(email)
user = User.create(email)
user.roles << Roles.find_by(name: 'admin')
user.permissions << Permissions.find_by(name: 'invite')
Templates.find_by(name: 'invite').call(user: user)
end
Pretty much anyone writing this kind of class would be able to understand the same code if it were written in a 4-line function in one glance. This here, however severely obscures the intent, the code flow and the operations.Doesn't that already look 100x better?
class UserCreator
def create_user(email)
user = create_user_object(email)
assign_admin_role(user)
assign_invite_permission(user)
call_invite_template(user)
end
private
def create_user_object(email)
User.create(email)
end
def assign_admin_role(user)
user.roles << find_admin_role
end
def assign_invite_permission(user)
user.permissions << find_invite_permission
end
def call_invite_template(user)
find_invite_template.call(user: user)
end
def find_admin_role
Roles.find_by(name: 'admin')
end
def find_invite_permission
Permissions.find_by(name: 'invite')
end
def find_invite_template
Templates.find_by(name: 'invite')
end
end def create_user(email)
user = User.create(email: email)
user.roles << Roles.find_by(name: 'admin')
user.permissions << Permissions.find_by(name: 'invite')
Templates.find_by(name: 'invite').call(user: user)
end
IMO this 4 line function is significantly better in terms of clarity, readability, and it avoids unnecessary state. Testability and encapsulation are the same.(I would argue that the encapsulation is better with a function, since encapsulation is way too easy to break in Ruby, but hey, that's me)
E.g.
lambda do
state = 0
->() { state += 1 }
end.call
(Note the `.call` at the end - we don't want the outer lambda; that is just used for creating the variables that will hold the state - we want the inner lambda)So let's say that for some stupid reason your user creator object needs to keep a count of users, you could "seal" that state into a lambda with the above technique, and make it really hard (I'm not sure if impossible) to break encapsulation. E.g. instance_eval or trying obtain the binding doesn't work, because they both will get you the state of the Proc object containing the closure, not the local variables accessible within it.
It provides some very marginal improvement, but doesn't address the actual problem of the code, which is the horrific verbosity. The use of attributes is the least of my problem with it. This is how I'd want it to look:
class UserCreator
# Arguably, I'd prefer *call* because that allows it to be interchangeable with a lambda
# But frankly this thing could *be* a lambda. E.g. you could replace the above class declaration
# with "UserCreator = ->(email) do" and ditch the "def"
# The exception, where I'd allow for an initialize and attributes would be in cases where you'd otherwise
# be passing a *lot* of *the same* state around between multiple methods
#
def self.create_user(email)
User.create(email).tap do |user|
user.roles << Roles.find_by(name: 'admin')
user.permissions << Permissions.find_by(name: 'invite')
Templates.find_by(name: 'invite').call(user: user)
end
end
end
The whole service object pattern is heavily abused by people who don't seem to understand which (limited) situations it actually provides benefits. This isn't one of them.I kinda like the Clean Code advice of small functions/methods, but they should pack a punch, not just encapsulate barely more than their own name.
E.g. here's a single-line method from the Window class of my Ruby X11 window manager:
def type = (@type ||= get_property(:_NET_WM_WINDOW_TYPE, :atom)&.value.to_i)
This does one thing: It queries the X server for the _NET_WM_WINDOW_TYPE atom attached to this window, and if a value is returned, it ensures the result is an integer (which will be 0 if what is returned is nil or somehow can't be parsed into an int, which would mean someone has been very naughty but defaulting to 0 is fine). It then ensures the value is memoised in `@type` (not doing more error checking here is fine, because failing is worse than defaulting to not knowing the window type, as we have no guarantee it's set in the first place)Tiny functions like that are fine, because they encapsulate a sufficient amount of non-obvious logic in a very concise result. Being able to write `my_window.type` without caring about that logic is great. Trying to reduce your methods to compact little methods is great when it makes you write little functions that tells you a lot.
Unfortunately - and so I agree with your reaction to Clean Code - adhering to the rest of the Clean Code advice won't leave you with a bunch of little methods that pack a punch and remove a bunch of complexity, but a bunch of little methods that each encapsulate barely more than their method names worth of complexity.
If the functions are well named, you can read up to the "private" line and stop reading. You now know what this thing does, and you've not had to complicate your thought about HOW it does it. If you need to know how one of the primary actions is performed, you can jump to that source with a simple keypress. With some settings, you can simply hover to see the function expanded.
What's more, writing tests - 100% coverage - for this example is trivial and clean. If you want to put the actual lines all in the main function, your tests will be a lot uglier and will involve more lines of test code.
The unnecessary branching makes me need to keep an eye on the stack, and a lot of extra information in my head.
The additional state (in this case @email and @user) needs to be tracked from a distance. Local variables are 100x better. If there's any mutation, I must watch out during debugging two or three levels deep. Often there are more variables than that. If I had the "role" and the "permission" as a parameter, I would need two extra instance variables. With a regular function this is just two arguments, a couple lines away from the usage site.
The original code I wrote into ChatGPT to generate the code above shows me the what and the how with much more clarity:
def create_user(email)
user = User.create(email)
user.roles << Roles.find_by(name: 'admin')
user.permissions << Permissions.find_by(name: 'invite')
Templates.find_by(name: 'invite').call(user: user)
endThe point of this pattern is to extract each operation into its own described-by-name method because they're each longer and more complicated than just a `<<` operator or whatever.
Well, here's some production code from a previous project which I happen to have authorization to use for educational purposes, with entity and fields anonymized as courtesy of ChatGPT.
This is by no means a rare thing in Rails world, and I have seen this in pretty much every Rails project I worked, save for one.
Every single time: newcomers would complain about it. Even senior/staff engineers. Only the people building it enjoy it.
Overall I see this pattern as a net negative - I've yet to work on a project where more than a tiny minority of service object classes have any reason to be one.
fun double(x: Int) = x * 2
https://kotlinlang.org/docs/functions.html#single-expression...However, in Kotlin there's no single-line constraint, so it's possible to define an expression function e.g. with a long chain of collection methods: `filter().map().findFirst()...`
Was a bit of fun in scala to see if you could rewrite something to be a single statement.
Sometimes the single statement version wasn't clearer, but sometimes it really was.
Point free programming takes this to code golf type territory and can be fun as a puzzle if not for real world code.
Maybe ruby just doesn't do it well?
func double(x: Int) -> Int { x * 2 }
It just seems less readable because it's introducing not only an additional syntax for declaring functions, but also a secondary meaning for the "=" operator.edit: in my original version of this comment I forgot to add the return type syntax, "-> Int", which Swift needs. I suppose eliding the return type is a bit more terse. You could also assign a closure in Swift to avoid that, like:
let double = { (x: Int) in x * 2 }
which is about as short as the Kotlin version, and has the advantage of not being "special" in any sense from any other Swift syntax (i.e., it's just assigning a standard closure to a variable).Please explain to this ruby noob if you have time, why use =? Why not some other less used symbol or symbol pair instead? What are they trying to achieve with this?
This entire syntax was unnecessary, undesired, and is flat-out a mistake. I’ve felt this way about most of the syntax changes since 2.0, with the exception of named parameters.
def initialize(text)
@text = text
end
def inspect
"#<#{self.class} #{text}>"
end
def ==(other)
other.is_a?(Word) && text == other.text
end
with def initialize(text) = @text = text
def inspect = "#<#{self.class} #{text}>"
def ==(other) = other.is_a?(Word) && text == other.text
I start drooling a little. That's 3 lines to replace 11. We have a soft limit on class length of 100 lines, and I like the extra conciseness this allows.You can also do it like
define_method(:inspect) { |text| "#<#{self.class} #{text}>" }
and we do that in some places, but that extra verbosity makes for more lines. def initialize(text)
= (@text = text)
def inspect
= ("#<#{self.class} #{text}>")
def ==(other)
= (other.is_a?(Word) && text == other.text)
seems like a good compromise.edit: this is similar to our standard when writing one-liner methods in C#. A bit noisier because of types and visibility, but still pretty concise, imho.
public void Initialize(string text)
=> Text = text;
// I actually hate the above a bit because "=>"
// originally implied functional/no-side-effects
// in C#, but the world moves on.
public string Inspect()
=> $"{this.GetType().Name} {text}>";
public static bool operator== (Word a, Word b)
=> a.Text == b.Text;https://google.github.io/styleguide/jsguide.html#formatting-...
> Terminology Note: Horizontal alignment is the practice of adding a variable number of additional spaces in your code with the goal of making certain tokens appear directly below certain other tokens on previous lines.
>
> This practice is permitted, but it is generally discouraged by Google Style. It is not even required to maintain horizontal alignment in places where it was already used.
>
> Here is an example without alignment, followed by one with alignment. Both are allowed, but the latter is discouraged:
{
tiny: 42, // this is great
longer: 435, // this too
};
{
tiny: 42, // permitted, but future edits
longer: 435, // may leave it unaligned
};
> Tip: Alignment can aid readability, but it creates problems for future maintenance. Consider a future change that needs to touch just one line. This change may leave the formerly-pleasing formatting mangled, and that is allowed. More often it prompts the coder (perhaps you) to adjust whitespace on nearby lines as well, possibly triggering a cascading series of reformattings. That one-line change now has a "blast radius." This can at worst result in pointless busywork, but at best it still corrupts version history information, slows down reviewers and exacerbates merge conflicts.The "compact" line format is something that is very information dense and really really hard to parse for a human's eye, compared to the clearly visually structured prior format.
Personally, if I'd see something like that outside of a code golf tournament, I'd run because that kind of code density means that at least one of the developer(s) believes themselves to be some kind of code wizard who loves Matrix-style display, and it means that new developers have a very steep learning curve ahead of them.
I do this commonly in the Perl I write. If I'm using a complex set of maps and greps to reorganize a structure, doing that inline can be useful to the developer because it can match mental state, but the later reader may be somewhat lost when seeing it depending on how much of the data state they've internalized. A nice comment to explain the intent is useful, and when you're already saving space by eliding syntax, keep a good ratio of action to code on the screen (which is really what we're optimizing for here most times anyway, right?)
Ask any typesetter. Blank space used well gives text breathing room, making it easier to parse visually.
In the former example, it is extremely easy to pick out what each method name is and a decent idea of what it does at a glance. In the latter, I have to read to even see the method names.
What is a typesetter? Someone or something which takes tree-structured sentences, cranks them into a linear sequence of words, which is arbitrarily chopped into a rectangular form, with the goal being that the rectangle appear evenly gray from a distance. Sometimes ugly hyphens are inserted into the middle of tokens.
I'd recommend your team to stop code-golfing... Even better: with this new syntax you MUST reduce the soft limit to around 50 lines per class, right?
And even if an extra line of separation is used, it's still 6 lines instead of 11.
The first snippet is much easier to read, and using this technique to get around your 100 line rules seems to be missing the point of the rule - or purposely subverting it.
1> (defstruct (word text) ()
text
(:method print (me stream pretty-p) (put-string `#<@(typeof me) @{me.text}>` stream))
(:method equal (me) me.text))
#<struct-type word>
2> (new (word "abc"))
#<word abc>
3> (equal *2 "abc")
t
You wouldn't define a print method for a struct like this, because it's counterproductive. You're throwing away print-read consistency in exchange for no benefit.I usually define print methods for complex structures with many slots, that are not expected to be externalized. Particularly if they are linked in a graph structure, where (even if you have the circular printing turned on to handle the cycles) the prints get large. You know, you wanted to just print the banana, but it was pointing to a gorilla, and basically a dump of the whole jungle ensued.
Nobody forces you to use them. I for one is extremely happy with most of the changes in recent years.
“Nobody forces you to use it” can justify just about any insane addition to a programming language. Nobody forces you to use any of C++’s zillion and one features, but because they exist you have to contend with their consequences even if you try to limit yourself to modern conventions.
We didn’t need two ways to define methods, one of which parses insanely in order to save a few keystrokes. Adding this didn’t make the language better, it made it more complicated for virtually zero net gain.
We have several more ways to define methods already, and always have. If you think this gives us two perhaps the problem is you don't know the language very well.
Choices like this, to create options to adjust how the code reads has always been central to the design of Ruby. It's an odd thing to be annoyed with Ruby over.
"Parses insanely" is highly subjective. It parses exactly as you should expect it to if familiar with Ruby. Some of it is not ideal, and will likely be addressed with adjustments to the grammar. In the meantime the simple fix is to add parentheses whenever in doubt.
Resorting to semantic-lawyering doesn’t add to your argument, it detracts from it. You know the point I was making.
int AddOne(int x) => x + 1;
or
void SetXToZero() => x = 0;
def [functionname] = [body]
seems fine to me. The big flaw (that it can't tell the difference between the end of the body and the end of the statement line) would be the same with any token, I think.From what I'm seeing, though, they should've made parentheses mandatory around [body] until they could fix the parser problem. They could easily make them optional in some hypothetical future where they've corrected the issue.
That said, it seems like C# wins at solving this problem, where the `=>` syntax is used for both lambdas and one-liner functions.
If you’re thinking functional, everything looks like a function. Examples (pseudocode that may be valid Ruby)
x = 3
def x = 3
def x()
return 3
end
all define a parameterless function called that always returns 3.So why would you use different syntax for them?
There’s only one reason: impure functions. For these 3:
x = rand()
def x = rand()
def x()
return rand()
end
The first would call rand() once, the other two each time they get called.That, I expect, is why Ruby still has def.
In scala, there’s similar thinking, which also let to them using () not only for specifying function arguments, but also for indexing into arrays or dictionaries. After all, an immutable array
x = [10,20,30]
behaves the same as def x(i)
switch i
case 0 return 10
case 1 return 20
case 2 return 39
otherwise throw indexOutOfBounds
end
Of course, if you unify notation, you’d also have to make these things perfectly interchangeable everywhere. Not being able or wanting to do that is a good reason for keeping syntax different.Another one is that you may want to expose the pesky detail that indexing into an array typically is a lot faster than calling a function to the programmers, so that they can get a decent idea about the performance of code they read. That can only work if you keep your execution model simple, though, and if your programmers can reasonably predict what the compiler does, and how that will run on the cpu. That worked fine for C in the 1970s, but not so well anymore today. I think Golang is an attempt to get back there.
// type annotations are optional, if the compiler can infer them
def foo = 42
def bar(x: Int): String = {
// stuff
}
It works fine IMO. Scala 3 introduced Python-style syntax, in which {} is optional: def bar(x: Int): String =
// stuff
end bar // end is optionalBut, for large projects I've learned I don't care for it. Not unless the project, and team working on the project, follow guidelines and conventions. Otherwise it will devolve into a mess, guaranteed. And too many rubyist are hesitant to simply use rubocop to enforce this at a PR or something similar. It's like most of them forgot about `rake` and just having a formatting or linting task.
But i digress. I like all these changes, but it's just me (i wouldn't use them on an actual open source project yet). So Matz is right to know the `end` may end ruby (seriously, my students just _hate_ it) but i'm more curious to see if the community will agree and follow along.
Also, endwise is a great extension to abstract away most those `end`s being typed anyway.
> return [] if denied?
So strikingly simple and expressive, I miss that :)
Amusing in the sense of aaah good old Ruby still having the same development style that made me stop using it long ago.
What do you mean here?
The fact that they released this and are now considering a change in precedence is a showcase of the haphazard development of the lang. If the feature were useful, somebody could have tried using it and noticed the issue before they released it. But it's just a clever hack that adds another way how methods can be written. Nobody sane will use it, but the golfers cheer it into the release.
Somehow I always seem to find the ruby projects where everything stopped being supported years before.
It's not haphazard. It was an intentional tradeoff knowing that introducing new rules into the parser that would address this would delay introducing the feature, and that it might make sense to defer changing it until the currently ongoing process of replacing the parser is complete, and deciding that it was useful enough this way in the meantime.
That there are corner cases was well-known before it was released.
If you're going to criticise how this was handled, at least understand the issue before you jump to conclusions. It's perfectly fine to disagree with the tradeoff, but that tradeoff was intentional.
So the tradeoff is that the parsing rules of that operator may change in the future? Who would even use it if it could change again?
It was an obvious consequence of the grammar rule that was updated.
> My understanding is that it's already released, warts and all. Did I misread?
It was added on a experimental basis 3.5 years ago, and released in Ruby 3.0.0 in December 2020 (Ruby currently bumps the first decimal every Christmas day, current release series is 3.2, and 3.3.0 is due this Christmas.
> So the tradeoff is that the parsing rules of that operator may change in the future? Who would even use it if it could change again?
You can safely use it as long as you understand that anything that inside the method body might operate on a whole assignment expression will likely operate on the `def` if applied to an endless method which is almost certainly not what you want, in which case the solution is to either not use an endless def or use parentheses to make your intent clear. IF operating on the `def` expression is what you want, then you either need to avoid end-less def's or enclose the expression in parentheses or your code might break.
That is, this is safe (but means different things), as long as "some_expression" is safe to put as the right hand side of an expression (which might take parentheses or not):
def foo = (some_expression if bar)
(def foo = some_expression) if bar
def foo = some_expression
This is not: def foo = some_expression if bar
IA "def" in Ruby is just syntactic sugar for calling the method `define_method(name) <block>`, and so there's no reason you shouldn't be able to operate on a def by Ruby thinking because it's just another form of expression and disallowing it would involve a special rule for a specific situation.
You can do the same with classes, modules etc. ("class Foo; end if expression_evaluating_to_false" will not define Foo, same as if you'd wrapped it in "if false ... end). Even more so because a training "if" is just syntactic sugar for "if <condition> then <expression> end" and so given that Ruby made the choice that defining classes and modules and methods is allowed as expressions in conditional blocks, it'd be an odd exception to exclude them from training if's.
The Ruby philosophy there is that this is what falls out of the generic rules, so it's allowed, and maybe someone finds a reasonable use for it, or not, but disallowing it for the sake of it doesn't really fit Ruby.
> Why would that be allowed if it is likely to change?
That isn't likely to change, and it's not a new thing - it's been part of Ruby from the start. Only the precedence is likely to change exactly to bring it closer to the principle of least surprise. You will still be able to write "def foo; end if some_condition" because expectations there (to Ruby programmers anyway) are clear, or "(def foo() = expression) if some_condition". If it happens, any precedence change will be based on what will cause the least surprise if people leave out the parentheses.
Almost everything in Ruby can be thought of as a method call or expression - it's our inheritance from Smalltalk, which is perhaps the strongest influence on Ruby. E.g. a "if" or "case" expression also has a return value, just like "class" and "def". There's "special syntax" in terms of parsing, where Ruby's focus has always been "developer happiness" over purity, but extremely little special syntax in terms of semantics.
I still don't understand why they released the feature if operator precedence around it could still change. But then, I don't understand the need for the feature, nor would I want to conditionally define a method. I realize the older I get, the less I like late binding :-)
Any langauge feature can be used well or poorly, so singling out some of these newer language features - just to show bad use cases - is kind of misleading. It would be better to stop using intentionally inflammatory words like "useless" and instead show "when to use".
The one-line "endless" functions are perfect for actual functions that fit on one line without additional punctuation.
For example, custom string manipulation utility functions read like documentation.
def string_of_tags(tags) = tags.join(', ')
def fullname(person) = [person.first_name, person.last_name].join(' ')
Swift JS Go Java etc just feel a lot easier for me to read if that makes sense.
Not sure why. I’ve worked with ruby in the past, but never liked the syntax. Same with python.
We read code much more often than we write it. Conciseness is great if it’s reducing boilerplate. But when it hides type information and linkage, it tends to obfuscate more than clarify.
def foo(s) s.upcase * 3 end
foo "Hey" # => "HEYHEYHEY"
No ; and no => What can I say! Once in a while, I forget how to Ruby :) This does not make the argument invalid (this way of writing methods is still frowned upon and never used; and still, reads like “several phrases”), but it probably should’ve been centered around the community’s view at `;`.
Linting rules can help to enforce careful use of this syntax even on team projects. The ambiguity can be avoided or explicitly removed as demonstrated with the parentheses around the expressions. It's just another syntax option for defining a method of appropriate simplicity. Ruby already allows for some heinous things:
class Foo
private
# Not actually private
def self.bar
'bat'
end
end
define_method(:".38 Special, the band?") { name == '.38 Special' }
Be smart about how you use it. A knife manufacturer ships sharp blades despite the possibility that a chef might cut themself with it.This is usually said about necessary but dangerous or easily abusable features, like the use of raw pointers, C++ templates, or global mutable state.
I would say that adding potentially confusing syntactic sugar to a programming language is more like adding a feature to a Swiss army knife that is redundant and makes it unnecessarily more complex to use.
It lets me avoid my most hated pattern, where you instantiate a thing with a temporary variable, do some stuff to it, and then return the instantiated object with a hanging call to the temporary variable. I've always found it very ugly and also fragile if the order of operations gets mixed up by later edits. This pattern:
def do_the_thing(with_me)
thing = MyThing.new
thing.do_something_that_does_not_return_self(with_me)
thing
end
I also love to use it to avoid "double ends" for methods consisting entirely of control-flow statements: def do_the_thing(with_me)
case with_me
when rofl then ...
when lol then ...
when lmao then ...
else ...
end
end
With endless methods I would write these two examples as: def do_the_thing(with_me) = MyThing.new.tap {
_1.do_something_that_doesnt_return_self(with_me)
}
def do_the_thing(with_me) = case with_me
when rofl then ...
when lol then ...
when lmao then ...
else ...
endIt's surprising for me to hear this, because IMO, Ruby is among the most aesthetically pleasing programming languages.
And I don't think I ever go around worry about types. If the typing isn't obvious, then that's a big warning sign to me about how the code is structured.