14 ms·
I don't necessarily agree with the step of putting the code in a separate function; that often works, but just as often makes it so that the code can't be read
by kouteiheika 4y ago
I don't necessarily agree with the step of putting the code in a separate function; that often works, but just as often makes it so that the code can't be read top-to-bottom anymore which hurts readability.
In this case there's, I think, a better alternative; the equivalent-ish code in Ruby for the example code here would be something like this:
values = s
.partition('?')[-1]
.split('&')
.map { |key_value| key_value.partition('=')[-1] }
You can write these nice functional pipelines where you just read the code top-to-bottom and see step-by-step what is being done to the data on each line. You don't have to jump up-and-down around the code when reading it, and you don't have to keep too much context in your head when reading it.
This is one of the reasons why I vastly prefer Ruby over Python for most data processing tasks. I wish more languages would support this style of programming.
- somehnguy 4y agoLooks similar to Streams in Java. We try to use that style where appropriate as it is much more readable and compact than imperative style imo.
- vbezhenar 4y agoFor me that's questionable. Yes, there's code which fits finely with streams and looks very readable. Yet there's code which looks like it was shoehorned into a Procrustean bed and looks much better with ordinary loops.
- xigoi 4y agoNim, D, VimScript and other languages have “uniform function call syntax”, which allows you to chain arbitrary functions like this (not just the ones that the author decided to declare as methods, like in Ruby and other class-oriented languages).
- irq-1 4y agoI wonder how much of Go's simplicity comes from having "package.function" syntax that doesn't allow you to put too many functions in a single line? People complain about Java because of the length of the names. Do they complain about to many functions in line? Maybe with ( ? : ) Lisp has )))))) which seems like a stupid complaint, but maybe that's tied to people having to remember too much?
- jonnycomputer 4y agoI like this style too, though it can make debugging trickier.
- wizofaus 4y agoIt's been my feeling for a while that debugger technology hasn't really caught up with newer styles of coding (despite them having been around for over a decade). Only being able to set a breakpoint at a line-level or watch values that are assigned to a named variable is incredibly limiting.
- veltas 4y agogdb for a long time has been able to watch almost any expression at all. Unfortunately the visual studio debugger can't watch very much at all and that's what many of us are stuck with.
- wizofaus 4y agoVS's debugger can watch arbitrary expressions, but it's not necessarily what you want (and admittedly isn't always reliable, or causes severe performance issues depending on the language etc.). What I'd really like is the ability to set breakpoints anywhere at all on a line, including a line like `xyz.map(_ -> ...).filter(_ -> ...)` such that, e.g. I can set a breakpoint between map( ) finishing and filter being called such that I can see what the map function has returned/what will be passed to filter, and of course at any point inside the lambda functions. And it's not just VS (or even VS code), but Chrome DevTools, XCode, IntelliJ/Android Studio etc. etc.
- layer8 4y agoOne issue with functional pipelines is that the reader has to keep track of what the types and the data are on each line. It’s fine for 2-3 lines, but it can get non-obvious quite quickly. Assigning intermediate points to named variables can be appropriate, or indeed factoring portions of the pipeline out into separate functions.
- mdaniel 4y agoThat may be true for less capable editors, but IntelliJ (and therefore Rubymine for the cited code block) annotates the stream type variable when it can prove what it is: https://www.jetbrains.com/help/ruby/viewing-reference-information.html#inlay-hints https://www.jetbrains.com/help/ruby/viewing-reference-inform... regrettably doesn't show an example of what I'm talking about .map { | *String* key_value | key_value.partition... } where String shows up in light grey text indicating that IJ knows `key_value` is a String
- layer8 4y agoThat’s nice, but it just highlights that the coding style isn’t sufficiently intelligible by itself. We can’t and shouldn’t rely on a static analyzer to be active to make code intelligible everywhere code is displayed (pull requests, diffs, ...). Doing so favors writing code over reading code, which is generally the wrong bias. Editor support is appropriate and useful to help writing readable code, but it’s bad for editor support to become a prerequisite for existing code to be comprehensible.
- _nalply 4y agoOne way I try to solve that problem in Rust: use repeated `let thing = simple.invocation();`. No need to rename the variable `thing` if not neccessary. Rust allows redefining a variable with the same name. Additionally I sometimes give the thing a type which makes it even more explicit. Something like this: let thing: Thing = source.prepare(); let thing = thing.make_iterator(); let (first, second) = thing.split(); let first = first.filter(); let second = second.reverse(); let items: Vec<Item> = first.join(second); Just to give an idea how to make a complicated ritual explicit and simple. This simplifies debugging too. You can set a breakpoint and view the variables or you can use println!.
- khendron 4y agoOne of the advantages of functions is that a well-named function is self-documenting. If you can take a bunch of lines and wrap them in a function whose name summarizes exactly what it does, then you have improved readability in my opinion. In this example, I don't really need to know the details of how the query parameters are extracted. I just want to know I've got them.
- marginalia_nu 4y agoEmphasis on well-named. Naming things is hard. Maybe not relevant in simple toy examples, but you don't have to look far until to find a function that isn't so easy to name.
- nmz 4y agoits also a level of indirection... It's sad but functions often slow performance down. but this depends on the language.
- marginalia_nu 4y agoCan't be many languages where that matters in 2022. Auto-inlining of short functions was a thing in the 1990s.
- extasia 4y agoThis may be true for compiled langs. In interpreted langs this is not the case. Function calls in Python for example are notoriously expensive.
- nmz 4y agoBeat me to it. Yes, I ran a quick benchmark[0] and not running a function always wins. now, one may argue that you are running a call, but if the interpreter was smart it would convert the small function calls to just a noops. [0]: https://github.com/Nomarian/crapbenchmarks/tree/main/call https://github.com/Nomarian/crapbenchmarks/tree/main/call Summary 'Perl/nofunc' ran 1.63 ± 0.30 times faster than 'Perl/func' 1.73 ± 0.34 times faster than 'Lua/nofunc' 2.11 ± 0.55 times faster than 'Lua/func' 9.37 ± 1.62 times faster than 'Python/nofunc' 12.18 ± 1.90 times faster than 'TCL/nofunc' 16.02 ± 2.58 times faster than 'TCL/func' 18.64 ± 3.02 times faster than 'Python/func'
- leobg 4y agoAny way to do that in Python? Basically an anonymous function across multiple lines, which can be collapsed in the IDE view?
- vorticalbox 4y agoNot exactly the same but in python you can use pipe to create pipelines https://pypi.org/project/pipe/ https://pypi.org/project/pipe/
- gilch 4y agoPython's lambdas can have as many lines as you want. Just wrap parens around it. Hissp uses this form as a compilation target. Its REPL shows the Python compilation. Play around with it til you get it: https://github.com/gilch/hissp https://github.com/gilch/hissp
- Too 4y agoThat’s misleading. While you can have multiple lines, you are not allowed to have multiple statements. Nobody wants to write code chained together with ternaries and and-operators. When someone says multi-line lambdas, what people actually mean is multi-statement lambdas.
- gilch 4y agoThe misleading part wasn't from me. I'm trying to correct a common misconception here. Both "multiple" and "lines" are wrong. Lambdas can't have single statements either. The body is only an expression. So why are we saying "multiple" (a word to distinguish from "single") if you meant to multiply statements? The number of statements is zero. Does a try/except fit on one line? That's a try statement. Does a class? Technically possible, but typically no. That's a class statement. It's not just the "class" word, or the first line. The class statement includes its body. Many Python statement types can't fit on one line, and many others don't have to. So why are we saying "lines" to mean statements when these are not remotely the same thing? The statement/expression distinction is a holdover from FORTRAN. Statements are just expressions that don't return a value. (Yes, Smalltalk and Haskell can implement control structures with expressions just fine, thank you.) Many functional languages do away with the distinction altogether and just use expressions. You only really need lambdas in Python for the functional style, and in functional style, expressions are all you need!
- davnicwil 4y ago> the code can't be read top-to-bottom The idea of the technique is to split out code at a different level of abstraction with a clear name communicating what it does, while hiding the details of the how, because you don't need to care about that detail at all to fully grok the code in the calling function. Where this breaks down is when the code you're trying to split out is not at a different level of abstraction, and how it works is meaningful to the surrounding code in the calling function. So I think the issue you are seeing isn't with the technique, it's with the technique being misapplied. I think this is likely the only difference between when it 'often works' and 'just as often doesn't' in the code you're working in :-)
- shellback3 4y agoI agree, my thoughts were along this line especially when I noticed the 'magic' number -3.
- wpietri 4y agoThis is my experience too. I love hiding detail for readability, but you have to hide the right details! It's also definitely related to testability for me. If I'm pulling out the right details, then I'll often get a nice cluster of tests that pin down the higher-level concept in a way where it's both the production and test code that gets more readable.
- lamontcg 4y agoEach function becomes something new that needs to stick in your brain. Someone that applies "MORF" to their code winds up nearly inventing their own language in the file that they're writing. All that takes up more memory when you're reading their code, because due to leaky abstractions the actual implementation of whatever the function name that you replace it with is often important. I have an actual track record of taking code that someone had MORF'd to hell and rewriting it, and making it about 40% shorter, with much fewer concepts to process. Inventing a term like "MORF" is probably illustrative of the problem itself. Without looking at the blog post what exactly was that acronym again? That is just one more thing for you to try to memorize. The author is riffing on things like "DRY" and "YAGNI" that are well-known, but it isn't really helping with readability when you lift it out of that context.
- deleted 4y ago[deleted]
- e_i_pi_2 4y agoDefinitely a good point, refactoring is great and has a bunch of benefits but can also be overdone and create problems. I've heard/read Sandi Metz talk about this as "The Wrong Abstraction"[1]. The basic argument is that any time you do an extract refactor you're creating a new layer of abstraction that the next reader will have to learn and understand. This can also get worse over time as the abstractions drift away from their original purpose. The solution she provides is to be okay with a little bit of duplication, then as patterns naturally arise in the codebase you can refactor when you know a few use cases and can clearly define the concept. [1]: https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction
- chrisweekly 4y agoYes! IME (24y and counting in the profession) devs reach too quickly for DRY while neglecting its counterbalancing principle: AHA (Avoid Hasty Abstractions).
- wizofaus 4y agoI've seen this complaint before and it doesn't really align with my experience of frequently seeing literal duplication of code where no extra abstraction was required to avoid it - just selecting the code in question and factoring it out into a function, which, providing you named it sensibly, would also make the code easier to understand and debug. I could count on one hand the number of times I've seen code where repeating logic would make more sense than any extra levels of abstraction created to avoid it.
- lamontcg 4y agoThe point isn't to maintain the repeated code forever. The point is to generate the repeated code and (maybe) live with it for a bit until you understand what the right abstraction is and then remove the duplication with the correct abstraction. I've lost track of the number of times that I've taken badly abstracted code, reintroduced all the duplication, then refactored to remove the duplication with an entirely different abstraction and the code was many times easier to read. And have you lived in a codebase for many years, where you've never come back to a method that you extracted and realized that you named it wrong? Or the requirements of the code grew and the duplication wasn't really at the level that you picked and you wind up with a method that grows all kinds of horrible options to change its internal behavior?
- KerrAvon 4y agoThis style is increasingly common in Swift, especially for Combine pipelines and SwiftUI modifiers.
- WalterBright 4y agoD does too: import std.algorithm, std.array, std.stdio; // Print sorted lines of a file. void main() { auto sortedLines = File("file.txt") // Open for reading .byLineCopy() // Read persistent lines .array() // into an array .sort(); // then sort them foreach (line; sortedLines) writeln(line); }
- deleted 4y ago[deleted]
- tomlin 4y agoRuby maps are so ugly. .map { |key_value| key_value.partition('=')[-1] } Reading this literally makes me sick to my stomach. Language design is much more important than language popularity, although it will be popularity that wins. (Yay downvotes for pointing out things everyone can see - highschool dynamics)
- samtheprogram 4y agoJust because it’s a different syntax than you’re used to reading in another language doesn’t make it ugly. If you’re used to reading it and work in the language regularly, it actually looks quite clean. This sounds like a Windows user who can’t stand macOS because they don’t know where anything is. Your post downvote edit assumes your opinion here is objective. It isn’t.
- tomlin 4y agoParentheses as pipes is what you do when you’ve backed yourself into a corner. Ruby has decided (for some reason?) to use two ways to declare. Function(param, param) and { |param, param| } why? What is a good reason for this? It would be an anti pattern in design, not sure why Ruby gets a special hall pass.
- klibertp 4y agocollect: [ :keyAndValue | (keyAndValue splitAt: '=') last ]. is why. EDIT: (it's been a few months since I used it, so I) made a mistake in the syntax (declared local variable instead of a formal block argument), it's fixed now.
- deleted 4y ago[deleted]
- _8j50 4y agoSub routines are underused. Make it a function right where it is?
- watwut 4y agoNo, this pipeline is not more readable. It is a annoying to have to constantly have to read it. It makes it harder to figure out what larger algorithm and design is.
- BiteCode_dev 4y agoI don't know, it doesn't seem very far form the python version: values = ( key_value.partition('=')[-1] for key_value in s.partition('?')[-1].split('&') )
- oriolid 4y agoThis is already showing why the the Python list/iterator comprehension syntax isn't great. The part "s.partition('?')[-1].split('&')" reads from left to right, and then the rest is read from end to beginning. It gets even more confusing with nested comprehensions. In my opinion both the dotted pipeline style and the Lisp style where you always start from the deepest nesting level are both more readable than Python's approach with sometimes from left to right, sometimes from right to left and sometimes from middle out.
- atoav 4y agoI build most of my classes that way in python. class Motor: def __init__(self): self.max_speed = 10.0 self.clockwise = True self.controller = Controller() def set_max_speed(self, max_speed: float=10.0) -> 'Self': self.max_speed = max(0.0, max_speed) return self def start(self) -> 'Self': self.controller.start() return self etc.. Then you can use it as follows: motor = Motor().set_max_speed(5.0).start() There is nothing stopping you from building iterators that work the same way