Add function application to Swift's standard library

Here's the simplest overload for partial application:

func |> <A, B, C>(
    lhs: A,
    rhs: @escaping ((A, B)) -> C
) -> (B) -> C {
    return { rhs((lhs, $0)) }
}

// usage:
func sum(lhs: Int, rhs: Int) -> Int { lhs + rhs }
let plus10 = 10 |> sum
let fifteen = 5 |> plus10

With three argument functions two overloads are necessary:

func |> <A, B, C, D>(
    lhs: A,
    rhs: @escaping ((A, B, C)) -> D
) -> (B, C) -> D {
    return { rhs((lhs, $0, $1)) }
}
func |> <A, B, C, D>(
    lhs: (A, B),
    rhs: @escaping ((A, B, C)) -> D
) -> (C) -> D {
    return { rhs((lhs.0, lhs.1, $0)) }
}

As the number of arguments grows, the number of overloads currently required for comprehensive partial application support grows as well. My team currently supports functions with up to four arguments. With variadic generics, a single overload could handle arbitrary numbers of bound and unbound arguments.

2 Likes

We don't need two forms, only the variant that accepts an optional-returning function: (T) -> U : (T) -> U? in Swift's type system. Covariant return is one of the special subtype relationships supported by Swift's function types.

I don't think we need "a bunch". My team is using |>, ?> and &>. The latter is sometimes called with, but I think it fits very nicely into this operator family:

public func &> <A>(
    lhs: A,
    rhs: (inout A) -> Void
) -> A {
    var lhs = lhs
    rhs(&lhs)
    return lhs
}

We also use >>> for sequential composition, but that is a separate topic from the family of function application operators.

1 Like

I didn't think about &>, that's very interesting :smiley:

I'm not sure I'd like the same ?> for what's essentially both map and flatMap: I like the visual distinction between the two when reading code.

For the flatMap case I'd use >>=, simply because it's already been used in other languages, and could be overloaded also for other monadic types.

About the map, I'm not aware of the existence of a map operator with the shape A? ยงยง (A) -> B... maybe >> could be ok.

This is how optional chaining works. Swift is its own language with its own concerns. I think a single ?> is the right choice.

That is not completely true. Optional chaining signals that a member is returning an optional with ?, and doesn't require it on other member calls.

In a chain like the following:

let x = foo
  .bar?
  .baz
  .fiz

I know that foo and baz don't return optionals, while bar does (it doesn't tell me anything about .fiz, unfortunately). With operators:

let x = foo
  |> bar
  ?> baz
  ?> fiz

Other that moving ? on the next line (only relevant with one-statement-per-line chains), I now need to use ?> also after baz, and in general after every single statement in the chain, making this look like how Kotlin handles nullable chains (with a ? after each statement after the first).

Of course, but wanting to know, by reading code, that a function returns an optional or not is a very general concern, not related to the language: other languages could overload operators to make this loose, but they don't and for valid reasons.

In an optional chain, if I see >>= or >> (assuming that we go with these for, respectively, flatMap and map) I 100% know that only functions passed to the first operator can make the chain "fail". Also, these would be the operator version of methods already present in the standard library, so would mirror a pattern that's already used today in Swift.

