“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 things
This is my biggest gripe with the widespread use of Prettier over in JS/TS land, especially when using React. It always seems to introduce *more* formatting inconsistency because it's only looking at line length to decide when to format. So in the same block of code I'll have things like:
This is also my main gripe with automatic formatting. Since they don't really understand the code they almost all work based on number of characters in a line. However I really don't care about characters in a line (except for at the extremes). What is much more important is how many ideas are in the line. If I have an array with 3 very interesting elements I don't want them on the same line even if they happen to be quite short.
They understand the syntax of the code. They may even understand the types and relationships. They don't understand meaning and can only have very rough estimates at logical complexity.
I think the commenter meant that it can produce such an inconsistent result if the identifier names add up to approximately the line length, but the example didn't actually make it so; the identifiers are much shorter than that.
This is a huge reason why I still use StandardJS and—shifting back to Ruby—why I rejected the countless requests for implementing line-length or any other metrics analysis rules for Standard Ruby (https://github.com/standardrb/standard). There is always a legitimate edge case when it comes to length of lines and functions and the alternative—chopping them off arbitrarily—is rarely an improvement.
Prettier always makes me want to rage-quit. Thankfully I'm at a career level where I usually have the power to dictate it be removed. A formatter that does not understand or allow for careful alignment to make concepts clear is a formatter I don't want near my code.
It's part of my job to "railroad" potential majorities that would prefer to do things that my judgment suggests will make things worse.
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.
As someone who maintains an automated formatter (Dart format), I agree that automated formatting raises the floor and lowers the ceiling. But my experience is that it raises the floor by a large amount and lowers the ceiling only a little. Most importantly, it reduces the engineering cost to reach the floor to zero.
> But my experience is that it raises the floor by a large amount and lowers the ceiling only a little.
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).
The problem for me is that the effect is that you almost never make it above the floor. Maybe I'd have a different view on it if I had to work with a bunch of beginners, but I luckily don't.
Counterpoint: if you want tools to help you edit your code, like linters that supply fixes or smarter autocomplete tools, you need algorithmic code formatting.
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.
My problem with Ruby is not so much the syntax. I kinda like this one-line-function feature, and I enjoyed it when using in C#.
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.
This is just the Clean Code style. If you read the book (or an article with examples [0]), you'll see that it advocates exactly this kind of thing: refactoring functions that are "long" and "unreadable" because they have a dozen or two dozen lines into a "clean" and "maintainable" web of tiny functions calling one another. Personally, I find this style very mentally straining because I have to constantly jump around and keep track of a huge function call graph in my head; it's almost as bad as beginner code with a thousand lines per function. The popularity of fads like this in the Ruby community is one of the reasons I'm glad I switched to Python.
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
Slightly less worse, but the original function that I inputted into ChatGPT to generate the code from my message was this:
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)
I think the choice between a method (ruby doesn't have functions ;) ) and a class here may well be right. The only reason for using a class here is more as a general principle if you want it to be easier to pass your "usercreator" around as a value, but you can of course wrap it in a lambda if needed or use `#method`. Alternatively you can just make it a lambda from the start. If your main objection to the class is the ease of breaking encapsulation, lambda's are quite nice for tighter encapsulation of state in Ruby.
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.
In a way, it seems to have similar problems to those of unstructured programming (i.e. rampant use of goto instead of procedures and loops), just to a lesser degree (since jumps that always return are still easier to understand than ones that don't). It's hard to understand the code when the control flow jumps everywhere, even when all the jumps are subroutine or method calls. A method having a good name doesn't necessarily mean it provides a better abstraction than writing the code inline, just like having well-named labels doesn't necessarily make the code easier to understand.
As a Ruby developer, I can't stand this style either. People ought to be aware that to many of us this does indeed also screams "beginner". It shows people slavishly following advice they don't understand.
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.
While I personally prefer more functional (non-oop) approaches, the example you show is positive to me.
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 problem is that I need to know HOW this class does something, especially when debugging.
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)
end
Yes, but that's because the example we were given in this thread is just a toy strawman in that each of the methods are just simple one-liners anyway. So yeah, given this literal code sample, I agree the private methods are extraneous.
The 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.
This is why it's difficult to have discussions about this. Pretty much anything that's bad enough will be countered by a "well, this is just a toy strawman", and status quo will prevail.
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.
I agree that is the point of this pattern. But in practice most places where I see it employed are full of single line method bodies because people have heard of the pattern but don't understand the (fairly frankly rare) instances where it is appropriate and/or don't understand when an operation is long enough that it makes the code clearer to break it out.
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.
This is much more of a Rails thing than a Ruby thing, and as someone who loves Ruby, I agree - when I come across code like that it is cringe-inducing.
The methods that start with “call_” and “find_” are particularly redundant, because they aren’t any simpler than the code they contain. Technically true of all the methods given but the rest could be excused as explaining less readable syntax or for future expansion if parameters change. Modules should be deep, not shallow, is one design approach. This half fulfills that because the methods are private, but it’s also true that the method names are effectively comments yet they obscure the natural reading order for the code. See John Ousterhout’s A Philosophy of Software Design for more on this idea of deep vs shallow modules.
(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.)
Then encapsulate them when you do. In this case these methods are private. They're almost certainly not used anywhere else.
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.
The “YAGNI” principle is something I’ve had to learn the hard way - and continue to learn it, just earlier this week, in fact. YAGNI and DRY are sometimes in vicious opposition. A pro tip I picked up from Gary Bernhardt’s Destroy All Software screencasts is to leave some DRYing (or other refactoring for readability/maintenance purposes) until after you write tests and are in a red-green-refactor cycle. You can always DRY your code later, especially now that IDEs make it so easy to extract methods for you with automatic population of required variables/parameters.
Personally I think DRY only makes sense as long as either the code is shorter (here it's longer...), or you encapsulate logic, especially when that logic will be called from multiple places in the code base. Here it won't...
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.
How about the JUCS principle - Just Use Common Sense. What do you want to see when you come back in half a year to make a change? Write 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.
In this case, the issue is that they've tried to "DRY" up things that don't actually reduce any repetition. Yet. Maybe it will in the future, then DRY it up then. Secondly, DRY is meaningless if applied at units so small that you replace one invocation of a method with another invocation of a method when the arguments to the first one are obvious. E.g. consider this from the example:
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.
> 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.
> 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 ;-)
This over-generalization of vague concepts isn't helpful.
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.
And as most commentors totally miss, this implementation "hiding" (encapsulation) reduces coupling and makes writing tests MUCH easier.
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" :).
All methods are private except for the initializer and create_user.
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]
This implementation isn't hiding any more details than a single method.
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.
Information/implementation hiding isn't for some kind of security or access protection; it's to reduce the cognitive load of understanding a system. By having the main functions before the private, and then having any number of simple or complex private implementation functions, we make it clear to the reader what the contract is. If the functions are well-named, the reader can just read up to the private and stop. They will understand what is going on without having to know any of the "how" details. That's a very good thing. In fact, we well written system using this approach can be read and understood by less technical people (also a good thing, for many reasons).
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.
Reducing the cognitive load is exactly the opposite of what the micro-functions pattern achieves.
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.
It’s true that if your goal is something like Cucumber BDD which can be read by non-programmers, then you would want an abstraction layer over the code that hides some of the syntax. This is also where we could get into Ruby being used as a DSL. The only gotcha is that the reason AppleScript is so hard to write while being so easy to read is basically the same reason we use IDEs to write Python and Ruby and generally prefer TypeScript to JavaScript. Namely, the coding assistance.
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.
In this case the implementation is barely longer than the bare method-calls. It doesn't hide complexity, it increases it, because odds are you'll need to chase down at least some of those methods calls to be sure what it does.
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.
The encapsulation here is exactly the same as you would have in a 6-line function.
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.
As I think we've both pointed out, it's actually less encapsulated, since you can do dirty stuff like `send` or `instance_eval`, while we can't "break into" the internal state of a single method.
It's reducing readability by introducing extra methods you have to chase down where the method content is just as clear as the method names themselves. If there are multiple clients that might use those individual methods then maybe some of it is excusable. Personally I'd refuse to accept a commit like that if reviewing that code.
I notice this pattern often appears as a result of taking rubocop’s default settings as gospel. It pretty much forces you into pointless abstraction by saying a method should be less than 5 lines, or a class less than 100, or whatever, even though such abstraction would offer no actual value.
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.
The operations themselves are fine but the code style gets in the way of readability.
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.
I don't mind this style, it's encouraged by dry-monads with the do-notation. The big concern that I have with this code is it's missing a service level transaction.
I don’t have a problem with small functions in funcional code, including code built around dry-monad, but this code is definitely not in this category :/
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()...`
I don't really understand what makes that better than something like (from e.g. Swift):
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).
One of my least favourite Kotlin features. Breaks the uniformity of method declarations for minuscule gains, making it harder to visually parse a class body quickly.
Am I the only one whose soul hurts looking at this syntax? Not a ruby user, but as soon as I saw the use of = to define the function body I immediately thought of all the ambiguous statements one could write with that. And voila, the whole irks and quirks section validated those fears.
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?
I’ve been using Ruby professionally for 15 years, and have a deep love for the language.
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.
Just 12 years full time with Ruby here, I'm a noob. But when I see that I could replace
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.
Unnecessarily cramming more density into one line to avoid soft limits seems like a poor tradeoff.
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 feel like I'd still want the body on its own line from the signature definition just for readability. And with the parser problems described in TFA, I'd say that parens should be mandatory coding-style enforced by linter.
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;
Personally I'd never let your first example through a code review. If you're going to break it up, use end. Where the one line format makes sense is when the body is trivial and often when there are multiple related methods that can be lined up to highlight their similarities and differences in the single line format.
> 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.
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.
You wouldn't define an `inspect` method for this simple example in Ruby either, because Ruby provides a default inspect that'd do just fine in this example. The exact same tradeoff for when to define inspect as for when you'd define print methods exist. In other words, that wasn't the point.
I agree that it shouldn't be used for anything complex, even if it reduces to one line. But I do like it for simple things. Maybe it's staring at very similar code all day, but such short statements read fluently for me.
And even if an extra line of separation is used, it's still 6 lines instead of 11.
> We have a soft limit on class length of 100 lines, and I like the extra conciseness this allows.
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.
What can be good, but requires discipline, is to replace some of the syntax that's no longer needed with a comment to help explain the what and why and goal. You reduce or eliminate the line savings, but overall may increase the information and understanding of intent.
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?)
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.
19 years here, and my terminal, editor, file manager and X window manager are all pure Ruby so as you can tell I love Ruby a lot, and frankly I love these changes - they've allowed me to reduce the size of my code in ways that makes it clearly more readable.
Nobody forces you to use them. I for one is extremely happy with most of the changes in recent years.
I am forced to read them and fix bugs related to the ambiguous syntax parsing.
“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.
If you're not in a team where you're in a position to influence the choices used, this will be far down the list of your problems. My sympathies.
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.
> 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.
Resorting to semantic-lawyering doesn’t add to your argument, it detracts from it. You know the point I was making.
I don't think the point you were making has any validity. It's not semantic-lawyering given that other ways of defining methods are common in larger Ruby code-bases, so you're already going to need to deal with a variety of far more complex things. If "end-less" defs is enough to throw you, then perhaps Ruby just isn't the language for you, and that's fine.
Not a Ruby user, I expect it is because they want as few implementation details as possible to seep into the syntax.
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.
Exactly what I am talking about when I say use some other unambiguous token. = is so incredibly overused in a programming language, why give it even more jobs?
This is my reaction as well. I like the idea of collapsing a simple method into a single line, but the `=` makes it such a weird thing to actually parse. I wonder what prevented them with using a proc-like braces syntax, which would be intuitive for those familiar with the language (and the `def` to set it apart from procs)
{} is already overloaded for both blocks and Hash literals, both of which commonly occur at the end of the argument lists. It seems like it'd create far more opportunities for making the code hard to read.
imho the "def" keyword saves it from being too ambiguous, since it's clearly defining a function/method.
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.
I've never seen this used, it's obscure and seems to be disliked/avoided by most people who do know of it. Flat out against the style guides of a lot of ruby codebases.
I like Ruby for my own projects because of how descriptive it is for when i come back to code later and need to understand what i did, and it requires possibly the fewest keystrokes of any modern language, which is a plus for my RSI hands.
But, 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.
Sorry I was short. I mean operator precedence was not considered. In the first implementation, 'def =' has higher precedence than 'if'. Which leads to surprising behaviour with tailing ifs. The if is evaluated to determine whether the method should be defined, instead of being part of the expression.
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.
"def =" isn't operator. It doesn't have a precedence. "=" is an operator, and has a precedence, and in ruby "def some_method ... if something" is an entirely valid expression, and not one you can arbitrarily ban for all def expressions because it is occasionally used (not a fan of using a trailing if here, it's rarely obvious).
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.
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
Thanks for the clarification. The whole thing is pretty far from the principle of least surprise for me. But so is operating on a def. Why would that be allowed if it is likely to change? Oh well, not that I needed more reasons not to use Ruby.
A "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.
Tanks for the context, I see what you mean about not disallowing things.
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 :-)
I can't speak for ruby development, but I can tell you literally every RoR project I've ever taken on has me losing even more respect for the community as a whole.
Somehow I always seem to find the ruby projects where everything stopped being supported years before.
Many people still argue that loops are more readable than collection operations ("comprehensions" in Python). And many of us here, myself included, would argue that there are plenty of perfect cases for 1-line collection operations (such as each/map etc.)
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.
Agreed. It’s not like readability is purely subjective either. Ruby and to a bit lesser extent Kotlin sacrifice readability for conciseness, which in my view is a terrible trade-off.
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.
I'm not sure when the author added their update but they responded to this point:
> 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 `;`.
I don't disagree, per se, but I think the point is stated too strongly.
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.
> A knife manufacturer ships sharp blades despite the possibility that a chef might cut themselves 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.
I don't think "one line" should get associated with endless methods. I really love endless methods for any situation where the first statement in my method is also intended to be the return value.
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 ...
end
in the ruby's ticket page that is linked in the article, one guy (who seems to be a respectable gentleman, I must add), this feature adds more cognitive load, requires all ides to support this syntax, and encouraging less readable code. coming from other programming language, I don't think that this feature is adding more cognitive load. especially when I believe any Ruby off Rails developer is also write js code. nor I do believe that it encourages less readable code, even though I don't deep dive into ruby's traditions, I believe stupid long one liners are the enemy of "happy developers".
In this case, hideous because of the overloading of =. But in general it's just an ugly inconsistent mess of a language. It can't decide if it's supposed to be expressive and read like English (begin, end, rescue, unless, etc) or needlessly terse by omitting random vowels (elsif, strftime, uniq). And then there's the utter mess that large codebases get into with metaprogramming.
For my brain it's an incredible amount of noise to parse through compared to {}. Especially in addition to the amount of things you have to keep in your head since there is no typing to rely on.
To me the incessant {} adds an incredible amount of noise.
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.
to
stand
OUT!