Additional Joined Functions

Additional joined Functions

I frequently find myself wanting to join collections whose elements do not conform to StringProtocol. I would like to suggest the addition of some new joined functions as extensions on Collection.

public extension Collection where Element: CustomStringConvertible {

    /// Returns a new string by concatenating the elements of the sequence, adding the given separator between each element.
    ///
    /// The following example shows how an array of integers can be joined to a single, dash-separated string:
    ///
    ///     let phoneNumberSections = [492, 573, 5843]
    ///     let phoneNumber = phoneNumberSections.joined(separator: ", ")
    ///     print(phoneNumber)
    ///     // Prints "492-573-5843"
    ///
    /// - Parameters:
    ///     - separator: A string to insert between each of the elements in this collection. The default separator is an empty string.
    ///
    /// - Returns: A single, concatenated string.
    ///
    @inlinable func joined(separator: String = "") -> String {
        guard let first = first else { return "" }
        guard count > 1 else { return "\(first)" }
        return dropFirst().reduce(into: "\(first)") { $0.append(separator + "\($1)") }
    }
}

public extension Collection {
         
    /// Returns a new string by mapping the given closure over the collection's elements and concatenating `String` results, adding the given separator between each string.
    ///
    /// The following example shows how an array of integers can be transformed into each integer's respective square and joined to a single string separated by addition operators:
    ///
    ///     let numbers = [1, 2, 3, 4, 5, 6, 7]
    ///     let sumOfSquares = numbers.joined(separator: " + ") { "\($0 * $0)" }
    ///     print(sumOfSquares)
    ///     // Prints "1 + 4 + 9 + 16 + 25 + 36 + 49"
    ///
    /// - Parameters:
    ///     - separator: A string to insert between each of the elements of this collection transformed into strings. The default separator is an empty string.
    ///     - transform: A mapping closure that produces a string. `transform` accepts an element of this collection as its parameter and returns a transformed `String` value.
    ///
    /// - Returns: A single, concatenated string.
    ///
    @inlinable func joined(separator: String = "", _ transform: (Element) throws -> String) rethrows -> String {
        guard let first = first else { return "" }
        guard count > 1 else { return try transform(first) }
        return try dropFirst().reduce(into: try transform(first)) { $0.append(separator + (try transform($1))) }
    }
}

Usage

let phoneNumberSections = [492, 573, 5843]
let phoneNumber = phoneNumberSections.joined(separator: ", ")
print(phoneNumber)
// Prints "492-573-5843"

let numbers = [1, 2, 3, 4, 5, 6, 7]
let sumOfSquares = numbers.joined(separator: " + ") { "\($0 * $0)" }
print(sumOfSquares)
// Prints "1 + 4 + 9 + 16 + 25 + 36 + 49"

struct Person {
    var name: String
    var age: Int
}

let people = [
    Person(name: "Vivien", age: 23),
    Person(name: "Joseph", age: 49),
    Person(name: "Marlon", age: 12)
]

let peopleStr = people.joined(separator: " and ") { "\($0.name) is \($0.age)" }
print(peopleStr)
// Prints "Vivien is 23 and Joseph is 49 and Marlon is 12"
//

What are everyone's thoughts on adding these joining methods to Swift?

I don't like it. I think that you usually want to be explicit about which way you transform your thing into a string. Using your example of phone number sections - it won't work for sections like 001, so you need to provide a custom transform anyway.

And the second method that you can provide a custom tranform for - I don't think it's better than just writing foo.map(transform).joined()

3 Likes

There's many different ways you can combine map, filter, reduce, dropFirst, dropLast, prefixWhile, etc. It doesn't generally make sense to make functions that are combinations of both, because you're going to have an n^2 explosion of pairings that need to be supported.

The code you show is already achievable with .lazy.map:

import Foundation

let phoneNumberSections = [492, 573, 5843]
let phoneNumber = phoneNumberSections.lazy
	.map(String.init)
	.joined(separator: ", ")
	
print(phoneNumber) // Prints "492-573-5843"

let numbers = [1, 2, 3, 4, 5, 6, 7]
let sumOfSquares = numbers.lazy
	.map { $0 * $0 }
	.map(String.init)
	.joined(separator: " + ")
print(sumOfSquares) // Prints "1 + 4 + 9 + 16 + 25 + 36 + 49"

struct Person {
	var name: String
	var age: Int
}

let people = [
	Person(name: "Vivien", age: 23),
	Person(name: "Joseph", age: 49),
	Person(name: "Marlon", age: 12)
]

let peopleStr = people.lazy
	.map { "\($0.name) is \($0.age)" }
	.joined(separator: " and ")
	
print(peopleStr) // Prints "Vivien is 23 and Joseph is 49 and Marlon is 12"
6 Likes