Syntax for function composition
Closed this issue · 34 comments
[@gavinking] I would like an operator for composing functions, but I'm not sure what actual symbol to use. I suppose =>
and <=
could work.
function times2plus1(Integer i) = 2.times => 1.plus;
or
function times2plus1(Integer i) = 1.plus <= 2.times;
Another possibility would be @
which sorta kinda looks a little bit like the traditional symbol in mathematics if you squint. But it doesn't make the direction of composition clear, and is less flexible than =>
and <=
because you can't choose the direction of composition.
Any suggestions?
[Migrated from ceylon/ceylon-spec#123]
[@quintesse] Or the other way around so it retains the same order as if you would
writing it without composition?
function times2plus1(Integer i) = 1.plus(2.times(i))
function times2plus1(Integer i) = 1.plus -> 2.times;
Update: what I wrote above isn't correct of course.
NB shouldn't this really be:
function times2plus1 = 1.plus -> 2.times;
where the parameters are determined from the (composited) function's
parameters?
[@gavinking] Sorry, I screwed up the direction of my arrow. Fixed now.
[@quintesse] Well I was referring more to the fact that you're supplying a parameter list while this should depend on the functions themselves, right? I mean where did i
go in your example?
Well I was referring more to the fact that you're supplying a parameter list
The name of a parameter (along with it's default value and whether is is a sequenced param) is important in Ceylon. Sure, you can always declare the above as:
value times2plus1 = 2.times => 1.plus;
But then you lose the ability to call it using a named argument invocation, along with the ability to specify default values, etc. You always have the choice in Ceylon between:
Callable<Integer,Integer> times2plus1 = 2.times => 1.plus;
and
Integer times2plus1(Integer i) = 2.times => 1.plus;
They mean approximately the same thing, but the second one is more readable and has more information in it.
[@gavinking] Even if we never add an operator for this, we still need a function to do it. FTR, here it is:
X compose<X,Y,Z>(X(Y) x, Y(Z) y)(Z z) => x(y(z));
There's a slight variation when the first function produces the argument list of the second function as a tuple:
X multicompose<X,Y,Z>(Callable<X,Y> x, Y(Z) y)(Z z)
given Y satisfies Sequential<Void>
=> x.invoke(y(z));
So what would be a good operator for composition? Well, now that =>
and @
are taken, I suppose (*)
could work...
[@gavinking] Hrm, actually (*)
doesn't look good at all. A diamond does look reasonable:
(f<>g)(x)
Or, alternatively, <<
works nicely:
(f<<g)(x)
[@chochos] But <<
is bitshift, already used... it probably wouldn't be confusing, as long as it can be parsed without any problems...
[@gavinking] A different approach would be to have a unary operator—probably a prefix *
or ^
, or perhaps a postfix !
—that acts as an abbreviation for compose(f)
given:
X compose<X,Y,Z>(X(Y) x)(Y(Z) y)(Z z) => x(y(z));
Then you could write:
^f(g)(x)
And:
^f(^g(h)))(x)
A different approach
Scratch that, the ^
would not work, because we can't infer the needed type parameters. Back to defining it as a binary operation...
[@gavinking] Here's a definition of compose()
that handles functions with multiple parameters:
X comp<X,Y,Z>(X(Y) x, Y(Z) y)(Z z) => x(y(z));
shared Callable<X,Z> compose<X,Y,Z>(X(Y) x, Callable<Y,Z> y)
given Z satisfies Sequential<Void>
=> unspread(comp(x, y.invoke));
But this definition depends on the following function, which can't currently be defined outside of the language module:
Callable<X,Z> unspread<X,Z>(X(Z) x)
given Z satisfies Sequential<Void> {
object result satisfies Callable<X,Z> {
shared actual X invoke(Z args) {
return x(args);
}
}
return result;
}
unspread(f)
does the exact opposite of f.invoke
.
[@RossTate] I believe |>
is often used for piping.
[@chochos] IIRC, F# has something like that actually... You can pipe several functions like that, a|>b|>c|>d
but I don't remember if the args go in a(x) or d(x)...
[@gavinking] Hrm. |>
looks fine in the font github uses, but it looks terrible in the font Eclipse uses on Mac. :>
looks better (in Eclipse at least, though not so good here) but for some reason it just doesn't appeal to me at all.
But this definition depends on the following function, which can't currently be defined outside of the language module:
Hrm, with the new stuff based on tuples instead of sequenced type parameters, the definition of curry()
also depends on unspread()
defined above.
Callable<Return,Rest> curry<Return,Element,First,Rest>
(Callable<Return,Tuple<Element,First,Rest>> f)
(First first)
given Rest satisfies Sequential<Void>
given First satisfies Element
given Rest satisfies Sequential<Element>
=> unspread((Rest args) f.call(Tuple(first, args)));
Of course, if I only cared about the case of two parameters, I would have been able to just write:
X curry<X,Y,Z>(X(Y,Z) f)(Y y)(Z z) => f(y, z);
Anyway, it's interesting how important unspread()
is do be able to represent these basic operations.
[@gavinking] Unless someone else has some brilliant ideas, I don't think we need a special syntax for this in Ceylon 1.0. I think we can live with compose(g,f)
and partial(f)
.
[@FroMage] Yes
[@chochos] You can't be serious - your argument against an operator is the way it looks with the default font in Eclipse on Mac?
Yes please let's just use regular function names for now.
[@sirinath] If you want to pipe forward or compose multiple function multiple nested composes can become hariry:
E.g. (From F#)
data
|> ParStream.ofArray
|> ParStream.filter (fun x -> x % 2L = 0L)
|> ParStream.map (fun x -> x + 1L)
|> ParStream.sum
but alternatively compositionBuilder
as a composition builder which can return a object which in turn can be used to this composes the results again forwardCompositionBuilder(f)(g)(h)(i).fun
. You can choose a shorter name. Also pipeForward(value)(f)(g)(h)(i).result
. Again you can choose some intuitive name.
As a variation on @gavinking’s initial =>
and <=
, there’s >>>
and <<<
in Haskell. They may feel a bit strange and ASCII-arty, but OTOH they don’t conflate with traditional bit shift lexemes and with use of the fat arrow in definitions here.
@arseniiv actually, stuff like >>
and >>>
requires really major hacks to be able to lex correctly, since it's impossible to distinguish between >>
and two >
using a regular expression. Probably not a problem in Haskell because AFAIR <
and >
are not used for grouping.
@gavinking Yes, they aren’t. Oh, damn, I forgot about this problem, sorry. I heard about it regarding some C++ history a long time ago and then forgot.
If #6615 ever comes live, I would prefer using the same |>
as other languages (F#, maybe JS...).
But then, in order to avoid ambiguity, a different token should be used for "prefix function parameter application" (i.e. ' |()' ):
|("hello") \\ piped parameter application
|> String.size \\ \
|> (=> Integer.format(_, 16)) \\ |> function composition
|> print; \\ /
@someth2say well, curiously, as long as there are two operators:
- pipeline
|>
- compose
||>
And if ||>
has a higher precedence than |>
, then
a |> f |> g |> h
would mean effectively the same thing as
a |> f ||> g ||> h
which is interesting, I think.
So, curiously, you kinda get your syntax for free.
Question: which looks better:
string |> String.uppercased ||> trim ||> print
Or:
string |> String.uppercased |>> trim |>> print
Since it seems to me that, with the addition of the pipeline operator, we should use something visually similar for function composition.
A third option:
string |> String.uppercased .> trim .> print
So, curiously, you kinda get your syntax for free.
Yes, that's what I was trying to explain 😄 I would love to actually hide this difference from developers, using the same tokens for both cases. But also agree the ambiguity may drive to issues.
Question: which looks better:
I like the .>
, and add my own proposal:
string |> String.uppercased +> trim +> print
as I see the composition like some kind of function "addiction" (without commutativity, indeed).
Question: Will we eventually consider "esoteric" compositions, like optional
or spreading
?
If so, even we are not targeting those right now, maybe it is worth having in mind a syntax allowing to add those (maybe +?>
and +*>
, but really make my ascii-art radar cry...)
Wait wait wait, isn’t it “obvious” that the correct syntax for composition, given that a pipe is written |>
must clearly be:
>|>
Since composition has a function on both ends, whereas a pipe has a function only as the RHS.
And then reversed composition, if we need it, would be:
<|<
Will we eventually consider "esoteric" compositions, like
optional
orspreading
don't know about the best ascii syntax, but I'd like to have this feature.
Wait wait wait, isn’t it “obvious” that the correct syntax for composition, given that a pipe is written |> must clearly be:
I agree on the reason for using >|>
for composition, but.... isn't it a bit "fishy"?
If we could create Callable
s, ", we could leave "esoteric" compositions out of the compact syntax, and leave those to the verbose way, as in "ceylonChain":
value fun = ifExists(String.uppercased).to(trim).to(print);
fun(string);
Or even without callables (but a bit less attractive):
value ch = chain(string).ifExists(String.uppercased).to(trim).to(print);
ch.do();
Now I see it, maybe it can be worked out to allow passing the value at the end...
I agree on the reason for using
>|>
for composition, but.... isn't it a bit "fishy"?
Sure, it's definitely a fish.
It's pretty trivial to implement this via the same trick I used to implement #6615, so might as well work in it now.
An implementation of this is now available on the 6615
branch.
I've merged this work to master. Still need to mention >|>
in the spec.
Added to spec. Closing this issue, but please reopen if you run into any serious problem with this work.