[HN Gopher] "Useless Ruby sugar": Endless (one-line) methods
___________________________________________________________________
"Useless Ruby sugar": Endless (one-line) methods
Author : todsacerdoti
Score : 99 points
Date : 2023-12-01 14:11 UTC (8 hours ago)
(HTM) web link (zverok.space)
(TXT) w3m dump (zverok.space)
| polygamous_bat wrote:
| 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?
| stouset wrote:
| 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.
| progne wrote:
| 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.
|
| 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.
| Pxtl wrote:
| 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.
| 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;
| vidarh wrote:
| 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.
| mschuster91 wrote:
| > 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.
| kbenson wrote:
| 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?)
| stouset wrote:
| 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.
| kazinator wrote:
| Ask a typesetter? No thank you.
|
| 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.
| sdf4j wrote:
| > We have a soft limit on class length of 100 lines, and I
| like the extra conciseness this allows
|
| 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?
| progne wrote:
| 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.
| jlarocco wrote:
| Seems counter productive to me.
|
| 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.
| kazinator wrote:
| TXR Lisp: 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.
| vidarh wrote:
| 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.
| stouset wrote:
| 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.
| vidarh wrote:
| 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.
| giraffe_lady wrote:
| 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.
| SideburnsOfDoom wrote:
| C# has something similar, but uses the existing lambda syntax,
| with "=>" to the left of the body or expression, e.g.
|
| int AddOne(int x) => x + 1;
|
| or
|
| void SetXToZero() => x = 0;
| polygamous_bat wrote:
| 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?
| Pxtl wrote:
| 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.
| Someone wrote:
| 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.
| ysavir wrote:
| 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)
| kagakuninja wrote:
| Not a Ruby guy, but Scala uses that syntax for all method
| declarations: // 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 optional
| lolc wrote:
| It's amusing how they released it without nailing the arity
| first.
|
| Amusing in the sense of aaah good old Ruby still having the same
| development style that made me stop using it long ago.
| mcphage wrote:
| > without nailing the arity first
|
| What do you mean here?
| lolc wrote:
| 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.
| frou_dh wrote:
| The following already worked in Ruby for single-line method
| definitions: def foo(s) s.upcase * 3 end
| foo "Hey" # => "HEYHEYHEY"
|
| No ; and no =
| lcnPylGDnU4H9OF wrote:
| 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 `;`.
| stouset wrote:
| Introducing new, ambiguously-parsed syntax to avoid an "ugly"
| semicolon or a bare `end` for code-golfing purposes is an
| extremely poor trade.
| lcnPylGDnU4H9OF wrote:
| 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.
| trealira wrote:
| > 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.
| lgkk wrote:
| I have always struggled with languages like ruby. def something
| do end.
|
| 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.
| owlstuffing wrote:
| 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.
| vidarh wrote:
| That's funny, because to me what I love about Ruby is above
| all that it is the easiest to read language I have worked
| with, of several dozen.
| oglop wrote:
| 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.
| t0mek wrote:
| This is similar to Kotlin single-expression functions, which are
| actually pretty useful: 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()...`
| dragonwriter wrote:
| There's no single line constraint in Ruby, either, _as the
| article mentions_ ; its a single expression, as in Kotlin, not
| a single line.
| adamgordonbell wrote:
| Yeah, I'm a fan of this.
|
| 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?
| MrBuddyCasino wrote:
| 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.
| marcellus23 wrote:
| 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).
| mvdtnz wrote:
| Hideous language, just hideous.
| trealira wrote:
| Hideous? Why? Are the "do ... end" blocks really that bad?
|
| It's surprising for me to hear this, because IMO, Ruby is among
| the most aesthetically pleasing programming languages.
| mvdtnz wrote:
| 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.
| trealira wrote:
| Those are fair critiques. Thanks for the response.
| trevor-e wrote:
| 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.
| whstl wrote:
| 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.
| ht85 wrote:
| I call it "assembly ruby", where registers start with "@" and
| opcodes are defined with "def".
|
| So dreadful.
| cratermoon wrote:
| https://thomascothran.tech/2023/11/readability/
| JasserInicide wrote:
| What is wrong with this example exactly? This is what you want
| in a service object.
| vidarh wrote:
| 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.
| ljm wrote:
| 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.
| vidarh wrote:
| In this case, though, it's not even producing shorter
| code. It's producing longer, less readable code.
| lstamour wrote:
| 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.)
| codr7 wrote:
| It's not only about DRY, it's also about encapsulating
| implementation details that you might want to change.
| vidarh wrote:
| 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.
| lstamour wrote:
| 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.
| vidarh wrote:
| 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.
| mike_hock wrote:
| 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.
| lstamour wrote:
| > 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
| ;-)
| mike_hock wrote:
| 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.
| whstl wrote:
| 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.
| michaelteter wrote:
| 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" :).
| whstl wrote:
| 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.
| vidarh wrote:
| 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.
| silasb wrote:
| 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.
| whstl wrote:
| 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 :/
| pkkm wrote:
| 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.
|
| [0] https://qntm.org/clean
| whstl wrote:
| "Mentally straining" is a good way of putting it. It also
| makes debugging much harder, as you have to keep track of
| instance variables in your head.
| ht85 wrote:
| If only functions could take arguments...
|
| 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
| whstl wrote:
| 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)
| vidarh wrote:
| No?
|
| 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.
| michaelteter wrote:
| 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.
| whstl wrote:
| 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
| fknorangesite wrote:
| 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.
| eurekin wrote:
| Haven't worked in ruby in years, but reading it just feels nice:
|
| > return [] if denied?
|
| So strikingly simple and expressive, I miss that :)
| lemper wrote:
| 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".
| Lammy wrote:
| 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
| hoosieree wrote:
| "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
|
| to
|
| stand
|
| OUT!
| mhink wrote:
| 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:
| 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.
| kevincox wrote:
| 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.
| JohnFen wrote:
| Good formatters _do_ understand the code, though. They
| parse it much like a compiler does.
| kevincox wrote:
| 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.
| vidarh wrote:
| I've yet to experience a good formatter.
| larschdk wrote:
| I tried your code on prettier.io and got a result that was
| more visually consistent than what you pasted here.
| oprypin wrote:
| 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.
| vidarh wrote:
| 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.
| humanrebar wrote:
| 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.
| munificent wrote:
| 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.
| a1369209993 wrote:
| > 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).
| vidarh wrote:
| 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.
| michaelteter wrote:
| 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.
|
| def string_of_tags(tags) = tags.join(', ')
|
| def fullname(person) = [person.first_name,
| person.last_name].join(' ')
___________________________________________________________________
(page generated 2023-12-01 23:01 UTC)