[swift-evolution] [Pre-pitch] Conditional default arguments

Tony Allevato tony.allevato at gmail.com
Mon Nov 27 18:10:11 CST 2017


On Mon, Nov 27, 2017 at 3:38 PM Matthew Johnson <matthew at anandabits.com>
wrote:

> On Nov 27, 2017, at 4:55 PM, Tony Allevato <tony.allevato at gmail.com>
> wrote:
>
>
>
> On Mon, Nov 27, 2017 at 2:39 PM Matthew Johnson <matthew at anandabits.com>
> wrote:
>
>> On Nov 27, 2017, at 4:25 PM, Tony Allevato <tony.allevato at gmail.com>
>> wrote:
>>
>>
>>
>> On Mon, Nov 27, 2017 at 2:19 PM Matthew Johnson <matthew at anandabits.com>
>> wrote:
>>
>>> On Nov 27, 2017, at 3:56 PM, Howard Lovatt via swift-evolution <
>>> swift-evolution at swift.org> wrote:
>>>
>>> Really like Tony’s suggestion, much cleaner than yet another annotation
>>> rammed into the signature. Also the idea of a static factory that could
>>> accept previously initialized arguments would be very powerful.
>>>
>>>
>>> It is syntactically cleaner at the site of the function declaration for
>>> sure.  Wouldn’t you agree that it is semantically less clear though?  Both
>>> are important, but which is more important?  If not semantics, why not?
>>>
>>> It’s also worth noting that it would be more verbose in at least some
>>> use cases (when a declaration such as `defaultConfiguration()` is used in
>>> the conditional default expression and is not otherwise necessary).
>>>
>>
>> (Apologies for the earlier blank reply—why is Cmd+Enter a keyboard
>> shortcut for Send something someone would think is a good idea?)
>>
>> That verbosity is kind of a feature of my design, in the sense that it
>> describes exactly what's going on at the location in code where someone
>> expects to see default values. If anything, the default factory name is an
>> opportunity to add context where normally there might be none.
>>
>> It's also worth noting that this design works well if you have multiple
>> methods that need to share the same defaults. Instead of repeating the same
>> annotations for each method that needs them, you just define the functions
>> once and refer to them everywhere. (Could you do the same with the
>> annotation based method? Probably, if you allow arbitrary expressions
>> within them. But that seems less obvious compared to this approach.)
>>
>>
>> You make a really good point here.  The annotations would need to be
>> applied to every declaration that uses them whereas your proposed syntax
>> would just rely on overload resolution succeeding or failing in a given
>> type context.  The use case I have would benefit in this respect as the
>> conditional defaults would be shared by several declarations.
>>
>> Howard’s idea of restricting conditional defaults to only use
>> declarations in the same file seems somewhat arbitrary but it would go a
>> long way towards helping an author understand clearly what the provided
>> defaults are as the rest of the module and imported symbols would not need
>> to be considered.  I wonder if this approach could be refined a bit so it
>> feels less arbitrary.  Users would probably need to rely on tooling to
>> discover defaults but I think I’m ok with that.  I am mostly concerned with
>> the ability of an author to reason locally about the API contract they are
>> publishing.  Do you have any ideas on how to refine Howard's idea?
>>
>
> I mentioned earlier in the thread (with a few messed up details) that
> there are already some restrictions on default arguments that I think
> already work well here. Declarations referenced in the default value
> expression of a function today must be accessible within the scope of the
> declaration being defined, so I'm not sure if we need to go further than
> that.
>
> Here's an example that I'll admit is completely contrived, but should I be
> prevented from doing this? Let's say I define an "Identities" module with
> this type:
>
> ```
> enum Identities {
>   func identity() -> Int { return 0 }  // let's ignore additive vs.
> multiplicative for a moment
>   func identity() -> Double { return 0.0 }
>   func identity() -> () { return () }
>   func identity() -> String { return "" }
>   func identity<T>() -> (T) -> Void { return { _ in } }
>   // and so on
> }
> ```
>
> Then somewhere I want to define a conditional default, using those
> identities:
>
> ```
> import Identities
>
> func someWeirdThing<T>( ...contrived args..., defaultValue: T =?
> Identities.identity()) { ... }
> ```
>
> Swift today already allows this with regular default value expressions, so
> the problem of tooling being needed to discover defaults already exists and
> this hypothetical construct doesn't change that. We *could* restrict such
> defaults to same file or same module and it seems reasonable to do so, but
> should we? If the defaults I want happen to live elsewhere, why not let me
> use them? Or, if it's a serious concern, why not lock down all default
> expressions the same way?
>
>
> You make a really good point about the current behavior default value
> expressions that I hadn’t fully considered.  It is currently possible for a
> new, more specific declaration to be introduced that would be selected.  It
> is also currently possible for the symbol that is resolved to be removed
> while a less specific declaration is available for resolution.  I think the
> reason I hadn’t considered this is that they always resolve to the same
> type (possibly a generic T) and a default is always available.  It seems
> unlikely that a change in overload resolution in this context would be
> problematic.
>
> Dave Abrahams summed up my reluctance to embrace your proposed solution
> very concisely:
>
> This sort of “it compiles if it’s syntactically valid, regardless of
> declared constraints” thing is deliberately avoided in Swift’s generics
> design with good reason; it’s possible that in this instance there are no
> problems, but I’m skeptical.
>
>
> You are effectively proposing that in this very narrow case we perform
> overload resolution on a symbol in a generic type context *after* the
> generic type has been replaced with a concrete type.  In every other case
> Swift has been very intentionally designed to resolve the symbol in a
> generic type context using the known constraints on the generic type.  Do
> you agree that this is a good rule in general?  If so, why should we make
> an exception to it?  If not, why not?
>

