Thoughts Regarding the Potential Assignment of Functions to Labelled Identifiers

Dear Swift,

In light of reinvigorated discussions regarding value identifiers (specifically, function and closure names) being enabled to encode (or 'carry') their own argument labels in Swift, here are offered two interconnected discussions regarding the potential approaches and benefits of being able to more expressively bind functions and closures to labelled identifiers in Swift.

1. Partial application of functions
2. Re-binding argument labels

I have created this topic principally to express a perspective of these connected ideas in one (hopefully) cogent manner. It is offered in the spirit that perhaps even the most humble thoughts might be of some value as Swift continues to develop as a capable, elegant and cohesive language.

1. Partial application of functions

Motivation

At the moment, partial application of functions in Swift is possible, but somewhat cumbersome. From what I can tell, these are the options currently available:

One may prepare a 'manually' curried function, and thereupon apply that function to each consecutive argument in a pre-defined order:

func curried(x: Int) -> (Int) -> (Int) -> Int {
    return { y in { z in x + y + z } }
}
let partCurried = curried(x: 5)
let finalValue = partCurried(6)(7)

One may wrap an existing function in a closure:

func plunk(_ x: Int, claxing y: Int, bewith z: Int) -> Int {
    return x + y + z
}
let partPlunk = { plunk($0, claxing: 6, bewith: $1 ) }
let finalValue = partPlunk(5, 7)

One may elect to 'concrete-wrap' an existing function into a new function entirely, encapsulating the concrete 'partial' arguments within the new function:

func partPlunk(_ x, bewith z: Int) -> (Int) -> Int {
    return { x, z in plunk(x, claxing: 6, bewith: z ) }
}
let finalValue = partPlunk(5, bewith: 7)

The above approaches of course apply to methods in much the same way as they do to functions. One may, for instance, wish to 'collapse' a value and one of its methods into a new, partially applied function.

In simple cases, the method is partially applied to its 'self':

// defined on Thing:
func method(_ x: Int, claxing y: Int) -> Int { ... }

// partially applied to 'self':
let collapsed = Thing().method(_:claxing:)
let finalValue = collapsed(5, 9)

In more elaborate cases, the method's other arguments may also be selectively supplied:

// partially applied to 'self', and also another argument:
let collapsed = { Thing().method($0, claxing: 9) }
let finalValue = collapsed(5)

Sketch

I wonder whether Swift might be ready for a more 'natural' mechanism for partially applying functions than these existing approaches afford.

Perhaps, for instance, the underscore character ( _ ) could be harnessed to stand in for values which are deliberately omitted from a function 'call'. The resulting value would be a function of reduced arity, with the remaining arguments occupying the same sequential order in which they were originally defined:

For functions:

let partiallyApplied = plunk(_, claxing: 6, bewith: _ )
let finalValue = partiallyApplied(5, 7)

For methods taking arguments beyond 'self':

let collapsed = Thing().method(_, claxing: 9)
let finalValue = collapsed(5)

For operators:

let addOne = ( _ + 1 )
let finalValue = addOne(41)

let oneAdd = ( 1 + _ )
let finalValue = oneAdd(41)

Benefits

The benefits of such an affordance seem three-fold:

Firstly, this approach is tidy, and more immediately comprehensible than current options (which are really 'workarounds' to achieve the desired outcome).

Secondly, this approach facilitates partial application by supplying any one or more arguments of a function. Unlike currying, arguments would not need to be supplied in a pre-determined order of application.

Thirdly, at this point in time, partial application of functions seems to be relatively rare in Swift. Once equipped with ergonomic affordances, and documented as a feature of the language, new and useful patterns could emerge, or patterns utilised in other languages could become more accessible in Swift.

Impact on existing code

With the caveat that I lack a certain level of expertise in these matters, I understand that this feature would be purely additive to swift, and would not affect existing code. In other words, existing approaches to partial application would continue to work.

Alternatives considered

