Add function application to Swift's standard library

Apologies for this bad naming. I was influenced by the .method() syntax. But .{ ... } does not define a method, because self is not redefined in this regular closure that accepts a single argument. Now there again I can foresee people feeling nervous about reusing the dot for invoking something which is not a method.

Personally, I see it as a method without a name. The OPs example of an 'apply' method on Collection can be rewritten to:

  • Be an extension on all types
  • Use no method name instead of 'apply'

This is not possible in Swift, and we probably don't want to make this possible in general, but if you look at it this way, it's equivalent to the following:

// Pseudo Swift
extension<T> {
    func _<U>(_ transform: (Self) -> U) -> U {
        transform(self)
    }
}

This works for the base syntax: value.(function), as well as the value.{doStuff} variant, which just uses trailing closure syntax. Another valid way to write this would be value.() {doStuff}.

2 Likes

This is an interesting framing: this _(_:) method makes value.(function) natural, instead of a new use of parenthesis as I said above. Thanks for making things click well together :-)

Now, should _(_:) be exposed and made usable by Swift users (let apply = value._) ?

If yes, why so? And doesn't it need a proper name, then?

If not, it's just an abstract method that does not need to really exist. It's just a "mental trick" that makes the sugared syntax "work" in the regular Swift way. That's quite valuable as well.

I'd love to see a solid pitch for it. I'm just trying to do some early QA, and I hope my questions do not sound aggressive. I just hope that exploring and exposing possible misinterpretations will help debunking them swiftly.

And |> is still an interesting alternative :-)

Right, a dot for something that's not a member is overloading the meaning of the dot; that's actually a major change instead of a natural extension of existing syntax.

Additionally, this syntax would be visually confusable with trailing closure syntax. Consider a method with a defaultable predicate frobnicate(something, { somehow }):

foo.frobnicate(bar) { print($0) }.toggle()
foo.frobnicate(bar).{ print($0) }.toggle()

I think, overall, readability and precedent greatly favor |> over any shorter or more novel alternative unless it meaningfully enables additional features. If chainability is important, |> could be made a built-in operator with custom precedence equivalent to ..

1 Like

I do think this is at least one of the major selling points. That's why I think nice support for optional chaining will be important in the eventual pitch, and why I was interested in supporting @orobio's exploration before we all jump on the |> wagon.

Many examples in this thread apply an initializer. Swift favors initializers for type coercion/conversion, this is a known fact. But those don't always fit well in chains:

let x = Set(value
    .lazy
    .compactMap { ... }
    ...
    .prefix(2))
    .union(otherSet)

vs.

let x = value
    .lazy
    .compactMap { ... }
    ...
    .prefix(2)
    .(Set.init)
    .union(otherSet)
//  ^ note the lovely alignment of chaining dots here

or

let x = value
    .lazy
    .compactMap { ... }
    ...
    .prefix(2)
    |> Set.init
    .union(otherSet)
//  ^ there is a visual outsider, for no good reason
5 Likes

How is this an advantage when we can also include ?> with the operator approach? The main advantage appears to be the ability to chain after the function without having to parenthesize the whole expression. In practice though, Haven't needed this.

One advantage of the operator approach is that it is able to include overloads that support partial application, which I think would be awkward with the chained approach.

I want to point out one little-known aspect of Swift's type system that is relevant here: functions accepting multiple arguments can be used in type contexts requiring a tuple-accepting function, so long as the number and type of arguments match up. This means that a single generic |> goes a long way, and easily supports multi-argument functions even without variadic generics:

public func |> <A, B>(
    lhs: A,
    rhs: (A) throws -> B
) rethrows -> B {
    return try rhs(lhs)
}

func takesTuple(ints: (Int, Int)) -> Int {
    ints.0
}
func takesTwo(first: Int, second: Int) -> Int {
    second
}

let x = (42, 43) |> takesTuple
print(x) // 42

let y = (42, 43) |> takesTwo
print(y) // 43
2 Likes

Yes, but @orobio's application method in Add function application to Swift's standard library - #23 by orobio can be extended with multiple arguments as well. Those solutions are equivalent, unless I'm mistaken. The difference is only syntactic (with a little compiler sugar for .(function)).

I find it elegant, and a nice way to "digest" a well-established functional feature and operator into Swift (plus no special case for optional chaining).

It’s worse than this:

You need parentheses around the expression to chain after the operator

let x = (value
    .lazy
    .compactMap { ... }
    ...
    .prefix(2)
    |> Set.init)
    .union(otherSet)
//  ^ there is a visual outsider, for no good reason

This is clunky with one use of the operator. It gets worse quickly. It is a significant downside of the operator approach. Note: this is not unique to |> and is the case with all operators.

1 Like

Thanks for fixing my sample code :-) Now, honestly, this is not that bad: the sequential nature of the chain is respected (everything happens in the same order as written - and this is all function application is about - and this is why I don't have anything against |>).

Given that the chaining approach would be baked into the compiler I’m sure we could make it similarly flexible for fully applied functions. Where it looks less capable is in support for partial application. With |> it’s possible to include overloads for partial application. I suppose that could be made to work with the chain syntax but I am skeptical it would be a good idea.

Not at all, it's great to have this conversation and sort out the basic details. If the idea is not viable at all, I'd rather find out now.

Good question. If we need to make this available, it should probably be spelled differently. I don't have a good answer though for what it should be.

I wonder if this will be a problem in practice. Personally, I think the dot provides a quite clear separation in this example.

I don't have much experience with this. For my understanding, could you provide an example of how the |> operator would be used with partial application?

I disagree with this philosophy. Rather, similar things should look similar, and different things should look different.

To me, it is not lovely (and, in fact, as I pointed out in an earlier reply, potentially confusable both at the point of use, and for teaching the semantics of the operator .) to have two different things look the same. There is a good reason for there to be a visual distinction; namely, that there is a semantic distinction.

2 Likes

Ah but you could argue that it IS the same. In the end, what is the practical difference between these?

myOptional.map(Set.init)
myNonOptional.apply(Set.init)
4 Likes

What is the "it" that you're talking about? We are not discussing the spelling apply.

You said

and I contended that at least from a practical point of view, the proposed chainable function (whether it's spelled . or .apply) is very similar to .map on an optional, so it makes sense that it looks the same.

1 Like

AFAICS they are not the same, because if you had apply on an optional, you could transform that optional to eg a non-optional String. Optional's map transforms the wrapped value (if it has one) and always "puts it back" in an optional, while apply transforms the whole thing to another thing.

You can argue that one function is similar to another, and therefore one function should be spelled similarly to another. However, map and apply are methods, but . is no such thing, so you cannot elide the difference into a parenthetical and say that they "look the same." It'd be like claiming that . and |> look the same, or that + and String.init look the same, or that C# and Haskell look the same. This is missing the entire point of the exercise.

Yes, in a category theory sense they are different. That's why I added "practical"... I mean, in a more naive sense, in one case you have (say) [T]? and the map converts that to Set<T>?, in the other case it's [T] goes to Set<T>. It's both applying a function and getting a value. Most of the chained methods in the example, like prefix and even compactMap are not like map either, since they either have nothing to do with monads at all, or almost violate them.

No idea why you are so hostile, but are you saying that you are only talking about the . option when you say they are different and should look different? You agree with me that if it is spelled .apply then it would be fine because .apply and .map are in fact similar enough? If this is not your position, I'm afraid I stil don't understand it.