Ok, I accept your objection. I wrongly used a personal usage/convention as an argument to add something to the standard library: the fact that I would use the new method in a certain specific way (thus, I’d love to see that added), doesn’t constitute a valid argument in general.
Actually, thank you for making me understand this with clear words.
Sure, Swift has (regrettably) no concept of "pure" functions, and no means to enforce pureness in any function. Therefore, by definition, Swift allows programmers to add side effects to their map transforms.
However, to do so is a clear code smell and any experienced programmer would have to make sure to carefully and explicitly call out the fact that the transform is in fact unpure, by either adding comments, or extracting the code into a separate named function or closure, and using its name to alert future readers of the code that something is going on.
To not do so, is to write hard-to-read, hard-to-reason-about, and error-prone code that some future programmer (possibly yourself) will curse at.
Yes, it is opinion, and it is not enforced by the compiler. But it is also a convention. And even if you don't follow the convention yourself, any experienced programmer should know that it exists, that it is commonplace, and that there is a reasonable chance that other people will misread your code.
I explored the usefulness of forSome in a real work project, sifting through hundreds of if let statements. Here's the result:
The best case scenario is turning something like this:
if let error = error { log(error) }
into this:
error.forSome(log)
This scenario is extremely rare. All sorts of deviations tend to make forSome either useless (increasing neither readability nor brevity) or impossible to apply.
Passing a single function (that takes the unwrapped value as its single argument) directly to forSome was practically never possible. What reduced the chances even more is that it didn't work for mutating functions:
Edit: Can/should this be handled via inout and function overloading?
The most common cases in which I did not apply forSome:
Optional chaining requires extra braces, making this extra ugly:
(thisIsOptional?.butNotThis).forSome { ... }
(foo as? Bar).forSome { ... }
There's an else body
There are other conditions:
if let error = error, error.code == x { ... }
The if body has many lines, in particular with many references to the unwrapped value. Readability would decrease, in particular when the bound constant's name already adds clarity instead of repeating the optional's name.
There's a return (or other control statements) in the if body:
if let error = error { return log(error) }
Also, often it's just better to break early and use guard. I wish there was a short form for:
guard let foo = foo else { return bar() }
... maybe as
guard foo : bar()
... and if bar() does nothing and returns nothing:
guard foo
My personal provisional conclusion is that forSome offers me not even marginal benefits beyond its nice consistency with forEach.
Some of the conversation about map being pure or not only by convention misses a bit of the benefit, I think. Regardless of whether you use map to produce side effects or not you are allowed to produce a return result. Whatever the spelling, the proposed forValue would not have a return type and therefore would only be useful for side effecty code. I think there is value in being able to state that even if you cannot state the opposite (that map will not produce side effects).
When you use if let in swift, you are declaring a new variable or a strong reference if the variable represent a class.
Kotlin choose to not use binding, that is, they are not declaring a new variable that is guarantee to be not null when the test pass, so they have to make strong assumption about when the variable can change.
You can't for instance use the if x != null pattern when x is an instance variable, as the compiler assume that it can be accessed from anywhere at anytime and change under the hood.
There is no weak var in Kotlin, but it would suffer the same issue. You won't be able to use this pattern with a weak var as it does not create a strong ref on it.
This is one of the most obvious issue, but there is other issues like don't being able to use simple function like if (x.isNullOrEmpty()) without having to force unwrap x later the compiler was not able to know that this function guarantee non null value (this is improved in Kotlin 1.3 thanks to some annotations).
It can be, if we make it so. if number? { requires the same and is just as implicit… if let number? { is a little more explicit.
It’s true though that it’s potentially confusing both to Swift newcomers, that might not have seen similar behaviour with a nil check in other languages - nil checks being something they likely have seen, but if let … less likely. Doesn’t seem like a big hurdle, though.
The more concerning potential confusion is to Swift users generally if the implicit strong local capture isn’t applied in cases in which they might want it to be, because they appear similar to a ‘naive’ user.
The alternative is as you assume - an explicitly local variable would have to be created first, by the coder. That does then raise the obvious question of how often it would then actually be easier than just writing with the existing syntax:
if let localVariable = some expression {
…
}
…as opposed to:
let localVariable = some expression
…
… // Some other code in the meantime
…
if localVariable != nil {
…
}
I’d be a little surprised if the latter were more common, though it definitely happens frequently enough in my experience, e.g. a simple scenario:
var value: String? = cache[key]
if value == nil {
value = cache[key] = getValue(for: key) // Can fail; returns String?.
}
guard value != nil else {
logError(“Unable to fetch value for {key}.”)
return
}
// Finally do whatever you came here to do, with ‘value’.
It would make logical sense, then, that the if let … espression were just a convenient shorthand for:
let value = …
if value != nil {
…
}
…much like C eventually allowed for (int i = 0; … instead of requiring pre-declaration of i.
So not a dealbreaker in my mind, and something that could be easily made understandable via appropriate compiler hints, e.g.:
var someProperty: Int?
…
if someProperty != nil {
useAsUnwrapped(someProperty)
}
The compiler could continue to raise a type mismatch error as today, but with additional help, e.g.:
Error: ‘useAsUnwrapped’ expects an Int, not an Int?. The earlier check against nil is insufficient to allow implicit unwrapping as ‘someProperty’ might change after that check - use ‘var’ or ‘let’ to capture it locally first.
It would be nice in any case if the compiler could reason that someProperty won’t change in the timeframe in question, since that would enable compiler optimisations irrespective of this proposed feature, but it does seem that Swift is pretty far away from that today - as I understand it Swift assumes every shared reference can be modified asynchronously by default, rather than requiring that such a possibility be at least stated by the coder, if not determined unequivocally through logic & deduction at compile time (a la Rust).
Though, again, this is all just in the case where we choose not to have if number != nil { implicitly capture it locally.
Sidenote: I wonder if it’d work if every reference were captured into local scope - i.e. switching the whole thing on its head. Then you’d have to explicitly tell the compiler that you want to re-read any given thing, rather than it always assuming it, and such a language would seemingly be much less prone to concurrency issues… (presumably never going to happen at this point for Swift, but an interesting thought exercise… I can’t think of a language that explicitly behaves this way, though it’s sort of similar to how C/C++ require volatile…)
I checked my codebases too (in which I actually extended Optional with an equivalent forSome method, and use it in full force), and I don't agree with your points, here's the details.
In this case, using log in a point-free way is a personal preference, related to code style: I personally find that using full lambdas frequently improves clarity. I would write your examples like the following:
I think that Swift itself actually encourages this style, for example the point-free style doesn't work for functions with default arguments.
This problem is present with all the methods on Optional, it's not really about the forSome method. The choice (with which I agree) in Swift has been to not expose the "optionality" of the result of a chain at call site (the .), thus requiring ? only for the actual optional values in the chain: in contrast, Kotlin for example requires that a nullable chain presents a ? for every single intermediate call. This means that, in Swift, we cannot just call map on a optional chain if the last call doesn't return an Optional: but in this case, we simply use map and flatMap instead on the sugared version.
which is pretty clear to me, and I use this style a lot.
It's interesting how few times I used else throughout my codebases. I actually found that 90% of my if lets could be refactored to use forSome. In those rare case an else was needed, it was because I was assigning some constant in the branches, something that I could have expressed better with methods on Optional, instead of the if let construct. In one case the full if let else was needed, but that's about it, adding forSome wouldn't mean removing other constructs.
But we could solve this problem by either:
using a different signature, that takes 2 lambdas, like for(some: (Wrapped) -> Void, none: () -> Void);
making the whole thing "flowable" by returning the same Optional (with @discardableResult) and adding also a forNone method.
This is solved with a filter method on Optional, that's another thing that should be in the standard library that parallels operations on Sequence:
extension Optional {
func filter(_ isValid: (Wrapped) -> Bool) -> Optional {
guard let value = self, isValid(value) else { return nil }
return value
}
}
In your example this would be used like the following:
error.filter { $0.code == x }.forSome {}
which to me is more clear and readable than the if let counterpart.
I disagree, readability would be the same by simply choosing the right name for the bound constant, like in
optionalError.forSome { error in
/// code
}
With this I agree, in fact in my post I stated that I would still be using guard all over the place where it makes sense: this doesn't remove the utility of forSome though, and actually makes it more useful.
guard requires a return in its else branch, that will exit the current function, while forSomecannot exit the current function, which means that I'm 100% sure that the code after the forSome call will always be executed: but if let allows for return making it more complex to follow the code flow, in some cases.
The example you presented, that is:
if let error = error { return log(error) }
shows a problem with Swift in general, that is, I would really like to use a guard statement here, but I cannot bind error with a guard. So in these cases, to actually use a guard, I prefer the force unwrapping, like the following:
guard error != nil else { log(error!); return }
But this is exactly a case were forSome is useful! Because we could simply write:
The benefits of many named higher order functions are demonstrated by sequences
Sequences and collections have a large number of higher order functions built in, and they're awesome. I love writing documents.first { $0.size < sizeLimit } instead of documents.lazy.filter { $0.size < sizeLimit }.first. Sure, I can accomplish arbitrary tasks with map, filter, and reduce, but my intent as a programmer is clearer with named functions. Working with optionals should be similarly elevated.
Map appears too often in too many contexts
map is overused. Anyone who writes in the functional style is likely to find multiple uses of map operating on different types visible in their editor at the same time. I often want to know whether map is operating on a sequence or an optional, but it can be difficult to tell at first sight.
It is worth the commuity's time to offer clearer and more specific replacements for map where appropriate. As the proposal points out, forEach already tackles void-returning closures on sequences. We should address the same use case on optionals.
Bikeshedding the name
forValue plays off forEach nicely. I don't think the term "value" is strongly precedented in Swift, though.
ifPresent has a strong precedent in Java, but Java optional handling is bad enough for that to count against it in my mind.
forSome plays off both forEach and case .some but is awkwardly phrased. It sounds like it's going to take a where-clause before its body, like forSome document where document.size < 3 { doSomething(to: x) }
ifSome plays off case .some and the Java precedent of ifPresent. In my experience, many working iOS developers are not familiar with .some/.none on optionals. The reuse of the term may still be a good idea as long as we expect ifSome to teach people about .some/.none, not the other way around.
unwrapped plays off unsafelyUnwrapped. It is my preference, though ifSome and forValue are not far behind.
I don't have anything useful to say about the more exotic syntaxes suggested in this thread or the let/get/do variants.
This capability is one of the few things I sorely miss in Swift compared to Kotlin. I generally try to avoid extensions to foundational Types like Optional, so I would welcome a (more readable) option built into the language.
My usage preference would be one of the following:
// Generic function available to everything.
// Naming related to closure syntax, of which I would like to see more use
optionalString?.in { print($0) }
// Added to Optional
optionalString.unwrapped { print($0) }
// Generic function available to everything.
// Similar to Kotlin, but doesn't feel as natural in Swift
optionalString?.let { print($0) }
// Operator sugar
// Doesn't seem quite right, but may be the best option without a function.
optionalString?{ print($0) }
Whether we pass the function directly or not doesn't alter the suggestion that the best case scenario is something of that sort.
The point is not that forSome causes this problem but that the problem limits the applicability and usefulness of forSome.
Good point. Though I doubt this adds to clarity, in particular when there are multiple conditions.
If I have to name the constant to add clarity, I could just as well (and not at all more complicated) use if let, and as I stated in particular when the original constant doesn't just repeat the optional's name but already adds clarity through a more specific name.
So this would be better than if let error = error { return log(error) } ?
I agree that the context of related functions is important and that such should be considered or even added to the pitch/proposal.
That problem limits the applicability of all methods on Optional; nevertheless, those methods exist in the standard library, and are used to great extent by me and many others: they encourage a different coding style, and have both positives and negatives compared to their imperative counterparts. Thus, a forSome or something would positively extend the range of methods on Optional.
I agree, my criticism is that the forSome style doesn't make readability worse, a you'd use the more specific name as the bound constant in the closure (for example from optionalError to error).
Yes, because guard is not just an inverse if: other than better conveying meaning by showing the happy path condition (error != nil, that would make the execution keep going) it requires the body to not fall through, and return, which is the main reason why one would use it.
Definitely! I'd suggest a proposal more in the ballpark of "Enhancing Optional with method-versions of imperative statements" that includes at least for(some:none:) and filter(_:) methods.