Live data from Hacker News

Swift and the Legacy of Functional Programming

realm.io

121–130 of 188 posts

Re: Swift and the Legacy of Functional Programming

#121

Earlier quoted context omitted.

> 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 filt…

> The first is less likely to require debugging in the first place. I'm all for functional languages but this scares me a bit. What do you do when you need to debug something and everything ends up being harder to debug but "less likely to need debugging." I've actually run into this situation a number of times and faced with a sea of linked compound expressions, debugging can be a daunting proposition.

It's still a net win in my experience. The minor inconvenience of inserting a few temporary variables to hold intermediate values is much less than the burden of all the additional reading and debugging needed when you spell out every step for everything you want to do.

It's kind of strange to me. We generally acknowledge that not repeating yourself and dividing responsibilities sensibly leads to better code that has fewer bugs and is easier to reason about. And yet when we consider doing the same thing with iteration, we say, "Whoa, hang on. Why can't we just write out the whole thing every time instead of factoring the common bits into a function?"

Re: Swift and the Legacy of Functional Programming

#122
post #104

Earlier quoted context omitted.

hmm, 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 validPerson : invalid : invalid : invalid : ... : thunk then finally at n = 1e9 validPerson : invalid : ... : validPerson because the intermediate calculations still need to happen. getting that…

I understood from your comment that the imperative version "avoids building the name size list of persons" that you thought the declaritive version would construct an intermediate List[Person] the same size as the source list of names. Most modern languages (e.g. C#, F#, Clojure, Rust) implement map and filter using lazy sequences rather than eagerly constructing intermediate collections (admittedly I don't have much…

Oh! yes. Yes we're on the same page as far as laziness goes. The lazy version will only construct as much of the intermediate list as you need to get the next answer, and potentially the garbage collector can reclaim the parts of the intermediate list that have already gone past.

But, there's already a good clean functional composable answer to this kind of thing,

fold takes an operation (like map does) a list (like map does) and an accumulator for building up results, and it returns the resulting accumulator.

   foldl(op, names, acc){
       if(names.empty)
           return acc
       else
           foldl(op, names.tail, op(names.head, acc)
   }
so with tail recursion, you get no stack growth. (i mean head like the first element of names, and tail as everything else)

the op would look something like

   op(name, acc){
      let p = Person.init(name)
      if(p.isValid)
          return acc += [p]
      else
          return acc
      }
So fold trundles down the list of names, the op checks each name to see if it's good, and only adds the good names to the final list. whenever fold notices it's out of more names to try, it just returns whatever the current accumulator might be.

Map forces you to build the intermediate representation, which the imperative version avoids.

Re: Swift and the Legacy of Functional Programming

#123
post #94

Earlier quoted context omitted.

It'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.

In 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 pr…

> In 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.

No "object" is made if the name can't create a "valid person object", it'll return a stack-allocated null/nothing value. That pattern is also much better for concurrency issues.

> much much better to filter the names, then make the objects.

You're just duplicating the validation logic (or worse, the "objects creation" assumes it's given valid names and does no checks)

Re: Swift and the Legacy of Functional Programming

#124
post #107

Earlier quoted context omitted.

Excuse my ignorance but why is the first one parallelizable but not the second?

In the first one, you are simply telling it to map and filter every element. Since you can do that to each element in an independent way and the code is more abstract, the map and filter can be done in parallel without you knowing (I'm not sure if would execute in parallel in Swift, I know that in java you can change the way it does it by using parallel streams). But in the second example, you are telling it to itera…

Put another way, the first example is about data flow. The required value is specified declaratively, in the form of an expression.

The second example is about control flow. The required behaviour is specified imperatively, in the form of a statements to be executed in a defined order, and the combined effect of those statements is to update the list until it has the required value.

In the second case, the programmer specifies more details explicitly. In this case, those extra details probably aren’t helping, but the optimiser still has to prove that transforming the control flow (for example, into a parallelised form) really will give the same observable behaviour. In the first case, the optimiser doesn’t have to prove the things the programmer never specified anyway, so it has more latitude in how it implements the underlying computation.

Re: Swift and the Legacy of Functional Programming

#125

Earlier quoted context omitted.

I 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…

> For example, the first one is completely open to being performed in parallel So what do you actually have to do to make this actually run in parallel? Or do you truly get it automatically?

I don't think that will happen in Swift, but conceptually there's nothing that stops the compiler from doing it since you're not specifying the order that anything happens. Some Haskell compilers can automate parallelization of the equivalent code. But pragmatically speaking, you can convert code in this style to parallel code without changing the algorithm; for example various "map-reduce" servers are built around the idea of mapping and reducing, which are fundamental concepts of functional programming.

In contrast, in the imperative form, you are specifying how items are appended to a list, which means that any attempt to do it in parallel could change the order of the operations and therefore the order of the result. Sure, a sufficiently smart compiler could in theory figure out what "should" happen and see how to optimize it, but in practice today's smartest compilers can barely handle the functional case.

Re: Swift and the Legacy of Functional Programming

#126
post #104

Earlier quoted context omitted.

hmm, 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 validPerson : invalid : invalid : invalid : ... : thunk then finally at n = 1e9 validPerson : invalid : ... : validPerson because the intermediate calculations still need to happen. getting that…

Even Java manages to get your example to work efficiently with map/filter. It's not rocket science. Just because some library designers screwed it up doesn't invalidate the whole concept.

but java provides collect. which is, like, the right way to do it.

I'm just sort of mystified that people are defending map.filter over filter.map (which isn't crazy, as name.isValidPerson isn't provided.) but, you know, fold is a thing.

Re: Swift and the Legacy of Functional Programming

#127

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.

I 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 st…

You're 100% right about debugability. It's the main reason I don't push to use haskell every day. That said, I think we really need to get clever and think of good tools to debug in spite of these difficulties. Most complex bugs are from interacting systems that can't be found by step debugging. For example, the following code is considered pythonic, and for good reason:

    fs = [f(x) for x in xs]
The other commenter's points about being cleaner and less prone to debugging are totally legit. If we can make e.g. list/set comprehensions debuggable, we can probably make other FP idioms debuggable and get the best of both worlds.

Kinda reminds me of microservices, actually. Tough to debug, but good in other ways.

Re: Swift and the Legacy of Functional Programming

#128

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.

I 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…

Hence why "structure and interpretation of computer programs" is a very important read.

Re: Swift and the Legacy of Functional Programming

#129
post #46

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.

I 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).

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.

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.

Re: Swift and the Legacy of Functional Programming

#130

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.

I 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.
Post reply on HN