I totally agree that that's a good rule in general—I'm not 100% comfortable
making an exception to it for this, but I wanted to start a discussion
about a different approach than had been considered so far.

The idea of forcing the user to acknowledge the explicitness of SFINAE with
a strawman syntax `=?` instead of `=` was a thought experiment to bridge
the wild-west-C++ world of templates and Swift's stricter generics, but I
can definitely understand if even that kind of approach is something that
the core team (who are far more familiar with the C++ side of that coin
than I am) doesn't wish to support. As was pointed out, it's not something
Swift supports anywhere else today.

If we look at it from that point of view, where such a semantic treatment
of generics would not be supported, I think it becomes a lot harder to
rationalize treating this as "default arguments". What you really do have
(and what writing it as constrained extensions makes clear) is additional
overloads, because they only apply to certain subsets of types. If that's
the case, maybe it's the wrong approach to try to turn overloads into
"partial default values".


> I think the bigger concern is the other one Xiaodi mentioned—we probably
> don't want people to be able to retroactively add overloads that would
> introduce a default where previously there was none. (This wouldn't be
> possible for global functions in different modules, but could be for
> non-private type members.) This might be something that Just Works Out[image:
> ™] depending on how and when the compiler resolves such expressions—but I
> don't know the compiler deeply enough to say for sure without investigating
> more.
>
>
> I think impact would be limited to the current module but I am also
> concerned that “this might be something that Just Works Out[image: ™]”
> isn’t good enough.  IMO, we need to know or sure that it will work out ok
> in this context before we consider it.
>

I think I'm being misunderstood here. Naturally I'm not saying we should
just go full-speed into an approach and hope that it "just works out". What
I'm saying is, I think there's potential with that approach that I don't
have enough knowledge in that area and I'm hoping someone with more
knowledge will chime into the discussion. :)

So to be clear, I'm not suggesting that my proposed approach, unmodified,
is the way we should definitely go. My intention is just to pitch something
that aims to be closer to how defaults work otherwise in the language, and
try to find a way to fit this use case into it, rather than wedging in an
@annotation.



