Why can't Int.init() take an optional StringProtocol?

I had a hard time finding any discussion of this online, but I wanted to ask about the reasoning of initializers like Int("123"), which return Int?. It’s declared as

public init?<S>(_ text: S, radix: Int = 10) where S : StringProtocol

It seems that it could just as easily have been implemented to accept an optional StringProtocol, in which case it would return nil if supplied nil, and it would allow for more succinct expressions like this:

let port = Int(Environment.get("DATABASE_PORT")) ?? 3306

which currently must be written (possibly) like this:

let port = {
    if let ps = { Environment.get("DATABASE_PORT"), let p = Int(ps) {
        return p
    } else {
        return 3306
    }}()

I’m curious about the reasoning here, and whether or not there’s any hope of changing the standard library to be more accommodating.

Thanks!

Note the Int("") will return nil so you could use:

let port = Int(Environment.get("DATABASE_PORT") ?? "") ?? 3306

You can also use Optional.flatMap(_:)

let port = Environment.get("DATABASE_PORT").flatMap(Int.init) ?? 3306
8 Likes

Okay, so those make things a little more concise, but they still don't seem as elegant as Int accepting nil, right?

But by this logic, shouldn't all failable and throwing initializers that currently takes one or more (non-optional) arguments be rewritten to accept optional arguments?

11 Likes

I don’t think it should take optional just because it can fail. It should take optional if it can succeed with nil.

19 Likes

Returning nil is one of the ways of signaling that an error has occurred. (Others include throwing, fatalError, etc. See the Error Handling Rationale and Proposal.)

It is a design goal of the language to help users reason about how they would handle errors and an anti-goal to allow errors not to be reasoned about.

We certainly have facilities such as optional chaining to make usage more ergonomic in certain circumstances, as well as functions such as flatMap already demonstrated above.

In all cases, however, the user indicates in some way how they wish to proceed if the result is nil. This is a tentpole feature of the language. Silent propagation of errors is not more elegant and would not be consistent with the overall direction of Swift.

11 Likes

I've had ideas similar to this case in the past. There's always some bit of change to the API that seems like a no-brainer. Improves readability, makes it shorter, etc.

The problem is that there are a lot of similar proposals, that each seem like obvious in their own right, which would be a total disaster if adopted together.

For example, I often find myself with a f: (A) -> B, wishing I could just pass it an array. So I would consider writing an f: ([A]) -> [B]. But then sometimes I want to do the same with promises, so I'd want an f: (Promise<A>) -> Promise<B>. I talk more about this in my answer here.

Other cases I've run into:

  1. I want my argument to be lazily evaluated, so I make it an autoclosure
  2. I want my closure argument to be able to throw and pass along the error, so I make it throws and make my functions rethrows.

The problem is that trying to be generic over all these variations simultaneously just complicates the UI. The solution is to abstract these responsibilities out to separate functions, like Optional.map, Array.map, and Promise.map. They abstract the idea of "I have a (A) -> B, but I with it was applicable to my Monad<A>.

Even though it needs slightly more characters to type, it saves your interfaces from exploding from generalizing over too many things.

7 Likes

I'm a member of the I Hate Optionals club. It's a simple fact of Swift life that use of optionals will result in awkward code, some of the time. I usually deal with this by converting optionals to concrete types as soon after the optional is created as I can and then only passing around concrete types to the rest of my code. Occasionally I'll have a couple related methods where one calls the other and I would normally prefer to pass a concrete type to the second method but accepting an optional makes the calling code neater. This is almost always with private methods in a type so I can adjust the caller and the callee as I like.

Anyway, this is basically what you're asking for: a helper method that makes your calling code neater. There are several helper methods that you can write to make your calling code neater. You can write the Int init(String?)method that you're asking for. (Go for it. Life's too short to wait around for the std library to add missing pieces that seem simple.) You could also write a simple method that does what that init method would do but isn't an init method: func myInt(from: String?) -> Int?

However, one of your problems is a method that returns an Int as a String. Why not fix that? Write this on Environment: func get(String) -> Int? That turns your problematic line into a one-liner.

let port: Int = Environment.get("DATABASE_PORT") ?? 3306

Not in the hate club, but that's usually the most appropriate things to do, to unwrap them as soon as you need to differentiate nil from some. I do pass Optional around if I don't care about nil vs some, though.

2 Likes

I guess my thinking with Int is it's returning nil to say "I can't parse this." It seems to follow that it can't parse nil, either, and it sort of flows through. It might be like writing this:

extension String {
    func parseInt() -> Int?
}

var s: String? = "123"
let v1 = s.parseInt() ?? 123   //  v1 is Int
s  = nil
let v2 = s.parseInt() ?? 123  // v2 is Int

Allowing an optional lets the optional "flow through," like it would when chaining. If it that makes it harder to reason, then optional chaining is similarly bad.

And is flatMap really a good solution here? What if I have to do this millions of times? Will the compiler optimize it down to a small bit of inline code?

As I wrote above, optional chaining requires an explicit indication at the use site. That is the entire point: the user must indication what they intend to do with a nil value; it must not propagate implicitly ("flow through").

What you write above doesn't work in Swift. You must explicitly write s?.parseInt() ?? 123.

If you're stating that what you write should be allowed, then indeed my reply would be that this example is similarly contrary to the overarching design philosophy of Swift, for the same reason.

2 Likes

If it wasn't a good solution for the very thing it says on the tin ("evaluates the given closure when this Optional instance is not nil , passing the unwrapped value as a parameter") or didn't have good performance, what would be the purpose of having Optional.flatMap at all?

Yes, you're right, that was a typo, my bad.

But Int(nil) ?? 123 seems just as explicit as s?.parseAsInt() ?? 123. Eh, maybe not. I dunno. I'm unconvinced of the benefit, but I'm not going to die on this hill.

If you’d work better with version with Optional arguments, then, by all means.
But It’d be a sloppy design in general if it’d fail just in the presence of nil argument, especially when the type system can easily prevent that.

Again, to answer your specific question (the one in the title):

If we agreed that the particular initializer in question should take an optional argument, ie this:

Int.init?(_ description: String)

should be changed to:

Int.init?(_ description: String?)

Then, by the same logic, and in order to keep things consistent, it would of course follow that the same change should be made to all the corresponding failing initializers for Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64, Float, Double and Float80.

And also for all failing and/or throwing initializers in the Standard Library, like String(contentsOf: URL)String(contentsOf: URL?), and I guess something like a hundred others.

This is unreasonable, not only because of the number of APIs involved, but because there's no reason why those API should assume that their arguments are optional rather than non-optional in the common case.

Looking at it from this perspective, you have a specific use case / problem, for which there are existing solutions (like unwrapping earlier, or using Optional.flatMap, etc). The solution you are suggesting / asking about, however, would imply the above (kind of huge) change to the Standard Library's API and design philosophy.


You'd like to be able to write:

let port = Int(Environment.get("DATABASE_PORT")) ?? 3306

You can write this today:

let port = Environment.get("DATABASE_PORT").flatMap(Int.init) ?? 3306

Note that the semantics of this (as well as the one you'd want to be able to write) is such that you'll get for example this:

String? : Environment.get("DATABASE_PORT")    |    Int : port 
----------------------------------------------+---------------------
nil                                           |                 3306
"9223372036854775807"                         |  9223372036854775807
"-9223372036854775808"                        | -9223372036854775808
"blah"                                        |                 3306

Which might or might not be what you want. This is another reason for why optional return values should often be dealt with immediately and thoughtfully rather than just kept and passed around.

4 Likes

One aspect of this question that hasn't been touched upon is why Swift diverged (somewhat) from Objective-C on this. It was never a strong convention in Objective-C, but "return nil if one of my arguments is nil" was fairly common, and went with the language rule of "return nil if the receiver (self) is nil". My answer for this would be twofold:

  • When you use Optional for a parameter with a type that has an "empty" representation, like String or Array, it's not always obvious what the difference in behavior would be for passing nil vs. passing "". If there isn't a difference, it's less burden on the users of the API if there's only one possible choice of what to pass.

  • One of the main purposes of distinguishing Optional and non-Optional types is to catch bugs where the user forgot that an operation could fail. The classic example for this goes something like:

let storage = getCloudStorage() // can return nil!
game.save(to: storage)
alert("Saved successfully!")

Neither of these exactly applies to Int.init(_:), since as you note that's already marked failable, and this is just one more way to fail. But passing a literal nil to Int.init(_:) would obviously be a mistake, and there's no reason to let developers do that. I think that's the principle here: if passing nil is always going to cause the operation to fail, the compiler should check that statically, and the developer should decide how they want to handle that. In this case, flatMap is your way of communicating your intent to the compiler.

I don't go as far as @phoneyDev to say I Hate Optionals—indeed, I Love Optionals. But the power of optionals is that they let you make the rest of your code not use optionals, i.e. you can now distinguish between what always produces a value and what might not.

P.S. There has been a lot of discussion about how to generalize optional chaining (foo?.bar) so that it would work with parameters as well as receivers, which would "solve" this problem in general. But none of the suggestions have been syntactically clean enough to really get support and move to a proposal.

17 Likes

Feels like suffix ? was the closest one we've got, not that there's much room for variations.

1 Like

As I recall, in the last thread about it (Unwrapped Arguments for Functions), there was broad agreement that postfix-? was the right spelling.

Prior discussions had bogged down in debating evaluation order and short-circuiting, but then Rob Mayoff observed that we have already answered exactly those questions for try, and it would make sense to match the behaviors:

Since throwing in argument position will short-circuit and prevent evaluation of subsequent arguments, it follows that a nil argument with postfix-? should do the same.

If that is something we want in the language, I believe the spelling and behavior are essentially nailed down. It’s just a matter of making it happen.

5 Likes

I find this to be a very appealing solution. It seem to satisfy both competing interests.