Add function application to Swift's standard library

Sorry, not trying to be hostile! Yes, and yes. We can't extend Any, though.

OK got it. We can't? That's a shame, I do like the operator approach and would really have preferred all these methods (prefix etc) to be free, curried functions so we could just use operators for everything, but as things are there is the big downside that using an operator stops the chain. Well unless you add parentheses.

It's a tangent but I think this quirk show the fundamental problem with method chaining, that it's really something of a hack. And sure most of the time it works fine, but the tension is always there.

1 Like

Not if it's built-in with the same precedence as .โ€”and if we agree that this worth adding to the standard library, it's eligible for compiler-level support.

1 Like

Is it my imagination or did they change the precedence of . a few years ago? I feel I have to add parentheses more often these days when dealing with optional chains. But as practical as it is, wouldn't it be weird if some other operator had higher precedence than .?

If you see it as a method without a name, with some imagination they do look the same:

  • value.apply(someFunction)
  • value.(someFunction)

But perhaps this is not the right way to look at it.

Another way to look at it

struct Foo {
    func bar(_: Int) -> String { "" }
}
let fooValue = Foo()

type(of: Foo.bar)          // (Foo) -> (Int) -> String
type(of: fooValue.bar)     // (Int) -> String
type(of: fooValue.bar(0))) // String

A method is basically syntactic sugar for (Self) -> (Method arguments) -> (Method return type). So, in value.method, what . does is applying value to method, resulting in a function that can be called with (Method arguments).

If we see value.(someFunction) as special syntax, we can say the following:

  • The parentheses say that we are not chaining into a method, but into a free function.
  • The signature for the free function is (Type of value) -> (T).
  • The . operation still does the same as it has always done. It applies value to what comes after it.

The same applies to the closure variant: value.{someFunction($0)}.

Edit: Changed to arbitrary function instead of Set.init and added emphasis.

Edit 2: I guess I should've said that the . operation applies the function that comes after it to the value that comes before it, instead of the other way around. Nevertheless, I hope the point is clear.

3 Likes

I havenโ€˜t followed the entire conversation, but will try to catch up soon-ish. One thing that is important in the current conversation: Any is not really a type that we want to extend, as itโ€˜s like an existential box that can hold any value.

Feel free to correct me here if itโ€˜s not correct.

The more appropriate extension could made available through parameterized extensions.

extension<T> T {
  func apply<R>(...) -> R
}

Obviously, we would need some other type of generalization to allow such extensions to exist, but it would allow us to provide methods and other members to all sort of (un-)constrained types. We may be able to move all KeyPath subscripts to such extensions.

1 Like

Stupid question, but why not writing it like this:

let includedProductIdentifiers = Set(puzzleLibrary.volumes.compactMap { $0.productIdentifier })

What apply() brings that's different?

A Reactive library may offer such capability in its tool box, because that's something that could be needed as part of a transformation pipeline, but in plain Swift I don't really see the value.

With apply the flow goes from left to the right

from puzzle library, we take volumes, and from volumes we take the identifier. Then we remove duplicates. End.

Without apply we either have to keep a virtual stack in our mind, or jump around in the text

At the end we will remove duplicates. From puzzle library (remember to remove duplicates at the end) we take volumes (remember to remove duplicates at the end), and from volumes we take the identifier and remove duplicates. End.

From puzzle library we take volumes, and from volumes we take the identifier. Next... oh wait, there's a closing bracket. Where did it open? Oh, we remove duplicates. Now we jump back to the closing bracket to check if there's anything else afterwards. End.

or in visual form:

let includedProductIdentifiers = puzzleLibrary.volumes.compactMap { volume in volume.productIdentifier }.apply(Set.init)
//                               ------------->------------------------------------->------------------------->-------->

let includedProductIdentifiers = Set(puzzleLibrary.volumes.compactMap { $0.productIdentifier })
//                              |^   ------------->----------------------->------------------.^
//                              ||                                                           ||
//                              | `----------------------------------------------------------'|
//                              |                                                             |
//                               `------------------------------------------------------------'
6 Likes

