Reduce


« Combine Kickstart Available

Next »

I want to show you something I tried with reduce() to pass state to the elements being processed.

I'd appreciate feedback - I understand it rubs some the wrong way. I'm not sure how I feel about it and am willing to be swayed either way.

Anyway - here's the context...

I format the code in my books and slides different than many others.

I don't use syntax coloring - instead I color the code that I've added or changed in an accent color.

In slides where I'm presenting a build, I will often hide code that I'll be presenting so that the rest of the code stays in place and you can focus on what's new without having to re-orient yourself.

So I use different colors for regular, emphasized, and hidden code.

I also use an emphasized with strikethrough to call out deleted code and color comments in a grey.

Usually you won't see more than one of these at a time - but here's a sample with all of the styles. The hidden code would ordinarily be rendered invisibly but then you wouldn't see it so I've used a color with low opacity.

Differently colored code

In coding this, I have a CodeListing struct that contains an array of LineOfCode. Each line of code contains an array of LineCodeFragments.

The challenge for me is that except for comments, the styles can span multiple lines.

I ended up with two solutions that I debated.

I went with the second option.

The problem is that I need a way to pass the previous line's ending style into the next line.

Let's start with the shape of the Result Builder and its buildBlock method.

@resultBuilder struct CodeListingBuilder { public static func buildBlock(_ string: String) -> [LineOfCode] { // ... } }

You can see that buildBlock takes a String and turns into an array of LineOfCode elements.

I split string into separate lines and then want to use reduce() like this.

public static func buildBlock(_ string: String) -> [LineOfCode] { let linesOfCode = string.split(separator: "\n") .reduce([LineOfCode]()) { (lines, substring) in let line = LineOfCode.line(from: String(substring)) return lines + [line] } return linesOfCode }

This would work beautifully if I'd chosen door number one above and insisted that styles not span lines.

To span lines I need to somehow hold on to the style from one line and use it in the next line.

Here's the way I tend to solve this.

I introduce a var named style and update it from inside reduce()'s closure.

public static func buildBlock(_ string: String) -> [LineOfCode] { var style = CodeFontStyle.regular let linesOfCode = string.split(separator: "\n") .reduce([LineOfCode]()) { (lines, substring) in let (line, endingStyle) = LineOfCode.line(from: String(substring), startingWith: style) style = endingStyle return lines + [line] } return linesOfCode }

This feels natural to me but after playing with the State and Reader monads I wondered about passing the state along. So let's eliminate style.

public static func buildBlock(_ string: String) -> [LineOfCode] { var style = CodeFontStyle.regular let linesOfCode = string.split(separator: "\n") .reduce([LineOfCode]()) { (lines, substring) in let (line, endingStyle) = LineOfCode.line(from: String(substring), startingWith: ????) style = endingStyle return lines + [line] } return linesOfCode }

To make the compiler happy, I introduced a local type that included the lines of code and style.

private typealias Result = (lines: [LineOfCode], style: CodeFontStyle)

Now, we can revisit reduce() and thread the style through our iteration.

public static func buildBlock(_ string: String) -> [LineOfCode] { let (linesOfCode, _) = string.split(separator: "\n") .reduce(([LineOfCode](), CodeFontStyle.regular)) { (result: Result, substring) in let (line, endingStyle) = LineOfCode.line(from: String(substring), startingWith: result.style) return (result.lines + [line], endingStyle) } return linesOfCode }

On the one hand, I really like how this feels. I'm no longer reaching outside the reduce() to pass on the current style.

On the other hand, it's harder to look at this code and tell what's going on.

On the - yet - other hand, maybe my eyes will get used to this.

If there is a more idiomatic or correct way to accomplish this, I'd appreciate you letting me know.