The other day, I posted the following snippet to demonstrate string regex:
"Hello".rangeOfString("l{2}", options: [.RegularExpressionSearch], range: "Hello".startIndex..<"Hello".endIndex, locale: nil)
This code is clunky for various reasons, not the least of which is that I had to type “Hello” three times. There already exists a Swift mechanism for cleaning this up using closures and anonymous arguments. Here’s what that looks like:
let range = { $0.rangeOfString("l{2}", options: [.RegularExpressionSearch], range: $0.startIndex..<$0.endIndex, locale: nil) }("Hello")
I’m not a fan of the trailing arguments. The intent is hard to read.
I should mention that the let range assignment is required. Without it, Swift complains of trailing closures:
// Trailing closure error for whatever precedes this line { $0.rangeOfString("l{2}", options: [.RegularExpressionSearch], range: $0.startIndex..<$0.endIndex, locale: nil) }("Hello")
Anonymous arguments are also required. You cannot use label binding. The following code errors because string is used before it’s defined.
// Error error: use of unresolved identifier 'string' let range = { string.rangeOfString("l{2}", options: [.RegularExpressionSearch], range: string.startIndex..<string.endIndex, locale: nil) }(string: "Hello")
Of course, you can work around anonymous arguments by declaring string within the closure. You can see how this is already losing the “simplicity win” I was going for.
// Thanks BigO and Ketzusaka let range = { string in string.rangeOfString("l{2}", options: [.RegularExpressionSearch], range: string.startIndex..<string.endIndex, locale: nil) }("Hello")
It would, of course, be nice to have some sort of do work-around like the following to avoid the naked scope error.
do { $0.rangeOfString("l{2}", options: [.RegularExpressionSearch], range: $0.startIndex..<$0.endIndex, locale: nil) }("Hello")
Even here, I’m not a huge fan of the postfix argument. Seems to me that something like do(arguments){} or with(arguments){} would be nicer than do{block}(arguments).
While I’m rambling, let me add one more complaint. I’ve been missing and wanting closure-specific state because
var n = 0; var naturalNumbers = anyGenerator{return n++}
seems less natural (no pun intended) than
var naturalNumbers = anyGenerator{static n = 0; return n++}
What do you think? Drop a comment, a tweet, email.
10 Comments
I think the other way wasn’t working because the return type is optional:
string -> Range? in
Thank you again!
One way to be less clunky: Don’t call the full method if you are just going to be supplying default arguments anyway.
let r = “Happy”.rangeOfString(“p{2}”, options: [.RegularExpressionSearch])
I’d love anyGenerator{static n = 0; return n++} too! I just had some code that needed closure state.
I think your with(args) {} could be done as:
For this specific example, it’s definitely worth noting that the string search methods in the Foundation overlay have default arguments for range and locale that would make this entire thing moot. (Not that this isn’t worth talking about for other cases.)
i’ve been using a couple of operators for this:
the first does what you describe here, you could write
“Hello” ->- {
$0.rangeOfString(“l{2}”,
options: [.RegularExpressionSearch],
range: $0.startIndex..<$0.endIndex,
locale: nil)
}
the second is basically a little shortcut for when you'd be doing return $0 with the other one – e.g. if you're taking some struct or object and changing some fields and then returning the same object (or modified value of same type). it captures changes to value types by making the implicit $0 argument an inout in that case. i've found this operator handy for initializing complex Cocoa objects.
they're both left-associative at the same precedence, so they should work as expected if you use them in a shell-like pipe chaining style.
The anonymous closure trick is neat, but it has a problem where Swift does not (currently) infer anonymous closures like that to be @noescape. I’d recommend using a with() function, as you suggested. You don’t even need multiple functions like Keith suggested, a single generic function can accept a tuple.
And in fact, it turns out the Swift standard library already has a with function. Except it’s actually called withExtendedLifetime, because the purpose of the function is just to ensure the value is not destroyed before the closure finishes executing. But it turns out to have precisely the method signature we already want. So if you don’t mind the typing, you can use it like
Although there is one improvement we could make, which is to make it rethrows. As I understand it, the Swift standard library currently doesn’t make use of rethrows because of compiler bugs, but this is what it would look like:
And actually, this right here demonstrates a compiler bug around rethrows, which is that it infers the anonymous closure to be throws even though it can’t actually throw, and so emits an error saying you need try.
A separate issue here is that your original code snippet,
“Hello”.rangeOfString(“l{2}”,
options: [.RegularExpressionSearch],
range: “Hello”.startIndex..<"Hello".endIndex,
locale: nil)
is not just annoyingly repetitive, it’s also dangerous. It’s illegal to use a collection index with a collection it didn’t come from, but your use of
"Hello".startIndex
and"Hello".endIndex
is doing precisely that. Yeah, in practice, it probably won’t break, because the compiler will almost certainly construct 3 identical strings from the 3"Hello"
. But there’s no guarantee that it will do that, and if you ever end up accessing the index on a string that does not have an identical in-memory representation, it can break in weird ways.