A special set of functions (let's call them partiallyApply(f:to:)) could be provided which take a function and n arguments, and return a partially applied function. I understand that in Swift a family of function overrides would be required, one for each potential arity of f, and that a distinction would need to be made in user-space between functions and methods. A mechanism would also be required to allow non-homogenous argument types to be provided (i.e. I understand that an array or variadic parameter would not work). Such an approach would be akin to Python's' partial and partialMethod functions from functools. However this approach doesn't seem particularly 'Swift-like', and I suggest that it is a less elegant solution even than the existing availability of closure wrappers in Swift.

Another alternative would be a built-in currying syntax ā€“ a feature which has already been part of Swift, and has subsequently been removed (for reasons which I fully appreciate, and don't imagine have changed since then).

As an alternative syntax, specifically for functions which are named as operators, a Haskel-like approach could be used, omitting the underscores, and instead implicitly omitting the missing argument:

let addOne = (+1)
let finalValue = addOne(41)

However, for reasons of flexibility, consistency, predictability and explicitness, the underscores might be better retained for operators, in keeping with the syntax for other functions and methods:

let addOne = ( _ + 1 )
let finalValue = addOne(41)

let oneAdd = ( 1 + _ )
let finalValue = oneAdd(41)

2. Re-binding argument labels

Please note that in keeping with the convention employed by the book 'The Swift Programming Language', the following discussion uses the term 'argument labels' to refer to a function's externally visible argument labels.

Motivation

In the foregoing discussion, one notices that in all cases (except the 'concrete-wrap' approach), argument labels are 'lost' to partially applied functions, as indeed they are any time a function is assigned to a new identifier in Swift.

Although a heavily used feature of the language, argument labels are currently useful only in the context of 'original' functions and methods. As soon as a function or method is assigned to a value its argument labels are lost in this way.

This means that, as things stand, two of the marquee features of Swift, 'functions with argument labels', and 'functions-as-values', are mutually exclusive:

{functions with argument labels} āˆ© {functions as values} = āˆ…

From my own experience learning the language, this disjunction does not seem particularly intuitive. It even seems to be somewhat 'submerged', in the sense that it becomes apparent only after personally bumping up against it one way or another (it is noteworthy that examples of functions-as-values in 'The Swift Programming Language' take care to avoid argument labels, and so do not immediately surface this dichotomy to the reader).

Sketch

To address this situation, and perhaps facilitate some new opportunities, I wonder whether it would be possible for an identifier which is being assigned a function (of any sort) to gain the opt-in opportunity to re-bind the function's argument labels to itself.

In brief, I can imagine something along the following lines:

  • If not opted into, the current behaviour is retained, whereby argument labels are 'lost' to the new identifier. Functions which originally had argument labels are called from the new identifier without labels.
  • If opted into, the new identifier gains whatever labels are provided, as though it were a newly defined function.
  • The arity of the labels would match the arity of the new value.
  • The order of the labels would match the 'anonymous' argument list $0, $1, etc, of the new value.
  • The type of each argument would of course be invariant (and therefore can be implicitly inferred).
  • Re-bound labels would be allowed to be the same, or different, to the labels of the original function.
  • Labels would be able to be explicitly omitted using an underscore, just as they are in standard function declarations.
  • The normal disambiguating annotations for argument labels and types would allow overloaded functions/methods to be re-assigned in this way.
  • The labels would only subsequently be required in the same contexts where labels are required today. In other words, the function's type would not be affected by the labels, and the identifiers could be used in certain cases without their 'label part', just as 'original' function identifiers may in certain cases be used without their labels today. Of course, this would change if labels are ever required to always appear as part of an identifier, wherever and whenever it is used, as I think has been discussed a couple of times.

I envisage the syntax of such a feature to closely follow existing 'label-disambiguation' grammar, but in this case used on the left side of the assignment operator:

identifier(label:label:) = ...

This would present an interface consistent with existing language features, and is in keeping with previous suggestions along these lines (which are referenced below).

Sketch examples

Here are some examples, presented in the context of partial application of functions (using the proposed partial application syntax outlined in part 1 above):

Example 1: A partially applied function, with re-bound labels not opted into. The current label behaviour is retained (i.e. labels are 'lost'):

func share(thing: Thing, between a: Person, and b: Person) { ... }
let partialShare = share(thing: _, between: _, and: kate)
let finalShare = partialShare(sandwich, leslie)

Example 2: A partially applied function, with re-bound labels opted into. The value has new labels bound to each argument:

func share(thing: Thing, between a: Person, and b: Person) { ... }
let partialShare(thing:with:) = share(thing: _, between: _, and: kate)
let finalShare = partialShare(thing: sandwich, with: leslie)

Example 3: As above, but with one of the new labels explicitly omitted:

func share(thing: Thing, between a: Person, and b: Person) { ... }
let partialShare(_:with:) = share(thing: _, between: _, and: kate)
let finalShare = partialShare(sandwich, with: leslie)

Example 4: A simple collapsed method with re-bound labels:

struct Thing {
    func method(x: Int, y: Int) {...}
}
let collapsed(wide:high:) = Thing().method
collapsed(wide: 9, high: 81)

Example 5: A collapsed, label-disambiguated method, with re-bound labels:

struct Thing {
  func method(x: Int, y: Int) {...}
  func method(p: Int, q: Int) {...}
}
let collapsed(wide:high:) = Thing().method(p:q:)
collapsed(wide: 9, high: 81)

Example 6: A partially applied, type-disambiguated method, with re-bound labels:

struct Thing {
    func method(x: Int, y: Int) {...}
    func method(x: Int, y: Bool) {...}
}
let collapsed(withSparkles:): (Int, Bool) -> Void = Thing().method(x: 3, y: _)
collapsed(withSparkles: true)

Example 7: A partially configured instance value, with a re-bound label:

struct Polygon {
  let sides: Int
  let envelope: Int
  let filled: Bool
  init(sides: Int, envelope: Int, filled: Bool = false) {...}
}
let filledPentagon(envelope:) = Polygon.init(sides: 5, envelope: _, filled: true)
let bigPentagon = filledPentagon(envelope: 200)
let smallPentagon = filledPentagon(envelope: 3)

Considerations

At this stage there occur to me three principal considerations which warrant some further notes:

  1. Implementation
  2. Overriding identifiers with labels
  3. (Im)mutability
Implementation

I understand that there is groundwork needed to implement this kind of idea due to the way that function types and argument labels are currently modelled in swift.

Specifically, I understand that the work required to facilitate this component is outlined in this pitch from @DevAndArtist Adrian Meister, which is in turn a modified version of 'step 1' in this commentary document from Chris Lattner.

Further recent discussion along these lines also appears in this pitch from @Saklad5 Jeremy Saklad.

Please accept my apologies if too much material from these threads seems repeated in my discussion. I prepared this submission some time ago, and although I have endeavoured to update it in recent days, it reflects some long-standing thoughts.

In any case, assuming the capacity to incorporate argument labels into identifiers could be implemented, I imagine this may provide a mechanism for opting-in to argument labels whenever the result of any expression is a function type.

Example 8: A closure value with labels bound to its identifier:

let go(from:to:): (Place, Place) -> Void = { ... $0 ... $1 ... }

let go(from:to:) = { (Int, Int) -> Void in ... $0 ... $1 ... }

let go(from:to:): (Place, Place) -> Void = { x, y in ... x ... y ... }

let go(from:to:) = { (x:Int, y:Int) -> Void in ... x ... y ... }
Overriding identifiers with labels

I imagine that, just like current function names, label re-binding might accommodate the re-use of a root identifier in cases where the argument labels or the argument types differ between declarations:

Example 9: 'Overridden' root identifiers:

let go(from:to:) = ...
let go(hither:yon:) = ...

let go(from:to:): (Place, Place) -> Void = { ... $0 ... $1 ... }
let go(from:to:): (Town, Town) -> Void = { ... $0 ... $1 ... }

This would provide consistency with current function declaration behaviour, and disambiguating mechanisms could be employed when needed, just as they currently are for functions and methods.

(Im)mutability

All of my examples so far deliberately use immutable values. I am less certain of the implications for mutable values, but the basic rules I envisage for variables are these:

  • Argument labels may be re-bound at variable declaration, but not at variable re-assignment.
  • Argument labels may be employed for disambiguation purposes at the point of variable re-assignment.

This would unify the behaviour between constants and variables, in the sense that argument labels may only be incorporated into an identifier at declaration, and are subsequently 'locked' for that specific identifier/function binding.

Example 10: A stored mutable property, with re-bound labels:

func visit(_ x: Place, then: Place) { ... }

struct Action {
  var go(from:to:): (Place, Place) -> Void
}

// The labels from the var declaration permanently apply:
let instance = Action()
instance.go = visit                   // assign a function to the property
instance.go(from: here, to: there)    // call the function via the property

// ... And may not be overridden:
instance.go(start:finish:) = visit
// << Error: type 'Action' does not have a member 'go(start:finish:)'

// ... But disambiguation works as expected:
struct Action {
  var go(from:to:): (Place, Place) -> Void
  var go(hither:yon:): (Place, Place) -> Void
}
instance.go(hither:yon:) = visit

Example 11: A variable value, with re-bound labels:

func visit(_ x: Place, then: Place) { ... }
func go(from: Place, to: Place) { ... }

// The labels from the var declaration permanently apply:
var travel(here:there:) = visit       // re-bind labels at declaration
travel = go                           // re-assign a function to the variable
travel(here: here, there: there)      // call the function via the variable

// ... And may not be overridden:
travel(now:then:) = visit
// << Error: no such value 'travel(now:then:)'

// ... But a different variable with these labels may be declared:
var travel(now:then:) = visit

// ... And then disambiguation works as expected:
travel(now:then:) = go

Benefits

With a mechanism like this in place, two important features of Swift: use of argument labels and assignability of functions to values, would no longer be mutually exclusive.

This mechanism would permit identifiers to be more consistently named within a code base. Specifically, assigning a function to a value would not necessarily mean foregoing the use of labels, or abruptly switching from 'natural language-like' to 'sequence-like' argument lists in every case.

This mechanism would permit identifiers to be more appropriately named when they are shifted to new contexts, including the maintainence of naming guidelines where appropriate. As well as improving consistency and readability, this may encourage code re-use, since the mechanism would preclude any problem of mismatches between root names and argument labels in shifted contexts.

As depicted in the examples, being able to re-bind argument labels to value identifiers could make partial application of functions more generally useful. In other words, the two parts of this discussion, taken together, present a compound of their individual benefits.

Impact on existing code

Because of the opt-in nature of this suggestion, I understand that this feature would be purely additive to swift, and would not affect existing code.

Project management

Although I have a sincere interest in the development and maturation of Swift, I am not a computer scientist, engineer or professional programmer, and I am not familiar with the underlying mechanisms or implementation of compilers or other components of the language system.

For this reason, if either or both of these suggestions are deemed to have merit, any resulting projects would require the willingness of someone in a position to write implementation(s) as part of any formal proposal(s).

Thank you

Thank you for your interest and consideration.

6 Likes

Could you please update my name in your post, thanks. ;)

1 Like

Could you please update my name in your post, thanks. ;)

