Holy war: Unswifty Procedural vs Swifty Functional

Background: I’m working on a proof of concept chapter.

Goal: Sample code that’s teaching about proportional spacing in character-by-character layout.

The contenders: I initially went with a basic loop (choice A) rather than reduce() (choices B & C).  For an audience that’s learning about graphics rather than Swift, does this read significantly more easily? Are there compelling reasons to prefer the functional approaches? And if so, how much would you break it down? Which would you go with and why? Let the battle commence. Which code reigns supreme?

// Choice A
// Calculate the full extent
var fullSize = 0 as CGFloat
for character in letters {
    let letter = NSAttributedString(string: String(character), attributes: attributes)
    fullSize += letter.size().width
}

// Choice B
// Calculate the full extent
let fullSize = letters
    .map({ NSAttributedString(string: String($0), attributes: attributes) })
    .reduce(0 as CGFloat, { return $0 + $1.size().width })

// Choice C
// Calculate the full extent
let fullSize = letters
    .map({ String($0) })
    .map({ NSAttributedString(string: $0, attributes: attributes) })
    .map({ $0.size().width })
    .reduce(0 as CGFloat, +)

[socialpoll id=”2395012″]

Which code reigns supreme? Results discussed here.

22 Comments

  • As a complete novice Sample A is the easiest to read. I like the use of closures, but for pure readability A gets my vote.

  • For teaching people, Choice A feels like it will be easier for a generic student to imagine the code looping over each letter and appending the width.

    If this was for a PR from a fellow professional into our actual codebase, then Choice B would be the clear winner, since I know my co-workers are expecting to see .map() and .reduce(), and this syntax lightens the mental burden of understanding what `fullSize` is.

    Specifically, it takes longer to mentally parse the for loop, because it could do so many side-effect or branched things, etc… so I have to read it carefully to make sure that it really is just transforming each letter and adding the results.

    But a co-worker seeing a .map() then .reduce() will quickly focus on the transform and the combine closures, blissfully leaving the mechanics of assembling the final answer to their (quite correct) intuition.

  • All are good in my book. I wouldn’t feel the need to rework any of these three if I came across them. (I would prefer that A be a separate function, so that fullSize isn’t a var beyond the scope where it’s actually being mutated.)

    If we’re nit-picking, I’m split between B and C. In a chain of collections transforms like this, I like each step to be a useful thought that conveys to a human some but not too much new information about what’s going on. In my book, map({ String($0) }) is not useful as a separate thought, but reduce(0 as CGFloat, +) is. “Make attributed strings, get their widths, and add them” is a nice way to break it down.

  • Sorry, I voted for A thrice. On an iPhone, it’s not at all clear if a vote has been cast. I expected to see the results of the poll so far.

    But I’m not really sorry I voted for A thrice. Options B and C remind me of the convolutions required to use the C++ STL. map() and reduce() are powerful, but they make the core hard to read IMO.

    Sorry not sorry. 🙂

  • Just curious: why do you think the procedural example is “unswifty?” Just because Swift supports FP doesn’t mean that everything else is a throwback to C.

    While I think FP is a useful tool for programmers to learn, it is one tool of many. In my opinion, go with whatever expresses the solution most clearly; to me, that’s in keeping with Swift’s design goals. When writing a book, it’s also a good idea to consider your audience; they may not appreciate having to pull another book off the shelf to understand your example.

    Best of both worlds: include the FP example in a sidebar to whet readers’ appetites for further study.

  • I feel like Choice B is the most concise and clearly illustrates the power of Swift functional programming as well as illustrating the use of map and reduce. However, since this is not your main intent (an audience of users learning graphics) then I’d say to stick with A. And, frankly, should you decide to go with Choice B, how bad would it be to combine the learning of swift functional programming along with your initial intent?

  • First, I’d probably rewrite the iterative version this way:


    var fullWidth = 0 as CGFloat
    for character in letters {
    let characterWidth = String(character).size(attributes: attributes).width
    fullWidth += characterWidth
    }

    There’s no need to allocate an NSAttributedString for every character. String is enough.

    But I’d lift that detail into an extension on Character anyway:


    extension Character {
    func size(withAttributes attributes: [String: Any]) -> CGSize {
    return String(self).size(attributes: attributes)
    }
    }

    And now the functional approach I think is very clear:


    let fullWidth = letters
    .map{ $0.size(withAttributes: attributes).width }
    .reduce(0, +)

    And if you add another extension (one that should be in Swift anyway), I think it is even clearer:


    extension Character {
    func size(withAttributes attributes: [String: Any]) -> CGSize {
    return String(self).size(attributes: attributes)
    }
    }

    extension Array where Element: FloatingPoint {
    func sum() -> Element {
    return reduce(0, +)
    }
    }

    let fullWidth = letters
    .map{ $0.size(withAttributes: attributes).width }
    .sum()

    • That’s just the way I would do it, too. Use OO to nicely get the size of a character, then use FP to sum it.

  • (Why do code-blocks get extra newlines? Hmmm, sorry about that)

  • If Swift had sum() as well as reduce() option B would look more reasonable.

  • Rob — Just skimming quickly here, but I think your Character extension changes the logic from computing the width of the entire string to summing the widths of individual chars, right? If so, it won’t account for ligatures & kerning anymore, and thus will give wrong answer.

    • Ligatures and kerning don’t apply since the desired result looks like this: http://i.imgur.com/Frk7kMo.jpg

      • Yeah, that’s what I was matching.

        I do notice that the spacing between V, W, X is much larger than the spacing between A, B, C, which I’m not sure is the goal. The way I’ve done this in the past is to let CoreText layout the whole thing, and then project the offsets onto the curve (CurvyText does this for the general Bezier case, but you can build a simpler version for circles). But your PoC may be attacking a different version of the problem, so definitely depends on what you’re trying to do.

      • Ah, well, if that’s what it’s supposed to look like, then character by character is indeed the way!

  • I prefer Choice B, but I think Choice A is easier to understand for the uninitiated.

  • Hi, I am a bit concerned that the functional approach is split between B and C, so A might get an unfair advantage of votes from all “old school” programmers whereas the more modern thinkers are split up. I voted for C based on the KISS-principle, but would rather use B for convenience.

    Ralf

  • I surprised myself by voting for C. As a long time Objective C developer, option A would be my usual choice, but option C breaks down the task into easily explainable steps:

    Convert each letter in turn to a string
    Convert that sting to an attributed string
    Get the width of that string
    Add all the widths to 0

    If the audience is familiar with Obj C, than maybe A would be best, but if they’re non-developers, or don’t know either Obj C or Swift, then C looks to be the way forward.

  • For my money, A wins by a mile. It’s just far more readable, maintainable, with far less mental work required.

    A may not be trendy, fashionable or sexy, but it’s code I’d want to read and maintain 5 years from now.

  • I’d show all three and show how they differ and why, so people who understand A can learn to appreciate B and C.

  • The map in B should include the width calculation, instead of re-mapping in the reduce.

  • A

    Of your snippets, only A keeps all the bit to do with the calculation you are teaching in one place. Both the others spread the calculation over two or more closures.

    It’s not immediately obvious to me why you haven’t got


    // Choice D
    let fullSize = letters.reduce(0 as CGFloat, {
    return NSAttributedString(string: String($1), attributes: attributes).size().width + $0
    })

    • Sorry, mistake:

      fullSize = letters.characters.reduce(0 as CGFloat, {
      return NSAttributedString(string: String($1), attributes: [:]).size().width + $0
      })

      Has been tested in Playground