Support for String multiplication by number to repeat string?

Hi!

TL:DR: 'string' * number == String(repeating: 'string', count: number)

Having worked a lot with swift in the terminal and making command line applications, I find myself frequently using String(repeating: count:). This is because, to make things like lines and use the utf8 box drawing characters, the spacing needs to be just right. Because of this, we need to constantly use String(repeating: count:) which gets tedious and annoying as well as severely increases the length of lines, especially ones that require this to be used multiple times. To deal with this, I simply created a operator overload for * to be a shortcut for the String(repeating count) initialization:

func *(left:String, right:Int) -> String {
	return String(repeating:left, count:right)
}

I propose that we add the above function to Swift. The drawbacks of having this function are very little; it is simply a one line function. If the user would like to use it for something else, they can simply override it, but I see this operator being used like this extensively in other general purpose languages like Python (which, I know, is a bit higher level than Swift). Having this function in the language would make working with the terminal a lot easier, and people who don't need it don't even need to worry about it, because it does not affect existing behaviors.

I will be happy to field any questions or explain more if you ask, I hope you consider this, thank you all for an already great language!

1 Like

The operator is certainly useful in that way for some situations, but I suspect its meaning is too ambiguous for it to gain much traction towards general inclusion in the standard library. I can think of at least three things * means to different users between a string literal and an integer literal:

  • For you, multiplication is this:

    let string = "blah" * 2
    // Expected: ā€œblahblahā€
    
  • Developers using infiniteā€sized integers currently cannot represent everything with integer literals and have to fall back to parsed string literals for very large numbers. In their minds, multiplication is this:

    let bigInteger = "1 000 000 000 000 000 000 000 000 000 000 000" * 2
    // Expected: 2 000 000 000 000 000 000 000 000 000 000 000
    
  • Developers dealing heavily with bytes often make use of ASCII aliases for their respective integer values. For them, multiplication is this:

    let byte = "A" * 2
    // Expected: 130 (because ā€œAā€ corresponds to 65 in ASCII)
    

Since the three different semantics cannot coexist very well, Iā€™m not sure any of them really belong in the standard library. As long as each lives in its own custom module, users can simply import only the one they want in any given case. But the standard library is an unavoidable implicit import, so whichever it were to adopt would interfere with anyone trying to use the others.

3 Likes

Thank you for the consideration for my suggestion, but based on what you have said and the fact that this operator will impact readability (one of the biggest reasons I like Swift) my suggestion should not be implemented.

Franky, if a programming language with strong typing did either of the last two things, I'd dump it. I would exepect that one would have a bignum class, such that