Hello Adrian, thank you for drawing my attention to this. I have edited my post to update your name.

Please accept my sincere apologies for the oversight.

No worries, I assume you started sketching your ideas quite a while ago, therefore my old last name.

Iā€™m not in a position to comment in-depth on your suggestions from a feasibility or holistic perspective. However, I wanted to voice my support of the objectives laid out and say that these are capabilities of the language that I would benefit from greatly. Having read and understood your post (to the extent my background and experience allows for), thank you for writing your thoughts down and for taking the time to organize them well.

[EDIT] I also happen to like the specific syntaxes proposed here quite a bit.

Without totally saying "it's a bad idea", I'll note that #1 has been discussed before, albeit not for several years!

I tend to agree with the conclusions there that the underscore-as-placeholder syntax doesn't provide sufficient benefit over a compact closure with anonymous arguments. I do however support the idea (in this thread and @DevAndArtist's) to allow names-with-labels for local and non-local bindings alike.

2 Likes

I would love the re-binding argument labels! I hate the fact that I lose the labels when assigning functions to variables.

3 Likes

My Suggestion for coding style

Partially Applied: using double underscore as the placeholder for more readability; keep one underscore meaning "ignored".

func sum(_ x: Int, _ y: Int, _ z: Int) -> Int {
  return x + y + z
}
sum(1, 2, 3)   // 6

