-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Extend the |>
operator in order to chain tuples.
#14476
Conversation
|>
operator in order to chain tuples.|>
operator in order to chain tuples.
@@ -664,6 +664,17 @@ Generic Functions | |||
julia> [1:5;] |> x->x.^2 |> sum |> inv | |||
0.01818181818181818 | |||
|
|||
.. function:: |>(xs::Tuple, f) | |||
|
|||
.. Docstring generated from Julia source |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no it isn't? docstrings need to be put in .jl code for this to be accurate
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sorry, I was away for a while, and haven't catch up to recent doc changes. I just copied it over. Where do I need to put this .jl code then, for this to be true?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oooh I get it now! :P
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's explained in CONTRIBUTING.md
I disagree with your readability comparison between the standard function application syntax used by the language vs the chaining pun of the |
see #13240 |
@tkelman I admit that my examples are short of imagination, here are good examples of idiomatic julian code in the wild, in which the logic is best read from left to right:
function main(window)
tex("T = 2\\pi\\sqrt{L\\over g}") |>
fontcolor("#499") |>
pad(5mm) |>
fillcolor("#eeb")
end
We have seen that people expect this to work. The stack overflow example in #13240 is one that I have been asked about a lot. When asked What's in your .juliarc.jl file? Stefan answers:
I think I'm with him on this one, why have every one extend
That's what I also think, not sure either. When I first tried this I used But mine conveys the idea that |
At the same What's in your .juliarc? thread:
|
|
That looks like Escher is pretending Julia is much more of a functional language than it actually is. It doesn't make a lot of sense to me to be returning functions from things like |
Instead of invoking currying or returning functions from |
Infix macros was #11608, which was closed with a "this is probably not a good idea, but you could try to implement it if you really want to." Macros don't play well with extending to new types since they work only on expressions, so packages would clobber each other's definitions if they both tried to define the same infix macro with different meanings. |
I don't think there are any pretensions, Escher's code works, and several people like this style, which they use and extend since we encourage the use of I'd like to hear @shashi opinion on this PR.
Why do you think it's a pun and/or dislike this syntax?, I'm truly curious. I agree that the
I don't understand why you say so, ie: julia> Base.|>(xs::Tuple, f) = f(xs...)
|> (generic function with 7 methods)
julia> foo(x) = "calling foo(x)"
foo (generic function with 1 method)
julia> foo(x, y) = "calling foo(x, y)"
foo (generic function with 2 methods)
julia> foo(x::Int, y::Char) = "calling foo(x::Int, y::Char)"
foo (generic function with 3 methods)
julia> foo{T<:Number}(x::T, y::T) = "calling foo{T<:Number}(x::T, y::T)"
foo (generic function with 4 methods)
julia> foo(x, y, args...) = "calling foo(x, y, args...)"
foo (generic function with 5 methods)
julia> 1 |> foo
"calling foo(x)"
julia> (1,) |> foo
"calling foo(x)"
julia> (1, "2") |> foo
"calling foo(x, y)"
julia> (1, 2) |> foo
"calling foo{T<:Number}(x::T, y::T)"
julia> (1, '2') |> foo
"calling foo(x::Int, y::Char)"
julia> (1, '2', "3", [4:6;]) |> foo
"calling foo(x, y, args...)" |
Yes, what I meant when I said:
I meant that the previous usage of It would be weird if
So it makes sense to me for |
I know currying has been previously discussed, with no consensus on sight, but this PR is not about a new fancy way of implementing currying either using macros or what else, it's just about extending the current Here is another evidence of people expecting this to work, it's in spanish so I didn't bring it to the table at first: Currently support for currying has been:
And that's it! But it is there. So I agree that if we won't really support this (why?) it should be removed from |
Has anyone benchmarked against code to do the same task but written idiomatically rather than currying everything? I'd never write numerical code like that. I dislike the syntax because it's backwards and I'd rather open source code be written with a mostly consistent style given that more people will be using and reading it than writing it. Having two syntaxes for calling functions that each look backwards and wrong to different subsets of the community is bad, I'd rather pick one style and stick with it. Pipelines work for bash but don't really work for Julia. Given the inference issues with function arguments, the performance penalty of splatting, and the semantic issue @vtjnash points out, I'm against merging this change. |
@tkelman here are some tests, there is of course overhead, but I don't think is that much, I understand that you wouldn't write numerical code like this (I wouldn't either), but not all code is numerical code, julia> @time 1 |> foo;
0.000004 seconds (4 allocations: 160 bytes)
julia> @time foo(1);
0.000002 seconds (4 allocations: 160 bytes)
julia>
julia> @time (1,) |> foo;
0.000007 seconds (5 allocations: 176 bytes)
julia> @time foo((1,)...);
0.000007 seconds (5 allocations: 176 bytes)
julia>
julia> @time (1, "2") |> foo;
0.000005 seconds (5 allocations: 192 bytes)
julia> @time foo((1, "2")...);
0.000005 seconds (5 allocations: 192 bytes)
julia> @time foo(1, "2");
0.000002 seconds (4 allocations: 160 bytes)
julia>
julia> @time (1, 2) |> foo;
0.000007 seconds (5 allocations: 192 bytes)
julia> @time foo((1, 2)...);
0.000006 seconds (5 allocations: 192 bytes)
julia> @time foo(1, 2) ;
0.000002 seconds (4 allocations: 160 bytes)
julia>
julia> @time (1, '2') |> foo;
0.000006 seconds (5 allocations: 192 bytes)
julia> @time foo((1, '2')...);
0.000007 seconds (6 allocations: 208 bytes)
julia> @time foo(1, '2');
0.000003 seconds (4 allocations: 160 bytes)
julia>
julia> @time (1, '2', "3", [4:6;]) |> foo;
0.000014 seconds (7 allocations: 336 bytes)
julia> @time foo((1, '2', "3", [4:6;])...);
0.000012 seconds (8 allocations: 352 bytes)
julia> @time foo(1, '2', "3", [4:6;]);
0.000005 seconds (6 allocations: 288 bytes) I agree with you about consistency, there are a lot of: foo() = begin
# ...lots of code
end In Julia's source code, which should be IMHO: function foo()
# ...lots of code
end But even if we enforce that style, I don't think that banning the first option would be nice nor a good idea (it's great at the REPL, where one do more writing than reading). Perhaps a Just to be sure, are you against this PR only or also against retaining Type inference is the worst issue: julia> @code_warntype 1 |> foo;
Variables:
x::Int64
f::F
Body:
begin # operators.jl, line 198:
return (f::F)(x::Int64)::Any
end::Any
julia> @code_warntype foo(1);
Variables:
x::Int64
Body:
begin # none, line 1:
return "calling foo(x)"
end::ASCIIString Do you think this is not going to change along with the splatting slowness (I've been told not to use it ...a lot lately!)? |
By the way, Could anyone tell me why is |
I don't think we'd have consensus on totally deprecating If you can find and fix any A real DSL would be implemented using macros, where it's free to do whatever it wants with syntax as long as it parses. DSL's that cause parse errors can be implemented as string macros, like Cxx.jl. The splatting penalty probably won't go away completely, since it forces the call to go through the Julia multiple dispatch machinery. Maybe if the splatted collection's length is known at compile time then more optimizations could be done. |
@tkelman Honestly just curious... if |
The former is the syntax for function calls, as evidenced by the pun implementation of |
@tkelman could you tell me if you would recommend the use splat at all in numerical code? I wonder because recently I've noticed that everyone promptly advises me not to use it, no mater how banal the example I use is. Are you OK with using it in non numerical code? |
In that quote I was referring to the currying API example you gave from Escher, not splatting specifically. Splatting is convenient and there are plenty of situations where it's the nicest-looking way to accomplish something, but it comes with a performance penalty that should be kept in mind is all. It's not really suitable for anything that's going to be performance-critical. If you don't care at all about performance then you can use whatever code style you want, but performance is probably the largest single selling point with Julia. |
@Ismael-VC I would not call this idiomatic Julia at all, it's something specific to Escher's DSL. The curried methods are generated by a macro (which has other odd features like optional leading arguments). There is absolutely no need to complicate the base language with such features. @tkelman, yes,
This is very true, in Escher I am cautious to avoid such situations while using the said macro. It is not and should not be the concern of the user of the library.
Rather, I'd like to think of Julia as a small, flexible substrate for doing anything. @Ismael-VC I don't think this PR should go in, because it adds a finicky special case to an otherwise very clear |
Thank you everyone for your comments, very educative as always! |
@tkelman The type inference issues are not introduced by this PR, that's the way it is now, so I don't see why adding the tuple method is something bad, but the current method isn't (it's used in Julia base) taking into account only the inference of course, ie. not the semantic meanings. I wonder about this, because don't you think this could be another good target for a cleanup?, ie, change all occurrences in julia source of:
Since it's more performant and inference is more accurate, If someone think it's a good idea I'd like to take care of that clean up too. |
I actually have a branch where I did just that - 212727c, it's a few months out of date and needs a rebase, I think I got distracted by other things before opening a PR for it. |
That's great! I'll take care of the other cleanup then. Maybe we can reach consensus to deprecate |
Here is yet another user trying to pass multiple arguments to
|
So... should Also: I suspect @one-more-minute will have an opinion on this / |
@hayd thanks for the ping. I do disagree with some of the arguments against this PR; for example, I don't think it's a good idea to be dogmatic about coding style. Fundamentally, low-level systems code should be written differently from high-level logic like web routers, and I personally think it's a great strength that Julia supports these different levels of abstraction reasonably fluidly. (In general, if I find code difficult to read it's because the concepts behind it are difficult, not because I'm confused by That said, this change means that the semantics of the |
@hayd If we are not going to extend it in Base, then I think it's a good idea to deprecate and move it to a package, it's causing to many _WTF_s as it is IMHO. And I really like the syntax and want to use it sometimes instead of deep nested calls or having to think about temporary variables. Package developers would be able to extend, use and most importantly: share this code and make it really useful and general, implementing it as they see fit. But if the type inference problems can be fixed, I think
It has been proven that it's not very clear to everyone, maybe changing the docstring:
to something that clearly states:
Or at least remove the easy part. After reading all your opinions, I also disagree with this being or not being idiomatic. Who gets to decide that? Could we at least make a poll or something more in the spirit of the Quorum language (The world's first evidence-oriented programming language)?
We could really leverage to crowd source opinions. Also even if the meaning of the method
The only show stopper for me so far is the type inference issue (we have survived with it in this case, so far), but I understand this is going to be fixed. |
@one-more-minute I actually wanted to do what #13240 solves, but it only works at the beginning of the expression AFAICT: julia> Base.|>(x, y, f) = f(x, y)
|> (generic function with 7 methods)
julia> Base.|>(args...) = args[end](args[1:end-1]...)
|> (generic function with 8 methods)
julia> (1, 2)... |> complex |> x -> (x.re, x.im)
(1,2)
julia> (1, 2)... |> complex |> (x -> (x.re, x.im))... |> divrem # people might try this
ERROR: MethodError: `start` has no method matching start(::Function)
in append_any at essentials.jl:127
julia> (1, 2)... |> complex |> (x -> (x.re, x.im))()... |> divrem # or this
ERROR: UndefVarError: x not defined
julia> Base.|>(xs::Tuple, f) = f(xs...)
|> (generic function with 9 methods)
julia> (1, 2) |> complex |> x -> (x.re, x.im) |> divrem
(0,1)
I understand but my solution seems to be more generic. |
+1 A related issue IMO is function composition, it'd be nice if there was an efficient multi-argument solution for that too. #4963 (comment) |
I agree with @vtjnash here. You can't switch to splatting based on the type of the argument. |
I think this is a problematic idea. I definitely see the value for relatively superficial decisions like how to spell keywords, and that you should use infix syntax for math instead of lisp syntax, etc. However I think the results on this are fairly unsurprising, and programming languages have been converging on them already. But these decisions are only a tiny part of language design. A human subject sitting for a brief session on language syntax will not be sensitive to many important factors. Perhaps the most obvious one is what happens when a project gets big and continues for a long time. The Stefik and Siebert paper is quite careful and explicit about narrowing the scope to, essentially, keyword choices. It's a fine result but it hardly gives you a full language design. One has to appreciate just how unimportant many syntax choices are. For example, Quorum seems to use I notice Quorum uses the syntax Quorum is a java-like, class-based, imperative language where the classes have "actions". Does that mean "the science says" people shouldn't learn or use functional programming? No, it means the authors are asking "what language does the evidence lead us to, assuming it must look a lot like java?" |
@JeffBezanson how can anyone say something is julian or not if its taking into account only personal opinions or the opinions of a relatively small group of people, when there are many other julians out there with their own (unknown) opinions that make up the community and are unaware of this discussions but may be willing to share their opinions?
How is that different from?
I understand that you disagree with their design choices and that is perfectly fine, but I think you miss the point. I'm talking about the evidence, the culture and spirit of quorum, science, a small group of people taking better and more informed decisions that benefit the whole julian community in an easy way, not in the current implementation of the Quorum language. |
The main difference is that I don't claim that superficial features of julia are evidence-based. But the only point of my comment is that human-factors-based evidence only gets you so far. The problem is this:
Clearly, the intent is to record whatever result we get. However, if one time the result happens to be a tuple, you get a no method error. This is surprising, especially because you can currently always rewrite You claimed that nobody would need to rewrite code because of this. That is false. Uses of |
@Ismael-VC Sometimes you'll still need parens...
We can't/don't need to change the semantics for passing tuples. I think the consensus is that Discussion of changing how language decisions are made is way off-topic. |
@hayd oh of course, thank you for pointing that out, it would be difficult to keep up the parenthesis in a multiline expression though.
@JeffBezanson yes I see what you mean now, I was the one how missed that. julia> foo(x::Tuple) = @show x
julia> Base.|>(xs::Tuple, f) = f(xs...)
|> (generic function with 7 methods)
julia> x = (1, 2, 3)
(1,2,3)
julia> foo(x::Tuple) = @show x
foo (generic function with 1 method)
julia> foo(x)
x = (1,2,3)
(1,2,3)
julia> x |> foo
ERROR: MethodError: `foo` has no method matching foo(::Int64, ::Int64, ::Int64)
in |> at none:1
julia> (x,) |> foo
x = (1,2,3)
(1,2,3) I can make another PR in order to deprecate |
Can we now consider #13240 again which is non-breaking and solves the problems with multiple arguments if there is no intent to abolish |
I was trying to use an annonymous function in place, something like this:
This works but is less readable:
Extending
|>
to map tuples of arguments:This will let us make arbitrary chaining possible:
Previous usage and semantics are left unchanged as far as I can tell, also see the tests and documentation.