As an aside, there is a 0% chance that we would overload >>= and >>, because these are existing operators; any new overload would explode type-checking time, not to mention that it is not advisable to overload the same operator with two semantics (Swift's operator + being the only current exception and an unfortunate one at that).

1 Like

I agree with the aside. And I really agree with the fact that the + overloads are unfortunate, for a variety of reasons.

I agree as well. + should not be used as a concatenation operator.

2 Likes

I'm +1 on this proposal as a whole, but strongly against using <value>.(<someFunction>) as the operator to do so. It's a different use of the . operator that seems like it would be extremely confusing to newcomers and experienced programmers alike. Additionally, it seems to me like it would be confused with calling closures:

func f(_ x: Int) -> Int {
    return x + 1
}

let val = 5
val.advanced(by: 4).(f) // function application; returns 10

func g(_ y: (Int) -> Int) -> Int {
    return y(1)
}

f.(5) // calls `f` with x=5
f.(g) // function application; returns 2

While this is admittedly a contrived example, having two completely different behaviors depending on the type given seems absurd to me (I'm aware that giving two different types to a function could have different effects due to overloading, but this is two different language-level behaviors).


I like both a built-in method (such as .apply(), or perhaps .passTo()/.pass(to:)), and the |> operator, although if use of the operator would require more parentheses I'd lean towards a method.

2 Likes

The operator will never require more parentheses. A method always requires parentheses. An operator only requires parentheses for precedence, or to chain on the result directly (i.e. without using another operator). The downside of parentheses with an operator is that the opening paren must be at the beginning of the expression, which can be awkward.

My sense is that the . has a firmly established meaning that the thing following it is a member of the thing preceding it. I would be quite hesitant to loosen that meaning.

3 Likes

Thanks for the example, I understand what you mean now. If we would support partial application with ., I think it naturally follows that it would look like this:

func sum(lhs: Int, rhs: Int) -> Int { lhs + rhs }
let plus10 = 10.(sum)
let fifteen = 5.(plus10)

// Or:
let fifteen = 5.(10.(sum))

I have to say it reads very well (once you start seeing the dot as performing function application instead of a member call).

This is not correct. With dot is function application, without dot is a function call:

f(5)  // calls `f` with x=5
f.(g) // function application; returns 2

Edit: Your example would be an error, because 5 is not a function that can be applied to f.

I understand the concern. On the other hand, the parentheses are a very clear indication that it's not a member that's following. I think it's hard to mistake one for the other.

Other than the notion that a . must be followed by a member it all seems to fit very naturally in the language, so maybe it's just something to get used to?

For better or worse, parentheses have a long and complicated history in Swift. AFAICR, at this point, parentheses semantically either provide grouping or have no meaning. By grouping, I refer to grouping of parameters/arguments in functions, methods, closures and initializers, including the tuple initializer, and grouping of expressions.

Where parentheses have meaning, I believe a user reads them as saying, "I'm gathering these things together to perform an operation that uses these things together." In the context of your proposed usage, what do you read the parentheses as saying? And, what do you need to already know about the thing between the parentheses to understand that the parentheses are not merely the vestigial wrapping of something that might be a member of the value that comes prior to the period?

Or you could use a closure:

let x = [2, 3, nil, 5]
    .lazy
    .compactMap { $0 }
    .prefix(2)
    |> Set.init
    |> { $0.union([100]) }
    |> { $0.subtracting([2]) }
    |> \.description

or

let x = [2, 3, nil, 5]
    .lazy
    .compactMap { $0 }
    .prefix(2)
    |> Set.init
    |> { $0
        .union([100])
        .subtracting([2])
        .description
    }

Not as nice as a compiler-supported solution could be, but quite usable today.

2 Likes

I was unclear. I don't mean that the operator would require more parenthesis than the method; I meant that the operator would require additional parenthesis in order to continue chaining (I'm agreeing with your concern stated before):

As of Swift 5.2.4 on my Mac, my example:

does what I say it does (provided the functions are defined as I did in my original post).

I understand that under the proposed use of . to signal function application my example would have two potential meanings (function application and calling f); that was the point of the example.

I suspect the vast majority of times that functions/closures are called, they are called as f(5). However, it was (and currently, still is) legal to call some subset of those (I'm not sure which of them at the moment) as f.(5). It therefore appears to me that using . to signal function application is a source-breaking change, in addition to (presumably) still conflicting with members for these types.


EDIT: Wow, I was completely wrong. Apparently Xcode auto-completes this way, but I only dreamt that I had tested it. My sincere apologies

1 Like

As @xwu said earlier, you have a circular argument. You're claiming that "it all seems to fit very naturally in the language" by ignoring the fundamental notion that makes it clearly not fit in the language.

If, instead, we keep the idea that "a . must be followed by a member" โ€” something that has been part of the language since its inception โ€” we see that attempting to redefine . to perform function application doesn't fit at all.

Good question. The idea started as a 'nameless method' with the same signature as the proposed 'apply' method.

value.apply(function)
// Becomes
value.(function)

From that perspective it's easy to answer your question. The parentheses are a function call, calling a method without a name.

However, the more I think about it, I like the other perspective better, seeing it as being able to use any function after .. In that case I read the parentheses as saying: This is an expression. As opposed to an identifier that must be a member.

value.member       // Identifier that must exist in the type's namespace.
value.(expression) // Can be anything (with the correct type).

Your question was specifically about the parentheses. Do you find the closure expression variant, value.{...}, more acceptable?

Would using a different marker to indicate that we're not specifying a member make a difference? For example:

values
  .map {...}
  .::Set.init

I have shown how it does fit the language, so I can say the same but the other way around. It all depends on which part you assign more weight to, the part that fits or the part that doesn't fit. That doesn't make it a circular argument. We just have different perspectives.

As I've shown, we don't have to redefine ., because it already performs function application. That's why this fits so nicely (in my perspective).

As I said earlier, I do understand the concerns. I'm just wondering (honestly don't know) if they are valid, or that perhaps in practice it will not be a big problem. Note that the meaning of value.identifier will not change at all.

Giving |> the same precedence as .

It was suggested that with compiler support |> could be given the same precedence as ., but I don't think that's a solution either. Then you run into different problems:

foo.f() |> bar.g()  // Will not do what you'd expect

This will be a problem with any solution that involves an operator, so I feel that if we want to make chaining into free functions work nicely with the existing chaining syntax, we need to base it on the dot syntax. Be it some special syntax to be able to chain into free functions, or by adding an 'apply' method to every type.

I also think that this will not fix everything. It creates a nice bridge between chaining member functions and free functions, but there's probably still room for operators like |> and <|, etc.

2 Likes