ceylon / ceylon-spec

DEPRECATED
Apache License 2.0
108 stars 34 forks source link

Syntax for function composition #123

Open gavinking opened 12 years ago

gavinking commented 12 years ago

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?

quintesse commented 12 years ago

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 commented 12 years ago

Sorry, I screwed up the direction of my arrow. Fixed now.

quintesse commented 12 years ago

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?

gavinking commented 12 years ago

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 commented 11 years ago

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 commented 11 years ago

Hrm, actually (*) doesn't look good at all. A diamond does look reasonable:

(f<>g)(x)

Or, alternatively, << works nicely:

(f<<g)(x)
chochos commented 11 years ago

But << is bitshift, already used... it probably wouldn't be confusing, as long as it can be parsed without any problems...

gavinking commented 11 years ago

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)
gavinking commented 11 years ago

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 commented 11 years ago

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 commented 11 years ago

I believe |> is often used for piping.

chochos commented 11 years ago

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 commented 11 years ago

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.

gavinking commented 11 years ago

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 commented 11 years ago

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 commented 11 years ago

Yes

chochos commented 11 years ago

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 commented 9 years ago

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.