>
> Please don't misunderstand - I appreciate the elegant syntax of the design
> you are proposing.  But I would hate to see it adopted only to have us
> regret the its semantics afterwards.
>
>
>
>
>>
>>
>>
>>
>>>
>>> -- Howard.
>>>
>>> On 26 Nov 2017, at 9:25 am, Tony Allevato via swift-evolution <
>>> swift-evolution at swift.org> wrote:
>>>
>>> On Sat, Nov 25, 2017 at 1:16 PM Xiaodi Wu <xiaodi.wu at gmail.com> wrote:
>>>
>>>> On Sat, Nov 25, 2017 at 15:06 Matthew Johnson <matthew at anandabits.com>
>>>> wrote:
>>>>
>>>>> On Nov 25, 2017, at 1:28 PM, Tony Allevato via swift-evolution <
>>>>> swift-evolution at swift.org> wrote:
>>>>>
>>>>> On Fri, Nov 24, 2017 at 7:18 PM Xiaodi Wu via swift-evolution <
>>>>> swift-evolution at swift.org> wrote:
>>>>>
>>>>>
>>>>> It's kludgy, but we could have something like:
>>>>>>
>>>>>> ```
>>>>>> @defaultArgument(configuration = (), where R.Configuration == Void)
>>>>>> @defaultArgument(actionHandler = { _ in }, where R.Action == Never)
>>>>>> func makeResource(with configuration: R.Configuration, actionHandler:
>>>>>> @escaping (R.Action) -> Void) -> R { ... }
>>>>>> ```
>>>>>>
>>>>>> I don't like that we'd be setting a default argument on something
>>>>>> lexically before even encountering it in the declaration, but it's
>>>>>> serviceable.
>>>>>>
>>>>>
>>>>>
>>>>> What if we could take advantage of the fact that you can have
>>>>> non-constant expressions in default arguments? Overload resolution could
>>>>> already do most of the job—what we need on top of that is a way for the
>>>>> author to say that “if no overload matches, then it’s not an error—just
>>>>> don’t have a default argument in that case”. Something like SFINAE in C++,
>>>>> but more explicit.
>>>>>
>>>>> I’m imagining something like this:
>>>>>
>>>>> func defaultConfiguration() -> Void {
>>>>>   return ()
>>>>> }
>>>>> func defaultActionHandler() -> (Never) -> Void {
>>>>>   return { _ in }
>>>>> }
>>>>> struct ResourceDescription<R: Resource> {
>>>>>   func makeResource(
>>>>>     with configuration: R.Configuration *=?* defaultConfiguration(),
>>>>>     actionHandler: @escaping (R.Action) -> Void *=?* defaultActionHandler()
>>>>>   ) -> R {
>>>>>     // create a resource using the provided configuration
>>>>>     // connect the action handler
>>>>>     // return the resource
>>>>>   }
>>>>> }
>>>>>
>>>>> The main difference here is the strawman =? syntax, which would
>>>>> indicate that “the default argument exists if there is a way the RHS can be
>>>>> satisfied for some instances of the generic arguments; otherwise, there is
>>>>> no default”, instead of today’s behavior where it would be an error. There
>>>>> could be multiple overloads of defaultConfiguration and
>>>>> defaultActionHandler (even ones that are themselves generic) and it
>>>>> would do the right thing when there are matches and when there aren’t.
>>>>>
>>>>> I like this approach because it mostly takes advantage of existing
>>>>> language features and is fairly lightweight in terms of how it’s expressed
>>>>> in code compared to regular default arguments—we’d just need to design the
>>>>> new operator and type-checker logic around it.
>>>>>
>>>>> This is an interesting approach.  One advantage to something in this
>>>>> direction is that it could support defining different defaults for the same
>>>>> argument under different constraints by overloading the default argument
>>>>> factories on their return type.
>>>>>
>>>>> One concern I have is that it doesn’t allows us to clearly define
>>>>> under which constraints a default argument is available.  I suspect this
>>>>> might be problematic especially for public interfaces where source
>>>>> compatibility is a concern.
>>>>>
>>>>
>>>> It's certainly an interesting idea but it would suggest that the
>>>> constraints under which a default argument is available can change at
>>>> runtime. I'm concerned, like you, that this is difficult to reason about.
>>>> It is still unclear to me how widespread the underlying issue is that
>>>> requires conditional default arguments, but the conversation thus far has
>>>> been about compile-time constraints and Tony's design seems to envision
>>>> much more than that.
>>>>
>>>
>>> This runtime/reasoning problem *already exists* today with default
>>> arguments, because you can write something like this:
>>>
>>> struct Foo {
>>>   static var defaultExponent = 2.0
>>>
>>>   func raise(_ x: Double, to exponent: Double = defaultExponent) {
>>>     print(pow(x, exponent))
>>>   }
>>> }
>>> Foo().raise(4)  // "16.0"Foo.defaultExponent = 3.0Foo().raise(4)  // "64.0"
>>>
>>> Swift lets you write a default value expression that references static
>>> (but not instance) vars of the enclosing type, as well as anything else
>>> that’s visible from that expression’s scope. Should people do this?
>>> Probably not, for the reasons that you described.
>>>
>>> But the point is that my example is no more harmful or difficult to
>>> reason about than default arguments in the language today. My proposed
>>> solution *in no way* changes the runtime behavior of default argument
>>> expressions. I’m not envisioning anything more than what default arguments
>>> can already do except for adding a way to choose different default
>>> factories (or choose none without error) based on the *static* types of
>>> the generic arguments that are bound at a particular call site.
>>>
>>>
>>>> I think I prefer Xiaodi’s suggestion for that reason.  His approach
>>>>> could also support multiple defaults for the same parameter as long as the
>>>>> constraints are not allowed to overlap (overlapping constraints would
>>>>> result in ambiguity similar to ambiguous overload resolution) or an
>>>>> explicit argument is required if they do.
>>>>>
>>>>>
>>>>>
>>>>>
>>>>>
>>>>>>
>>>>>> On Fri, Nov 24, 2017 at 8:36 PM, T.J. Usiyan via swift-evolution <
>>>>>> swift-evolution at swift.org> wrote:
>>>>>>
>>>>>>> I am all for this. are many types where there is an obvious 'zero'
>>>>>>> or 'default' value and the ability to express "use that when possible"
>>>>>>> without an overload is welcome.
>>>>>>>
>>>>>>>
>>>>>>> The best thing that I can think of right now, in terms of syntax, is
>>>>>>> actually using @overload
>>>>>>>
>>>>>>> ```
>>>>>>> struct ResourceDescription<R: Resource> {
>>>>>>>
>>>>>>>   func makeResource(with configuration: R.Configuration,
>>>>>>> actionHandler: @escaping (R.Action) -> Void) -> R
>>>>>>>  @overload(R.Configuration == Void) func makeResource(actionHandler:
>>>>>>> @escaping (R.Action) -> Void) -> R
>>>>>>> @overload(R.Action == Never)  func makeResource(with configuration:
>>>>>>> R.Configuration) -> R
>>>>>>> {
>>>>>>>     // create a resource using the provided configuration
>>>>>>>     // connect the action handler
>>>>>>>     // return the resource
>>>>>>>   }
>>>>>>> }
>>>>>>> ```
>>>>>>>
>>>>>>>
>>>>>>> This isn't great though…
>>>>>>>
>>>>>>> On Fri, Nov 24, 2017 at 6:11 PM, Matthew Johnson via swift-evolution
>>>>>>>  <swift-evolution at swift.org> wrote:
>>>>>>>
>>>>>>>> As mentioned in my prior message, I currently have a PR open to
>>>>>>>> update the generics manifesto (
>>>>>>>> https://github.com/apple/swift/pull/13012).  I removed one topic
>>>>>>>> from that update at Doug Gregor’s request that it be discussed on the list
>>>>>>>> first.
>>>>>>>>
>>>>>>>> The idea is to add the ability to make default arguments
>>>>>>>> conditional (i.e. depend on generic constraints).  It is currently possible
>>>>>>>> to emulate conditional default arguments using an overload set.  This is
>>>>>>>> verbose, especially when several arguments are involved.  Here is an
>>>>>>>> example use case using the overload method to emulate this feature:
>>>>>>>>
>>>>>>>> ```swift
>>>>>>>> protocol Resource {
>>>>>>>>   associatedtype Configuration
>>>>>>>>   associatedtype Action
>>>>>>>> }
>>>>>>>> struct ResourceDescription<R: Resource> {
>>>>>>>>   func makeResource(with configuration: R.Configuration,
>>>>>>>> actionHandler: @escaping (R.Action) -> Void) -> R {
>>>>>>>>     // create a resource using the provided configuration
>>>>>>>>     // connect the action handler
>>>>>>>>     // return the resource
>>>>>>>>   }
>>>>>>>> }
>>>>>>>>
>>>>>>>> extension ResourceDescription where R.Configuration == Void {
>>>>>>>>   func makeResource(actionHandler: @escaping (R.Action) -> Void)
>>>>>>>> -> R {
>>>>>>>>     return makeResource(with: (), actionHandler: actionHandler)
>>>>>>>>   }
>>>>>>>> }
>>>>>>>>
>>>>>>>> extension ResourceDescription where R.Action == Never {
>>>>>>>>   func makeResource(with configuration: R.Configuration) -> R {
>>>>>>>>     return makeResource(with: configuration, actionHandler: { _ in
>>>>>>>> })
>>>>>>>>   }
>>>>>>>> }
>>>>>>>>
>>>>>>>> extension ResourceDescription where R.Configuration == Void,
>>>>>>>> R.Action == Never {
>>>>>>>>   func makeResource() -> R {
>>>>>>>>     return makeResource(with: (), actionHandler: { _ in })
>>>>>>>>   }
>>>>>>>> }
>>>>>>>>
>>>>>>>> ```
>>>>>>>>
>>>>>>>> Adding language support for defining these more directly would
>>>>>>>> eliminate a lot of boilerplate and reduce the need for overloads.  Doug
>>>>>>>> mentioned that it may also help simplify associated type inference (
>>>>>>>> https://github.com/apple/swift/pull/13012#discussion_r152124535).
>>>>>>>>
>>>>>>>> The reason that I call this a pre-pitch and one reason Doug
>>>>>>>> requested it be discussed on list is that I haven’t thought of a good way
>>>>>>>> to express this syntactically.  I am interested in hearing general feedback
>>>>>>>> on the idea.  I am also looking for syntax suggestions.
>>>>>>>>
>>>>>>>> Matthew
>>>>>>>>
>>>>>>>> _______________________________________________
>>>>>>>> swift-evolution mailing list
>>>>>>>> swift-evolution at swift.org
>>>>>>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>>>>>>>
>>>>>>>>
>>>>>>>
>>>>>>> _______________________________________________
>>>>>>> swift-evolution mailing list
>>>>>>> swift-evolution at swift.org
>>>>>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>>>>>>
>>>>>>>
>>>>>> _______________________________________________
>>>>>> swift-evolution mailing list
>>>>>> swift-evolution at swift.org
>>>>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>>>>>
>>>>>
>>>>>
>>>>> _______________________________________________
>>>>> swift-evolution mailing list
>>>>> swift-evolution at swift.org
>>>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>>>>
>>>>> _______________________________________________
>>> swift-evolution mailing list
>>> swift-evolution at swift.org
>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>>
>>> _______________________________________________
>>> swift-evolution mailing list
>>> swift-evolution at swift.org
>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>>
>>>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20171128/97a11110/attachment.html>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: emoji_u2122.png
Type: image/png
Size: 795 bytes
Desc: not available
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20171128/97a11110/attachment.png>


More information about the swift-evolution mailing list