"1 000 000 000 000" * 2 -> "1 000 000 000 0001 000 000 000 000", but
BigNum(("1 000 000 000 000") * 2 -> BigNum("2 000 000 000 000")

Similarly, and specifically because Swift eschews characters as secretly being integers, that "A" * 2 would strictly get "AA".

In other words, I feel like Swifts reputation is that these little tricksy things that languages in the past have loved are removed in service of having incredibly deterministic results. Sometimes I miss the char secretly is just UInt8, but I understand and support the idea that strong typing means more predictability, and so those tricks are out.

I like this proposal, I have written that extension into a few of my programs. Part of that is that the syntax String(repeating: Count:) is terrible. String(_ string: String, repeats: UInt) I might have used.

*Yes, I know 1 000 000 000 000 is representable in regular Ints. I got tired of typing.

5 Likes

Would this be good enough?

extension String.StringInterpolation {
    mutating func appendInterpolation(_ str: String, repeated n: Int) {
        appendLiteral(String(repeating: str, count: n))
    }
}

Used like so:

let string = "\("blah", repeated: 2)"
print(string) // blahblah

For clarity, I personally donā€™t like the last two much either.

  • I do actually use the large integers that way occasionally, but I would much rather have it be served by a proper integer literal, which is probably what the distant future holds.
  • When I work with bytes, I usually prefer 0x41 over A anyway.

Instead, I bring the issue up early because this sort of overloading with differing semantics has halted proposals in their tracks before. (See the SEā€0243 review for example.) You deserve to know how contentious such proposals can be and how much effort you would likely need to invest to actually carry it through to completion.

I should also clarify that it is more of a concern about the confusion caused for developers than it is what the compilerā€™s type checker is capable of when you make your types explicit.

If you do continue with the proposal, it would be worth considering whether it actually belongs on RangeReplaceableCollection instead of String, since that is the abstraction level the algorithm operates at.

1 Like

The usage pattern which I have looks like this:

func *(left:String, right:Int) -> String {
     return String(repeating:left, count:right)
}

func boxEnd(length: Int, tl: String, mid: String, tr: String) {
     print(tl, mid * length, tr, separator: "")
}

I wouldn't be too happy with:

func boxEnd(length: Int, tl: String, mid: String, tr: String) {
     print("\(tl)\(mid, repeated: length)\(tr)", separator: "")
}

I'm not sure I see a use case have it on RangeReplaceableCollection, but I may just be tired. :)

If interpolation is not sufficient, then your best path probably would be to write an extension on String func repeating(_ n: Int) -> String, assuming you are actually finding more than a couple times that you need it in your codebase.

The extension is the same as String(repeating:count:), but with less typing, which I think is what you are looking for. I doubt the extension would be justified to make its way into stdlib, but that's the point of extensions - offering conveniences suited to the specifics of your project.

I'm not sure whether I'm in favor of this proposal or not, but I do have to say that this example shouldn't be a reason for disqualifying it. Swift has a Character type for dealing with characters, and it's the position of the language to treat one-character strings as strings, not characters.

1 Like

So after reading all of your responses, I still feel like we need an easier way of repeating strings. The current method with String(repeating:count:) we can all agree is pretty clunky.
I think, because the other ways strings can be multiplied by ints all have better ways or methods for doing it, the * could still be a good candidate for repeating strings. After thinking about the readability of the operator, it doesn't seems like it would impact readability too much. In the ways that we are using the operator, it would be somewhat obvious what it does. A comparable operator in terms of readability might be the ternary operator. The ternary is not the most obvious of meaning operators, but it does have specific uses where it would make sense, similar to my operator. Also, the string * int operator is used in other languages, notably in python. The * in this way is extremely commonplace in different programming circles, like in penetrating, where having a set number of characters of padding is common to do (e.g buffer overflow exploit: "a" * number + payload).
Unless there is another good use for the string * int operator that is common, I do not see a reason why this should not be formally considered to be in the language. The previous example of "A" * 2 multiplying the ascii value could be taken care of using Character * int, which would make more sense than using a entire string, because strings could be variable width. Also, to address the big ints, a big int class would probably be a lot more robust if you were to be using those in any amount, and would pose less problems than a string.

Sorry for formatting I am on mobile.

2 Likes

Not that it necessarily changes anything, but in the interpolated version you don't need to pass in a separator, because you're only printing one thing.

1 Like

Fair enough.

IMO, proposal makes sense. It is intuitive to expect a * 3 == a + a + a, regardless of the type of a. And in case of String, addition already does concatenation, so using multiplication for repetition sounds like a logical extension of the existing behavior.

4 Likes

Note that the use of + for string concatenation is an often-cited example of a spot where early Swift was perhaps a bit overzealous with operator overloading, so extending this "analogy" from addition to multiplication isn't necessarily a win.

6 Likes

Sure, people might criticize the + for concatenation, but compared to the alternatives like ., it does make logical sense, especially to people who have been programming for a long time. Even with the controversy, the + for concatenation has since been strongly set into place, so I don't think that extending that analogy to the * operator would be a far cry from Swift's style. Maybe it would be a problem if the + for string concatenation didn't make sense, but it does, and other languages have implemented + and * in the same way proposed here.

2 Likes

-1. I donā€™t think this operator makes much sense. Adding two strings together with + makes sense, but both of the operands are the same type. I personally had no implicit understanding of what ā€blahā€ * 2 would evaluate to. I could theorize what the output would be, but I donā€™t have to do that with any of the other operators that I use.

That's not a solid justification IMO. One could argue that the following should also be obeyed:

  1. Addition is commutative. a + b == b + a. Doesn't work with strings.
  2. Multiplicative inverses work. (a * -1) + a == 0 (0 == "" being the identity of +). Doesn't work but can be ruled out by using UInt instead of Int, going against the typical API guidelines.
  3. Multiplication distributes over addition. n * (a + b) == n * a + n * b. Doesn't work for n = 2 and non-empty strings a and b.

If people find String(repeating:count:) excessive, we should just add a method to do it, not introduce a new operator like this.

10 Likes

This does sound like a better idea. Personally, I'd prefer something like "string".repeated(10) (or repeating/repeat whatever fits best) to repeat the string 10 times.

2 Likes