I often encounter repeated use of map to transform elements of a sequence (numbers.map { $0 + 1 }.map(String.init).map { $0.appending(" suffix") }โ€ฆ), which, afaik, can have some significant downsides.
A part of the explanation might be that map is "trendy", but I guess the transformation is easier to understand when the steps are written in the order as they are executed.

Having an operator that allows chaining on a per element basis might save some developers from iterating over all elements several times.

You can see, then, how you've just demonstrated why the operation we're talking about isn't consistent with the meaning of .

In attempting to explain your mental model, the re-use of an unrelated syntax has caused you unintentionally to reverse the receiver and argument of the operation. And this for an operation whose basic purpose is to reverse them in the first place!

This is the point I'm making: you're proposing to make two different things have the same syntax. It is confusing to learn, and clearly as you've shown here, confusing to teach.

I don't think that's correct. I mixed up some terminology, because I'm not well-versed in language theories, but I still think it's very consistent.

Let's say we have the following:

struct Foo {
    var x: Int
    func bar(_ y: Int) -> Int {
        self.x * y
    }
}

func freeBar(_ foo: Foo) -> (Int) -> Int {
    { y in foo.x * y }
}

type(of: Foo.bar)) // (Foo) -> (Int) -> Int
type(of: freeBar)) // (Foo) -> (Int) -> Int

The dot syntax is as follows:

value.<function>

Where <function> is a function of type (Value) -> (T), which is applied to value.

If we accept that <function> can be:

  • A method (already possible today)
  • A free function (disambiguated with parentheses)
  • Or a closure expression

Then we can say that the following are all equivalent:

let fooValue = Foo(x: 21)

fooValue.bar(2)           // 42

fooValue.(Foo.bar)(2)     // 42
fooValue.{Foo.bar($0)}(2) // 42

fooValue.(freeBar)(2)     // 42
fooValue.{freeBar($0)}(2) // 42

Now, the only difference between array.{Set($0)} and the examples above is that Set($0) directly returns the value we want, while the examples above return a function, which is further applied to the value 2.

In short, we already have function application: the dot syntax. We just have to extend it to work with free functions and closure expressions.

Edit: Use <function> as placeholder

2 Likes

I mean, I do not accept that. What comes after . is a member. Yours is a circular argument: if we erase the current meaning of . and instead accept that it can encompass anything, then everything is equivalent; if we erase the distinction between different things, then they are no longer different.

If you find it easy to parse the meaning of fooValue.{freeBar($0)}(2), that's wonderful for you; but I cannot, and I do not think that people will agree that enabling such a spelling for Swift is a good result.

3 Likes

Yes, currently only members are allowed. If you're saying that accepting any function after . is inconsistent with that interpretation, then yes, it's different. However, as I've shown, it's very consistent with the language today when it comes to the operation that . performs.

I'm not advocating for writing code like this, but was merely demonstrating how consistent this interpretation of the feature is within the language. It does enable writing clean code like:

let foo = [1,2,3]
    .map(String.init)
    .(Set.init)

And some people expressed interest in being able to do this.

Now, do I think this is the way to go? I don't know. I like the consistency it brings with being able to chain with the dot syntax all the way to the end. I see that as an advantage compared to the |> operator.

If (as @DevAndArtist also mentioned) in the future we will get the possibility to write extensions for any type, like:

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

Then that's a very interesting direction as well. I think I once read somewhere that extensions for unconstrained generic types will probably not happen, so I actually did not consider this direction.

5 Likes

Let's consider a couple of assumptions:

  1. we don't want the possibility of extending every type with a new member;
  2. we prefer using methods instead of operators;

I'm personally neutral on both of these, but I've seem mostly these sentiments over the years within the Swift community.