// Partially Applied using double underscore
sum(__, 2, 3)(1)  // 6
sum(1, __, 3)(2)  // 6
sum(1, 2, __)(3)  // 6
sum(__, __, 3)(1, 2)  // 6
sum(__, 2, __)(1, 3)  // 6
sum(1, __, __)(2, 3)  // 6

[1, 2, 3]
  .map(sum(3, 5, __))  // [9, 10, 11]

Curried function declaration:

// From this
func sum(_ x: Int) -> (Int) -> (Int) -> Int {
  return { y in { z in x + y + z } }
}

// To this
func sum(_ x: Int) -> (_ y: Int) -> (_ z: Int) -> Int {
  return x + y + z
}

// Usage
sum(1)(2)(3)  // 6

[1, 2, 3]
  .map(sum(3)(5))  // [9, 10, 11]

Curried closure: like JavaScript ES6

// From this (current Swift)
let sum = {(x: Int) in {(y: Int) in {(z: Int) in x + y + z}}}

// To this
let sum = (x: Int) -> (y: Int) -> (z: Int) -> x + y + z
// Or this: 
typealias SumUp = Int -> Int -> Int -> Int
let sum: SumUp = x -> y -> z -> x + y + z

// Usage
sum(1)(2)(3)  // 6

[1, 2, 3]
  .map(sum(3)(5))  // [9, 10, 11]
Terms of Service

Privacy Policy

Cookie Policy