I understand. I'm not sure if I managed to get across the reason I do think it fits nicely, so I'll have one more attempt to hopefully make my perspective more clear:
let foo = Foo()
// If we say these are both valid:
foo.identifier // identifier is looked up in Foo's namespace (valid today)
foo.(expression) // Expression can be anything of type (Foo) -> T (new syntax)
// Then, ---without any other changes to the language---
// the following two lines produce the exact same result:
foo.bar
foo.(Foo.bar)
I'd like to see this in Swift sooner rather than later, so let's keep hammerin'.
Continuing from my previous post, it seem that an operator-based solution is simply not going to work for chains that flow naturally. Let's explore operators further.
Even for a less "odd looking" operator like .. the precedence rules, that favor member calling, will make the operator work differently:
let x = value
.lazy
.compactMap { ... }
.prefix(2)
.. Set.init
.union(otherSet)
in the example above, .union(otherSet) will be considered a member call on Set.init. Even if we assume that we can give that operator the same precedence and associativity of ., it would still look bad for single line code (let's return to |>):
let x = value.lazy.compactMap { ... }.prefix(2) |> Set.init.union(otherSet)
The whitespace around |> makes the code hard to understand because, again, Set.init.union(otherSet) seems a separately evaluated expression. So for single line code the compiler should force losing the whitespaces, which would be inconsistent with the multiline example. A lot of work, for a rather ugly solution.
The root problem here is that, to put it simply, flowing member calls and operators don't work together in Swift, if any operator usage is not at the end of the chain.
An option could be to just only use operators for chains like these:
let x = value
|> { $0.lazy }
|> { $0.compactMap { ... } }
|> { $0.prefix(2) }
|> Set.init
|> { $0.union(otherSet) }
This is ugly, and a lot of characters. Interestingly, thanks to the fact that we can use keypaths where functions are needed, we can actually express |> { $0.lazy } with |> \.lazy. This is definitely better, but keypaths don't work for other members. If they did, we could write somethings like this:
let x = value
|> \.lazy
|> \.compactMap { ... }
|> \.prefix(2)
|> Set.init
|> \.union(otherSet)
This is pretty ok, and I don't see any particular reason to not be able to write something like \.compactMap { ... }. Without this kind of keypath support, we might think about using method references, like:
let x = value
|> \.lazy
|> Array.compactMap { ... }
|> Array.prefix(2)
|> Set.init
|> Set.union(otherSet)
but this won't work, for many reasons. First, method references in Swift have inverted argument order:
let x = Array<Int>.prefix(_:)
/// x type is `(Array<Int>) -> (Int) -> Array<Int>.SubSequence`
/// x type should be `(Int) -> (Array<Int>) -> Array<Int>.SubSequence`
This is unfortunate. If the argument order was the right one, method references could be a lot more useful in general. But even if it was, that code would still be completely broken (even with some kind of overload on |> that accepts method references) simply because the Swift standard library makes extreme usage of specific types as return types of many kinds of functions. For example:
let x = value
|> \.lazy
|> Array.compactMap { ... }
Array.compactMap should actually be LazySequence<Array<Element>> where I should actually put the Element type. All this for saying that using method references for this use case in Swift is a terrible idea.
Thus, in my opinion the only way for an operator like |> to work for arbitrary chains is if we had keypaths on all members.
Yes this is a great shame, if the unapplied methods worked like you'd expect it would be very easy and clean to work with chained operator calls! For methods that don't take any arguments (in their instance form) you can probably make a special version of the operator, but you'd really want all cases to work.
It might be the case that you're constantly misunderstanding my posts.
My current opinion, based on my research and experience, is that the best possible solution here is to add to Swift an implicit .apply member (possibly with a different name, I'd prefer .to) with compiler support.
But I'm exploring the solution space, in particular the usage of operators, and my current conclusion is that operators are not the right solution because they don't mix well with instance members. If we had keypaths for all members (or if we intended to get them soon), than I'd consider something like
let x = value
|> \.lazy
|> \.compactMap { ... }
|> \.prefix(2)
|> Set.init
|> \.union(otherSet)
good enough to not warrant urgency for native reverse function application. In this case, adding |> to the standard library would be a small, easily implementable, incremental solution, that would make sense in itself and wouldn't get in the way of future improvements. But right now I'm not advocating for it, given the current language state.
Unfortunately that's not the case right now, given that Swift methods return a lot of very specific nested types. But if we could use keypaths. the problem would go away, and we'd be able to leverage type inference.
That might be partially true. I do think I interpreted correctly that we came to the same conclusion: 'apply and operators solve different things', but after your last post I was indeed not sure which direction you were advocating for.
Clear, thanks!
My reply wasn't necessarily disagreeing with you though. I was just trying to make the conclusion that:
Adding apply or a similar feature is worth it to get clean chaining syntax.
If we all can agree that operators are orthogonal to this feature, then we can probably drop them from this discussion (and perhaps explore them further in another thread).
Agreed. With the following remarks:
I'd prefer .into
If implicit members are a no-go, then I think .(into: ...) is a good alternative
This is something that has been talked about. Instead of narrowing the scope of consideration in this thread we should try to identify the best long term solution. If we need to enhance key paths to get there that does not seem problematic to me. I’d like to see them enhanced regardless.
I think |> is that solution and really don’t like the implicit member approach at all.
IMO, this thread is way too focused on chainability and does not spend enough time considering other reasons operators can be preferable. Operators often lighten syntax by remove parentheses and allowing word-based names in the expression to focus on terms. This can enhance clarity and readability. Consider: foo.apply(compose(aFunc, otherFunc))
vs foo |> aFunc >>> otherFunc
When one understands the meaning of a couple of basic functional operators the latter reads much more clearly than the former.
There is a reason we can’t extend Any. I understand that this is not what is proposed with apply, but I think many of the reasons we don’t allow users to define members that appear on all types still apply to apply. I don’t want an apply member available on all my types. No argument around chainability is going to change my opinion about this. I’ve been using |> for some time and in practice chainability has not been an issue.
Further, if we introduce apply where do we draw the line? Certainly there are other members people will come up with that they might wish to have available on all types. It’s a very slippery slope and I think we’re better off avoiding it altogether.
Let’s try to make |> work as best as it can in Swift even if that means working on complementary proposals along with this one.
I 100% agree with this. I'd love the addition of (rather standard) composition operators in the Swift standard library, and I use them myself. In these days I tend to use them less than in the past because some time ago I noticed extremely slow compilation times for long chains based on operators: not sure if things have improved in that area, but I was forced to gradually search for different solutions.
As I stated a few posts earlier:
But I've seen resistance in other threads in this forum when someone (including me) started to talk about the big picture instead of the very specific case proposed by the OP, so I'm trying to be focused on the possibility of an apply member, and how operators (that are not in the standard library, so they'd be an addition too) could work, and I find that the current state of Swift doesn't allow for easy and ergonomic mixing of operators with instance members.
Notice that operators as a means to compose and apply functions are not the only option here: if we could write instance members on functions we could achieve the same composability without symbols that many find unsavory. For example, in Kotlin I can actually add methods to functions, like .compose(...). An intellectual burden that operators might impose is related to associativity and precedence, that don't exist for instance members, in the sense that there's only one associativity, and precedence is only explicitly stated with parentheses.
But again, I'm all for operators, but even the Pointfree guys had to shy away from operators after the first episodes, and released overture, probably due to the fact that the initial feedback was negative, and many companies and teams don't see operators well, I'm sure for unrelated "historical" reasons that have nothing to do with the composition operators themselves.
Another problem with talking about the big picture is that it becomes harder to come up with relevant examples, because each example can (and will by someone) be rewritten in terms of a simpler case that would require less work with a specific solution. Encouraging operators usage at the standard library level is a major undertaking and, while I'd be all for it, I've seen resistance in the past. More guidance from the core team would also be useful here. This is not really a case of operators enabling things that cannot be done on different ways: it's more about language philosophy and style. Honestly, it doesn't seem a winning battle right now. If someone elaborated a pitch I'd be 100% in, and I'd try and help with the implementation (if no compiler support is going to be needed).
But chainability is the core argument here. The whole reason one might want any kind of composition member or operator is for writing on-the-fly single-expression code: in any other case, things can be simply put in constants, that will be used in the next statement. Even for function composition, if I'm fine in putting the result of the (forward) composition between aFunc and otherFunc in a constant, I can simply write:
let composed = { otherFunc(aFunc($0)) } /// this closure could be equally used in-place
One might even argue that { otherFunc(aFunc($0)) } is easier to understand than aFunc >>> otherFunct: I disagree, but that's just because I'm very familiar with >>>.
I think we actually can draw the line in a reasonable way: the bare minimum is an .apply method with inout version, with throw/rethrow.
I'm in. I'd start with a proposal to just add |> and an inout counterpart (I really like your &>): purely additive, and useful in itself.
Then I'd move on making instance members easily callable from a |> chain, for example with keypaths.
Then, I'd add >>> and <<< (at least) for function composition, that would work perfectly with the new keypaths' syntax, for example this
let x = value
|> \.lazy
|> \.compactMap { ... }
|> \.prefix(2)
|> Set.init
|> \.union(otherSet)
could easily be turned into this
let x = value
|> \.lazy
>>> \.compactMap { ... }
>>> \.prefix(2)
>>> Set.init
>>> \.union(otherSet)
Finally, I'd consider adding more application and composition operators for contextual computations (for example with Optional and Result), to complete the picture.
This does make it a lot clearer to me. I still think that it would be confusing, especially to newcomers, but I must admit it has a certain elegance to it.
but in the case of newcomers, or even experienced devs, spelled out function names instead of new operators should be preferred, and, since people have mentions also() and with() if we are gonna add those too, do we need new operators for them too?
so i think the operators approach (imho) kind of has its limits...
while not "perfect" either, if people are worried about polluting the top level namespace, cant we just use a protocol + extension?
that would also allow for existing code which has same the signature to still be called as expected
I've been asking for a while now what's the opinion of the community about the following question: must every conformance of every type to every interface (set of members) be declared explicitly in Swift? Because if this is what Swift is about, if this is a cornerstone of the language, it should be spelled out explicitly in some guideline, in order to help excluding solutions to problems based on anything "automatic".
I personally don't think that automatic addition of members to types should be banned, but it would be useful to know what's the language opinion (as in "opinionated language") on this.
Yes. Even key players in the development of Swift have said (anyone can search the forums as well as I) that the implicit conformances of enums would be in hindsight something that they'd change if they could, and that no further such implicit conformances are desirable.
thanks for the reply, and sorry for rehashing what you proposed earlier, let me just say i feel the same way, so +1 from me for protocol-based approach
from what i intuit from the language design, is that its the exact opposite to obj-c and more like rust... since swift is static, code has to be generated for conformances, and adding to every class/struct automatically probably adds code-bloat (is my guess, please correct me if im wrong about this)
personally, i have no problem adding conformances (im used to it) so i dont feel the need to push for a global addition (though it makes things a little less cool/powerful i suppose)
Thanks for this. I've missed the statement about enums implicit conformances (I assume you're talking about enums without associated types).
I'd like if this was stated in some guideline, though. Assuming that adding members to all type (with compiler support) is against language integrity, then an implicit .apply member is a no go. Given the limitations that I outlined with operators, if no further work is done in that direction then the only option I can see is some protocol to which an explicit conformance should be declared.
Let's consider just .apply for now (the argument is similar for other possible members). Let's say we have a Transformable protocol, and all types conforming to this protocol would get an .apply method: would it be preferable to have the method explicitly declared in the protocol? Or it would be better to just have the method implemented in a protocol extension?
To be more clear, given that the implementation of .apply will be:
extension Transformable {
public func apply<T>(_ transform: (Self) throws -> T) rethrows -> T {
try transform(self)
}
}
what would be the difference if this method was also declared in the protocol itself? That is:
public protocol Transformable {}
/// versus
public protocol Transformable {
func apply<T>(_ transform: (Self) throws -> T) rethrows -> T
}
This would be a protocol that exists only for "injecting code" into other types, that is, giving a method and a specific implementation for that method to a type. This is obviously possible in Swift, but would it be "uncanny"? Is this an acceptable strategy for standard library code?
The idea of placing this apply member into a protocol reminds me of @ole's blog post from several years back when a DefaultConstructible protocol with a single init() requirement was under discussion. The short version: we shouldn't think about protocols as just enabling syntactic constructions, they must also guarantee particular semantics.
In that light, Transformable seems ill-suited as a protocol, and if it was a choice between Transformable and adding an implicit member to all types, I would come down on the side of the implicit members, for a couple of reasons.
For one, Transformable doesn't guarantee any useful semantics about Transformable types. The discussion here is entirely about the syntax of chained function applications, and how to enable a "nice" way of achieving this construct, but it doesn't actually enable any new generic algorithms.
Secondly, though, all types are inherently transformable. The mere existence of a (T) -> U type for every pair of types T and U is evidence of this fact. I'm definitely in agreement that implicit conformances to protocols shouldn't be allowed, but I'm not yet convinced that the same argument applies to adding implicit members. Some of the issues identified in SE-0096 (like code-completion pollution) are definitely applicable to an implicit apply member as well, but the proposal is decidedly not an indictment of implicit members in general.
It seems silly to me to take a syntactic problem and solve it with a tool meant for semantic generalization which has limitations (no implicit conformances) which make it less-than-desirable for solving the original syntactic problem! IMO, it would be much better to address the syntactic issue head-on.