Anyway, by those assumptions there is no way to add a new .apply (whatever the name, I'd probably call it .to) member to every type without compiler support.

Let's try and lose the second assumption. The |> operator is widely used and recognized as the "standard" operator for function application when the function is on the right hand side: languages like F# and Elixir basically consider this operator as their ..

Can an operator work? Not by itself, because the "member call" operator . has precedence over everything else, so |> cannot be chained with .:

let x = foo
  .bar
  |> baz
  .fiz /// this is called on `baz`, not on the whole previous chain

Incidentally, this is a problem in general in Swift (for example, using ?? in chains is cumbersome). So, |> wouldn't solve the "chainability" problem without compiler support. We would also need a ?> operator for optionals, probably in 2 forms (also with functions that return an optional) to avoid nested optionals.

Two options, both require compiler support: I'm not sure which would be harder to implement, but assuming that the impact is comparable, I'd go with .apply, that also has no problems with optional chaining.

The "anonymous member" .(f)/.{f($0)} seems a nice idea to me, but has a few more problems compared to the previous solutions:

  • it still requires compiler support, and probably more than the others;
  • it looks overkill if compared to just add an .apply member to everything;
  • could produce code that's harder to read.

Is there an option that doesn't require compiler support? Not from a Jedi.

Another thing I observed in the Swift community (on average) is the preference towards adding members to types via explicit protocol conformance, in order to avoid "polluting" a namespace (I don't particularly endorse this).

For example, a type can be extended with a Convertible protocol:

protocol Convertible {}

extension Convertible {
  func apply<A>(_ f: (Self) -> A) -> A {
    f(self)
  }
}

This can be done right now, but requires declaring explicit conformance for any type involved: a solution like this can be good if the community agrees on the fact that always declaring the explicit conformance is the way to go.

Another thing that can be done right now is just add |> to the standard library, but together with a bunch of other operators that make sense. If we only used operators in a chain, instead of methods, everything would be simpler, but |> is not enough, as shown by the need for something like ?>. We could take inspiration from Haskell operators, or Elixir's witchcraft and add at least the essential operators that represent operations like the following (I'm using ยงยง as placeholder operator):

A ยงยง (A) -> B /// function application, usually |>
A? ยงยง (A) -> B /// map
A? ยงยง ((A) -> B)? /// applicative
A? ยงยง (A) -> B? /// flatMap, in haskell is >>=

Once we have these in the standard library (also defined for Result, at least) we could use them in functions defined for other types.

4 Likes

This has been mentioned several times, but what would these do? Isn't that just map and flatMap?

1 Like

Personally, I read the type-first version like this:

A set of identifiers extracted from the volumes of the puzzle library.

Also if the main rationale of adding an apply() is readability, we should ensure the improvement of readability is shared by many. I would argue that's not the case :). What I dislike with the apply() version is that you've to go at the end of the expression to understand the type. On the other hand, with the type-first version, you know from the start what you're dealing with.

1 Like

But with these method chains you always have to go to the end, so it's in keeping with that, which is the context where it would be used.

3 Likes

You can call map and flatMap only on an Optional: in a generic chain, this works only if the last call returns itself an optional.

let x = foo
  .bar? /// does return an optional
  .baz ///  doesn't return an optional
  .map { ... } /// compilation error: you can't call map here
1 Like

OK sure, that's true. But this is also ties back to the main drawback of the operator approach, it leads to the same issue. Or rather, the drawback is in mixing approaches. Personally I would prefer to only use operators, which would be pretty convenient if Type.instanceMethod had type (Type) -> T rather than (Type) -> () -> T. Then we could:

foo
|> FooType.bar
|> BarType.baz
||> ....

where ||> would be map. But in a way, couldn't we use .map to mean .apply? And/or use the same operator for both? I mean the usage would always be complementary, as long as the method name isn't overloaded. It's been 20 years since I took category theory and that wasn't in a compsci context, but couldn't you argue that there is a trivial monad where TrivialMonad<T> is simply T? Then you could with good conscience create a version of map that is just apply.

You could wrap everything in the "identity" functor and use map there: as you say, it would work as .apply.

I don't have a solution, and I also don't like the mixed approach. I'd love to see a push for operators in Swift like it was in the good ol' Swift 1 era, but it seems a losing battle right now.

EDIT: to better qualify, there's plenty of extremely useful operators that could find a good place in the Swift standard library. Off the top of my head: left and right function application, left and right function composition, same-type (magma) composition, et cetera.

1 Like