9 ms·
I'm curious, does every else find this let persons = names .map(Person.init) .filter { $0.isValid } easier to read than this? var per
by seanalltogether 10y ago
I'm curious, does every else find this
let persons = names
.map(Person.init)
.filter { $0.isValid }
easier to read than this?
var persons: [Person] = []
for name in names {
let person = Person(name: name)
if person.isValid {
persons.append(person)
}
}
I understand and appreciate the value of compact code, but I find the first one harder to read. A lot of inferred/token based coding is harder for me to mentally parse.
- ryanschneider 10y agoPersonally, I prefer the map/filter approach because people is now a let constant, but I work at a startup with people that don't come from a software development background that would have next to no idea what that line even means. So don't forget your code's target audience, I've personally had to back off some of the more functional styling in Swift just so I'm not the only one that can maintain our code base. For that reason, I really wish Swift had list comprehensions, just because it's the first "slightly functional" exposure most non-developers get if they come from Python.
- tekromancr 10y agoI don't think it's THAT difficult to learn to understand functional style programming. I like to think that I would consider trying to show my team mates the joys and positive aspects of functional programming before I just wrote them off, were I in your situation.
- pklausler 10y agolet persons = [ p | n <- names , p <- peopleNamed n , isValid p ] is even better, if your programming language has comprehensions.
- aninhumer 10y agoEh, maybe for more complicated examples, but I think that comprehension is a lot more complicated than: filter isValid (map peopleNamed names)
- pklausler 10y agoExcept that doesn't support multiple people with the same name.
- aninhumer 10y agoWell I was basing mine on the original. If peopleNamed in fact returns a list, then I'm not really sure what it's doing to create that? (I assumed your example was Haskell, in which case I don't think it can do anything particularly useful) In any case, all you'd need to add is something like concat.
- shados 10y agoits almost a wash in this case, but when you start having a very complicated flow, the first way becomes easier and easier in relation. It also depends on the language obviously. In this example, the first example has special tokens for the filter. It doesn't have to be so.
- macintux 10y agoThe first is much more declarative. When everything in the language looks and acts like that, it's pretty easy to read. When it's optional, well, sadness ensues.
- virmundi 10y agoI think part of the answer is that for many functional is a new hotness. The same thing happened with Scala (probably still does). With type inference and functional shortcuts, you can get a lot into one compact space. This is "impressive". The other thing is that experience brings the ability to track what's going on. The formulation of the answer here is probably new to you. As you get use to this, or Streams for Java, or threading for Clojure, etc, you'll understand it by default.
- melling 10y agoIf you learn Haskell or OCaml, you'll prefer the first snippet. I took a couple of MOOCs and blogged how I incorporated what I learned into my Swift: https://h4labs.wordpress.com/2016/09/30/functional-swift-using-array-filter-and-foreach-functions/ https://h4labs.wordpress.com/2016/09/30/functional-swift-usi...
- niklasrde 10y agoI think it depends what you're used to. I used to be a Scala programmer, then I moved to JS. At the moment I'm working in Objective-C, and I literally just wrote a loop to filter an array and form a new array with the results like the one in the example - and god I wish there was an as straight-forward, easily understandable functional way in Obj-C to do it like in the Swift example. I miss these 'nice' things - I don't need the fully functional Haskell package, but I like having some of the nice things Swift has, just because I got used to them and can actually write and read them better, and it is definitely more elegant!
- antfarm 10y agoIt's ugly, but possible: NSArray *filteredArray = [array filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id object, NSDictionary *bindings) { return [object test]; }]];
- pjmlp 10y agoYou mean something like: a := #(1 2 3 4 5). Transcript show: (a printString); cr. b := a select: [:x | x \\ 2 == 0]. Transcript show: (b printString); cr. Sadly Objective-C isn't really Smalltalk.
- donut2d 10y agoI used to find the first example more difficult to read until I learned Haskell. Now I find it much more explicit about what's happening. In the first example, because I know what "map" means, I know that `Person.init` is applied to each name. And then I know that only the valid `Person`s are returned by the `filter` call. In the second example, I have to understanding the unique logic of the loop block to get to the same conclusion.
- chc 10y agoYes, I find the first one easier to read. It might be me being weird, but I suspect you just haven't built up the instincts to read the first one more quickly. There's no "magic" or anything tricky going on here — it's just a normal use of map and filter. Even good and useful things can seem less readable before you're used to reading them.
- svachalek 10y agoI think if the second one is easier, you've more or less been taught to think like a microprocessor. That happens to most of us after a few years of writing imperative code. The more abstract functional approach is, however, conceptually simpler and more powerful at the same time. (For example, the first one is completely open to being performed in parallel.) With a little experience, functional programming is quite straightforward. Practically everything comes down to map, filter, and fold, or whatever they're called in a given language.
- ifdefdebug 10y agoI can't help thinking like a microprocessor. Even when mixing functional style into my code, my brain tries to get a grasp on how it's going to be processed. So the first example looks to me like two loops (hopefully the compiler does a better job on that), while the second one is obviously one loop. I am already trying to guess how a compiler would parallelize that...
- bad_user 10y agoThe second one might be obviously a single loop, but it is not obvious that it is O(n) and depends on the implementation of "append". It also tells you nothing about how the memory gets accessed and I'm assuming here that the for loop works just as well over linked lists and over arrays. In other words your knowledge that this is a single loop can be very misleading when thinking of expected performance. Which is fine because we work with abstractions such that we don't have to think about such details all the time, because that's not doable. The first example is simply more abstract. It might be a single loop in case the implementation is lazy, it might be two loops in case it is strict. However, look closer. Those filter and map operations can be applied to basically anything you want, including asynchronous / infinite streams. Your second loop on the other hand would have a hard time being translated to anything else than something that works on finite lists and that builds a finite buffer and not doing anything else while doing this. Or in other words, your knowledge about this manual loop is not transferable ;-)
- 10y ago
- nickbauman 10y agoFor me the problem with Haskel (and, by extension, Swift) is the syntax is very complicated. The example above is better taught through a functional language that has almost no syntax like a Lisp. (filter (fn[p] (p :valid?)) (map (fn[n] (Person n) names))
- aninhumer 10y agoHaskell's syntax isn't actually that complicated. There are some common functions with operator names that can be hard to read if you're not used to them, but those are library defined. The syntax itself is actually fairly simple. And incidentally, this would probably be something like (EDIT: made example more realistic): filter personIsValid (map initPerson names) in Haskell. Which looks much cleaner than the lisp to me.
- Scriptor 10y agoThat's because your code assumes those convenience functions exist while the parent's code writes out the anonymous functions. The direct translation of yours' to Clojure would be: (filter valid-person? (map init-person names)) Meanwhile, the direct Haskell translation of the parent comment is: filter (\p -> isValid p) (map (\n -> Person n) names)
- BrandonBradley 10y agoOther than the $0, yes I do. Easier to read and think about in respect to operations on a collection.
- hota_mazi 10y agoIt's a matter of habit more than code readability. Five years ago, I would have found the second form easier to read. These days, not only do I find the first form much clearer but I find the second one a bit smelly because it mutates data. It's actually surprising how quickly you get used to the first form of code once the language you use supports it.
- jfoutz 10y agoIn this case, it's unfortunate that there's no, "will this name create a valid person object?" predicate. much much better to filter the names, then make the objects. In this case, as long as append is O(1), i think the imperative version has a big benefit, it avoids building the name size list of persons. If you've got a billion names and 2 valid person objects, the imperative version is a big win. Of course that predicate i mentioned is the right way to go though. I think the right way to do it is with a fold. But that's not built in, so hard to expect of novices. I see what you're saying about mutation, i guess i have a higher tolerance for mutating stuff that you have the only reference to. I'm not really sure doing a += [validPerson] is much of a win. (but i think would be the right answer in a fold)
- lkitching 10y agoMost languages implement map and filter in terms of lazy sequences so they would not allocate an intermediate list (Scala is an exception but I believe you can request laziness).
- jfoutz 10y agohmm, i don't think lazyness is enough. for example, in the billion names example, let's say the first and last elements are valid. at the first step you'll wind up with something like validPerson : thunk after n steps where n < 1e9 validPerson : invalid : invalid : invalid : ... : thunk then finally at n = 1e9 validPerson : invalid : ... : validPerson because the intermediate calculations still need to happen. getting that first answer is cheap and quick. getting the second answer requires evaluating those billion - 1 thunks. maybe you're thinking of list fusion? AFAIK, only haskell does that. Perhaps i'm misunderstanding your point. But even then, lisp, scheme, smalltalk, python and perl don't implement map and filter in a lazy way. Of course, i'm thinking in terms of haskell lazy, perhaps, again, i'm misunderstanding and you mean something else. In retrospect, i think i'll revise my opinion and say map.filter is probably a bigger code smell than referentially transparent mutation.
- Jtsummers 10y agoPractice will help make the former more readable. It helps, for me, that I have more of a math background (academically) than CS (dual majors, but strongly preferred the math coursework). For me, I see three ways to write, as an example, a summation: n Σ g(i) i=1 vs. seq(1,n).map(g).sum() // or something similar vs. for(i = 1; i < n; i++) sum = g(i) In my mind, the first is what I see, and is what shows up on paper. The second is what I want to write (and will in any language that lets me). The third is what I often have to write when a language requires that I be more explicit (like C).
- solidsnack9000 10y agoThere is a certain straightforwardness about the second format, because it is general and plain. But at the same time, it could be anything -- it's much harder to tell at a glance that a for loop is a filter than it is to tell that `filter` is a filter.
- benhoyt 10y agoI definitely hear what you're saying. In this particular case I find the more succinct map/filter a little easier to grok, but as soon as you have a bunch more clauses with some flatMap() and reduce(), the "functional" way can get out of hand quickly. In simple cases, I prefer (Python's) list comprehensions. In more complex cases, I prefer the loop(s).
- chc 10y agoI find Python's list comprehensions mind-numbing in many simple cases, such as [x for x in xs if some_condition(x)] rather than just xs.filter(some_condition). We're repeating the variable name three times and zero of those times convey any useful information, and we don't find out until the very end of the line that the everything except "[", "]" and "some_condition" was ignorable. And in cases where a data-deriving loop has so much going on directly in the loop body that it makes map/filter/reduce hard to read, there's very often some refactoring that would improve either version.
- jstimpfle 10y agoThis is the one case where python's list comprehension syntax isn't so efficient. Don't worry about it - be consistent and use it nevertheless. It doesn't matter much. In the cases where x gets also transformed or you do some unpacking the syntax is very efficient and easy to read: [f(y) for x, y in items if g(x)]. Basically it's a poor man's relations programming (think databases). It's brilliant. (And definitely easier to read and write than the Haskell list comprehension syntax).
- Chris_Newton 10y agoIn this particular case I find the more succinct map/filter a little easier to grok, but as soon as you have a bunch more clauses with some flatMap() and reduce(), the "functional" way can get out of hand quickly. Funnily enough, I find exactly the opposite: for me, the functional style is significantly easier to work with when things get crazy. I think this is mostly because you tend to be composing recognisable patterns, which in turn means the only custom code you’re writing is the “interesting” parts, like deciding exactly which data to select or exactly how to combine each pair of elements. With lots of loops and conditionals and early exits, I also have to work out whether the code is really doing what it looks like or whether there are edge cases that work differently, and even the “what it looks like” part can wind up scattered across several places in the code that are some distance apart. Some of the projects I work on do a lot of quite intricate manipulations of complicated data structures. Earlier incarnations were written in Python, but even there I found myself using a functional style for most of these situations as the code base grew in size and complexity. More recently, for various reasons including that one, I’ve been writing this sort of code in Haskell, a language designed for that programming style and therefore cleaner in both syntax and semantics. IMHO, it would be hard to overstate how much easier the newer code is both to write originally and to read, fix and extend later. Possibly the most striking thing is how much shorter the code is: the functional style combined with a language and libraries designed to support it really is remarkably expressive for data crunching work compared to the “longhand” form of writing out all of the loops and conditionals manually.
- heisenbit 10y agoI often wish it was only the reading. That may be a matter of getting used to it and there may be pluses to the functional style. But when it comes to using a debugger loops seem to be so much more approachable.
- chocolatebunny 10y agoWith the map, you know, as soon as you see the map that you're making a new list based on an old list. With the for loop, you have to read and understand every single line of the for loop to understand what it's doing.
- ebola1717 10y agoI find it easier to read with the exception of the anonymous variables. In scala, I would write val persons = names .map { name => Person(name = name) } .filter { person => person.isValid }
- virtualwhys 10y agoDunno, it's fairly obvious what you're working with, being explicit doesn't buy that much (for me at any rate). val persons = names.map(Person.apply). filter(_.isValid)
- hinkley 10y agoFor me it's neutral, except for one critical difference. I've been shepherding a bunch of front end Javascript tests, and recently had to go through fixing a systemic problem with how we were handling multiple promises. The broken code didn't look too different from your second example, but the same three people made the same mistake repeatedly, leading to tests that generated empty lists and thus didn't verify anything. Now, I'm not claiming this is a good pattern of testing. Indeed in all of the straightforward cases I removed the array entirely, and with great relish (especially since it also sped up the tests dramatically). And there are obviously some gaps in their theory of testing that they didn't notice the problem until I pointed it out. But it did illustrate to me again that there are (alarmingly) a lot of people struggling with basic data manipulation, and if your language supports anything like list comprehensions, I think you should probably get used to using them. It keeps those gaps out, and makes people decompose the problem instead of mashing together a block of conditional code that reads like a Choose Your Own Adventure.
- carapace 10y agopersons = [person for person in (Person(name) for name in names) if person.isValid()] This is Python.
- contras1970 10y agoi've been struggling with python incomprehensions since time immemorial. map/filter are a breeze.
- laylomo 10y agoIt's easy. The first bit: [person tells you that you're getting a list of persons. That's the most important part. The rest is telling you how they're getting in that list. It's almost like a select query in (pseudo) SQL. select person from people where person.isvalid = true vs [person for person in people if person.isvalid]
- naasking 10y agoI don't see why either is more difficult. For the functional version, you just need understand the meaning map and filter, and for the imperative version, you need to understand the meaning of for loops and append.
- wyager 10y agoNo, but this shouldn't be taken as an indictment of FP general. I prefer persons = filter isValid (map init names) to both. Swift just isn't really a functional language in any useful sense.
- mikeash 10y agoI find the first one to be far easier to read. You can basically just read it from top to bottom and know exactly what it does. "Take names, turn each one into a Person, keep the ones that are valid." The second one takes a lot more work. First, I have to read through the code and recognize that this is a loop that accumulates values into a new array. Then I have to pick it apart and see exactly where the accumulation takes place, and how that value is derived. It's not hard by any means, but the first one is far more straightforward.
- skizm 10y agoIs the first one two loops or one?
- actsasbuffoon 10y agoIt depends. Using lazy data structures, transducers, or a number of other common tricks would turn it into one loop. Haskell, Clojure, Ruby, Scala, Swift, and many other languages make it pretty easy to do.
- mikeash 10y agoTwo. These methods are not lazy by default. Of course, you have to know the language pretty well to know that, but I do. For anyone wondering, you can write names.lazy.map(...) to get lazy evaluation.
- chii 10y agoThe real answer is that it doesn't matter. Just like for most code, the output assembly or machine code doesn't matter.
- andreareina 10y agoNaïvely, two. The compiler can probably figure out that the semantics are the same and choose depending on what its model says will be faster.
- Nadya 10y agoPitching in with the sea of other voices: I prefer the first. Vastly. In fact, while I still have issue reasoning about some aspects of functional programming when writing the code - it is magnitudes easier for me to read, maintain, modify, or extend the code.
- erikpukinskis 10y agoI think the readability is a bit of a wash. But the second one is more debuggable than the first, which I think is even more important than readability. In the first case, you need to rewrite the control structure to even be able to inspect anything: let allPersons = names.map(Person.init) {log allPersons[0].name} {breakpoint} let persons = allPersons.filter { $0.isValid } There are lots of data structures in this style of programming that don't have any names. Who knows what kind of data structures map and filter create in order to do their work. Are we allocating 2 arrays? 1? None? In the procedural style, everything that exists in memory has a name. It's also totally unclear what the order of operations is. Are Person.init and $0.isValid alternated? Is the `map` run in full before the `filter` starts? No way to know. People talk shit about procedural programming, as if it's antiquated. But the core promise of functional programming—that you can stop thinking about the underlying procedures—never seems to fully pan out. So when you inevitably need to start digging under the hood to figure out what your declarative code actually means in practice, you end up having to think procedurally anyway. Now you're thinking procedurally, but your code is declarative, and the runtime is trying as hard as it can to prevent you from knowing what's exactly happening moment to moment. I think there are specific cases where a declarative interface is the right abstraction. CSS is a good example. But these are narrowly defined domains with relatively clear semantics that get frequent use, so the time it takes to learn the semantics will pay off. The idea of littering your entire codebase thickly with declarative APIs, each of which has unique control structures that must be understood in order to read code, is not a good approach in my opinion. This is what Rails is, and it creates a situation where you are captive to your tools: you can do a lot very easily, but you cannot stray far beyond the declarations that your library author overlords have chosen for you, or you quickly find yourself in a space where in order to not shoot yourself in the foot you need to have a huge body of internals in your head.
- catnaroek 10y ago> But the second one is more debuggable than the first, which I think is even more important than readability. The first is less likely to require debugging in the first place. > There are lots of data structures in this style of programming that don't have any names. So you can only reason about things that have names? Now we know where idiomatic Java comes from. > Who knows what kind of data structures map and filter create in order to do their work. In most reasonable implementations, the only data structure being created is the final result (a functorial value in map's case, a sequence in filter's case). For example, in SML: fun map _ nil = nil | map f (x :: xs) = f x :: map f xs fun filter _ nil = nil | filter p (x :: xs) = if p x then x :: filter p xs else filter p xs > But the core promise of functional programming—that you can stop thinking about the underlying procedures—never seems to fully pan out. Functional programming doesn't promise freedom from procedures. It promises (and delivers) freedom from physical object identities when you only care about logical values. --- @banachtarski: Code that's likely to require debugging (say, because it implements tricky algorithms) should be isolated from the rest anyway, regardless of whether your program is written in a functional style or not. Say, in Haskell: Bad: filter (\x -> tricky_logic_1) $ map (\x -> tricky_logic_2) $ xs Good: -- Now trickyFunction1 and trickyFunction2 can be -- tested in isolation. Or whatever. trickyFunction1 x = ... trickyFunction2 x = ... filter trickyFunction1 (map trickyFunction2 xs)
- partiallogic 10y agoI find it easier to read but I'd struggle to come up with it myself from zero
- louthy 10y agoI find the first example much more intuitive. It declares intent, rather than the mechanics of delivery. And that's where we need to be heading as programmers.
- RotsiserMho 10y agoThis. So much. Declaring intent (what) is much more useful than declaring the mechanics (how). I don't typically care about the "how", just the "what". And the more readable that is, the better.
- yvsong 10y agoAnd the source code comments are the why.
- bertiewhykovich 10y agoIn Python, the only good language, this could be expressed as: [person for person in (Person(name) for name in names) if person.isValid()] or: filter(lambda p: p.isValid, map(Person, names)) or: persons = [] for name in names: person = Person(name) if person.isValid(): persons.append(person)
- harpocrates 10y agoSo much for "There's Only One Way To Do It". :)
- pjmlp 10y agoThat is long gone, specially regarding string formatting.
- bertiewhykovich 10y agoIn fairness, the list comprehension would probably be the most "Pythonic" way to do this. The nested iterator might be discouraged (under "explicit is better than implicit," perhaps), so a more Pythonic snippet might look like: persons = [Person(name) for name in names] valid_persons = [person for person in persons if person.isValid()] Take this all with a grain of salt, though -- I haven't spent that much time internalizing classical Pythonicness.
- allengeorge 10y agoI find it much easier. It might be a question of getting used to it.
- lacampbell 10y agoI found the first one easier to read up until I got to the lambda syntax. Map and Filter are intuitive to me, but I wasn't at all clear what { $0.isValid } was doing.
- weaksauce 10y agoComing from ruby and using a lot of maps and functional constructs I personally think the first one is way easier to read.
- lucio 10y agoMe too. The first one is harder to read, and also the first one is harder to extend if the business rules change.