Skip to content
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

["Request"] New syntax for partial application #652

Open
ferranpujolcamins opened this issue Nov 10, 2020 · 6 comments
Open

["Request"] New syntax for partial application #652

ferranpujolcamins opened this issue Nov 10, 2020 · 6 comments

Comments

@ferranpujolcamins
Copy link
Contributor

ferranpujolcamins commented Nov 10, 2020

Description

It's annoying when I can't write a function point-free style just because I cannot partially apply the second argument of a 2-ary function (or more generally, the ith parameter of an n-ary). For example, we need the lift function in Functor just because we can't partially apply the function in map in a convenient and easy way.

I propose a new syntax for partial application that extends the current one to allow partial application of any parameters in a function.

Sample usage

I propose to add a new operator <|, that takes a function as first argument and a tuple with values and placeholders as second argument. The placeholders (a placeholder is two underscores) help determine what parameter a value is being applied to.

For partial application of the first parameter, we can also keep the current simplified syntax.

let f: (String, Int) -> Int = { s, i in i }

// Partially apply first argument
let p1CurrentSyntax /* : (Int) -> Int */    = f <| "a"
let p1NewSyntax     /* : (Int) -> Int */    = f <| ("a", __)

// Partially apply second argument
let p2              /* : (String) -> Int */ = f <| (__, 1)

Note how I've written the resulting function type in comments, meaning that type inference works well with the syntax.

I chose to reverse the direction and parameter position of the current |> because now the position of the function and the tuple of arguments resembles the order in which you normally write function application.

Potential implementation

The trick is to define a special PlaceHolder type just so we can write .__ on our tuples.

If we also define __ as a global value we can avoid the leading dot. This is not needed (type inference also works if we write .__). It makes the syntax nicer, but it pollutes the global namespace. What do you think?

public enum PlaceHolder {
    case __
}

public let __ = PlaceHolder.__

infix operator <| : AdditionPrecedence

// Support for simplified syntax for partial application of the first parameter
public func <|<A, B, C>(_ f: @escaping (A, B) -> C, _ value: A) -> (B) -> C {
    { b in f(value, b)}
}

public func <|<A, B, C>(_ f: @escaping (A, B) -> C, _ values: (PlaceHolder, B)) -> (A) -> C {
    { a in f(a, values.1)}
}

public func <|<A, B, C>(_ f: @escaping (A, B) -> C, _ values: (A, PlaceHolder)) -> (B) -> C {
    { b in f(values.0, b)}
}

// ... implementations for higher arities

Modules

Bow

Breaking changes

None, unless we remove |>.

@ferranpujolcamins
Copy link
Contributor Author

ferranpujolcamins commented Nov 10, 2020

I find the |> and <| to be a bit distracting, because they are visually "big".
I suggest to use ~ instead.

let p1 = f<|("a", __)

let p2 = f~("a", __)

The later seems more clean to me. At the end, it's the __ that tells us that we are doing partial application. In an ideal world, we wouldn't even need the ~, so the "smaller" the better.

@truizlop
Copy link
Member

truizlop commented Nov 18, 2020

Looks good to me. Are you planning to support all combinations for this new operator? I mean, supporting something like:

func  f(a: Int, b: Int, c: Int) -> Int

f~(1, __, __)
f~(1, __, 3)
f~(__, 2, 3)

Also, up to which arity do you plan to support? This can be a huge amount of combinations.

@ferranpujolcamins
Copy link
Contributor Author

Yeah combinatorial explosion is a problem here. To support up to 6 parameters, we'll need 114 functions.

This has to be written with a script for sure. Maybe it's just not practical?

@truizlop
Copy link
Member

Using a script sounds good. I think we are currently supporting things up to 9 or 10 parameters (check zip implementations in Applicative, for instance). I think both the code and the script should be committed to the repo, in case we need to make any changes in the future. An alternative is to only support a single argument to be applied each time, like:

func f(a: Int, b: Int, c: Int) -> Int

f~(__, 1, __)~(__, 2) == f~(__, 1, 2)

However, the fact that the tuple keeps decreasing makes it hard to follow what's going on.

@ferranpujolcamins
Copy link
Contributor Author

ferranpujolcamins commented Nov 18, 2020 via email

@truizlop
Copy link
Member

Ok, let's go for 6 parameters, I think that's within most reasonable use cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants