Referring to failable initializer with leading dot syntax

If I have a struct Foo with a failable initializer, why is this expression ambiguous?

let x:Foo = .init(3)!
// error: type of expression is ambiguous without more context

this is annoying because I want to be able to write for example

guard let x:Foo = .init(13)
// value of optional type 'Foo' not unwrapped; did you mean to use '!' or '?'?

yet for some reason, this works

guard let x:Foo = Foo.init(13)

The rule for leading dot syntax is that it will look in the contextual type for a method/initializer/case whose result type is identical to that type. That means it can't find failable initializers: if it looked for them on Foo, it would find a result type of type Foo?, and if it looked for them on Optional, it wouldn't find them at all.

That said, Optional is already magic treated specially in various parts of the language—failable initializers being one of them—and so it might be reasonable to extend the rule to allow cases like this. I'm not sure how much work that would be in the type checker, though.

5 Likes

but guard let at least expects the type to be Foo?, right?

We already try to look through the Optional in cases where looking up in Optional fails to produce any candidates.

This, for example, works today:

class Foo {
  static var newFoo : Foo { return Foo() }
}

func test() {
  guard let _ : Foo = .newFoo else { return }
}

The logic is in performMemberLookup after the comment:

  // If we're looking into a metatype for an unresolved member lookup, look
  // through optional types.

This could certainly be updated to always perform lookup in the underlying type as well as Optional itself if we wanted the language to work that way.

1 Like

It's also worth mentioning that there has been some discussion about extending dot short in other directions as well. It's syntactic sugar that could support more than one (non-overlapping) resolution. This use for failable initializers (and failable factories?) would just be one way to extend it. For reference, here are the prior threads I was able to find: [Proposal Idea] dot shorthand for instance members and Dot notation as shorthand in subscripts and functions.

Why aren't you just writing

let x = Foo(3)!

That works in all the contexts you mention and requires fewer key strokes..

The decl identifier:Type = .init(...) style means that the type name is always found in the same spot no matter how the variable is initialized.

let value1:Int = -1, 
    value2:Int = .init(bitPattern: 0xffff_ffff_ffff_ffff)

It also helps with linebreaking if the typename is long, eg.

let buffer = UnsafeMutableBufferPointer<Float>(start: 
    UnsafeMutablePointer<Float>.allocate(capacity: count), count: count)

vs

let buffer:UnsafeMutableBufferPointer<Float> = 
    .init(start: UnsafeMutablePointer<Float>.allocate(capacity: count), count: count)

which allows you to break on the equals sign (a much more natural linebreaking point), and gets along nicely with initializers that aren’t called init, like

let buffer:UnsafeMutableBufferPointer<Float> = .allocate(capacity: count)
1 Like

I noticed something weird, how come this works?

enum Foo 
{
    case a, b, c
}

let foo:Foo? = .a

.a is a member on Foo, not Optional<Foo>

That's Mark's comment above:

I don’t see any Optionals in that example? but whatever do you think it’d be worth a proposal?

Foo? is Optional<Foo>.

Yes, I think your original idea is a reasonable proposal (to allow dot syntax to find things returning Optional<X> and not just X, such as failable initializers).

1 Like

i was talking about mark’s comment lol

1 Like

Ah, I think the guard let provides enough Optional context, like you said at the time.

1 Like

Update:

I had become convinced that leading dot had suddenly started working because i realized i could write this

guard let bar:Bar = .create()

One /guard\s+let\s+[a-zA-Z0-9]+(\s*:\s*[a-zA-Z0-9]+)?\s*=\s*[A-Z][a-zA-Z0-9]*\./ later i realized this still does not work:

guard let bar:Bar = .init()

this behavior really does not make sense at all.

I guess this is just one of many inconsistencies related to inconsistent conveniences. I expect that the recently accepted special case added to the behavior of try? will add some more.

A special case that is convenient in one situation will probably be inconvenient/confusing in some other situation.

And I guess "fixing" the "inconvenient" side of these special cases will have to be done by adding more special cases, ie by counterbalancing rather than removing special cases, since they were introduced for the sake of (local/isolated/case-by-case) convenience/ergonomics. This breaks down the difference between "fixing" and "introducing" bugs.

I'd vote for consistency over convenience, or conveniences that are consistent / does not involve special cases that can be both convenient and inconvenient.

:P :)

Swift Evolution is the perfect place for reaching this goal.

Yes, the Swift compiler has many convenience features. The intent behind them is almost always the same: let the user build a self-consistent mental model of a given Swift feature, because this is a precondition for programmer productivity. This is UX applied to language design.

In the domain of optionals, for example, such conveniences are quite abundant. Without them, the language would be more pure and simple, but way less usable. Imagine if you could not, for example, feed a function that accept optionals with a non-optional value. You'd have to write label.text = .some("meh").

Now, there is one subtlety, and a few cracks.

The subtlety is that there is not a single "self-consistent mental model" for a given Swift feature. There are many. This is because progressive disclosure, a well-entrenched design goal of Swift, lets a user enter in the language and build, step after step, on his own pace, a more complete knowledge of the language. It is a trial-and-error process, as all learning processes.

Unfortunately, there are the cracks: all the inconsistencies and inconveniences created by both missing patches, and ill-advised conveniences. Those cracks do harm. They don't help the Swift programmer in his trial-and-error learning process, on the contrary! It's difficult to build in one's mind a consistent mental model of a feature when surprising exceptions appear here and there :sweat_smile:. It is difficult for a very precise reason: the user is forced to look at a deeper level in order to figure out the reason for the experienced inconsistency. The user can no longer learn at his own pace: those cracks break progressive disclosure.

(Side note: by reading and contributing to those forums, we have all opted in for a Swift crash course that lets us look at the last pages of the book. It is to be expected that some of us get confused sometimes. I regularly am. But the benefits overweigh the costs, at least for me.)

This thread is about a forgotten patch that aims at aligning the actual language with one of the mental models of "leading dot syntax". SE-230 is a fix for an ill-advised convenience. Things get better :+1:

2 Likes

I agree that this is a discussion for another thread.

But I'll leave my reply here.

I think we largely agree, ie that consistent conveniences and progressive disclosure are very nice, and a must have in Swift.

We probably disagree on specific cases of what is and isn't a crack and/or ill-advised convenience.

An example of this is SE-230, which imo "solves" a problem by complicating the behavior of the already problematic try? instead of getting at the root cause.

The current behavior of try? is simpler (always return the sub-expression's type with one added layer of optionality, no matter the type the sub-expression, because the added layer represents the error handling that was converted from throws to optional) than what it will be with SE-230, because it introduces the special case of not adding the throws-to-optional-layer if the sub-expression happens to already be a statically knowable optional type. I say statically knowable because the following special case of the special case has to be part of the "fix":

Generic code that uses try? can continue to use it as always without concern for whether the generic type might be optional at runtime. No behavior is changed in this case.

To me, try? is an ill-advised convenience to begin with, and will be more so with SE-230 added on top. The specific design of the added convenience comes at the cost of a much more complicated behavior; with possibly needless special cases that has to be disentangled by users before they can continue to progressively disclose a more complete knowledge of the language.

My guess is that Optionals (and all their magic) is one of the biggest stumbling blocks for new users and their ability to form a consistent mental model of Swift, and I think this is because not enough care has been taken to make sure that all their conveniences are as consistent and simple as possible.

It's as if Swift tries too hard to hide what an optional really is, because it hasn't actually decided what it is. Should it be possible to nest or not? Why nil and not .none? etc. Such ambivalence leads to the forming of misconceptions rather than useful concepts.


I'll let others create a separate thread in case they wish to continue this discussion