robrix / Prelude

Swift µframework of simple functional programming tools
MIT License
409 stars 26 forks source link

Partial application operators #22

Closed robrix closed 9 years ago

robrix commented 9 years ago

Fixes #19.

robrix commented 9 years ago

I’m not at all sure this should actually be the same operator as normal function application. It works well enough for binary functions & combined with forward application:

let strings = [1, 2, 3] |> (map <| toString)

But it’s much less workable for ternary functions and chained with itself. This is because:

Therefore, we need an operator for partially-apply-at-right (and at that point we may as well add partially-apply-at-left).

/cc @dnalot

andymatuschak commented 9 years ago

What about %|>? You're only applying a part (a percentage) after all. :)

andymatuschak commented 9 years ago

(I hope you are prepared for you to be the only one who can read clients of this operator family)

robrix commented 9 years ago

What about %|>? You're only applying a part (a percentage) after all. :)

That’s clever!

(I hope you are prepared for you to be the only one who can read clients of this operator family)

That’s one of the reasons why I’m interested in precedent. F# has <| and |> as analogues (unflipped and flipped respectively) of Haskell’s $, and Prelude follows F# here (because $ isn’t a valid operator in Swift and because symmetry is nice).

texastoland commented 9 years ago

This operator ought to be left-associative because it applies to the rightmost

I think I finally understand why F#'s |> is left associative while $ is right (I think that's right)?

assert(f |> a |> b |> c == f(a)(b)(c)) // F# style
assert(f |> g |> h |> c == f(g (h (c)))) // Haskell syle

You're right they serve completely different purposes! F#'s is For applying parameters while Haskell's is for applying composition. That would mean F#'s <| acts more like $ than its own |>, lacking symmetry. I prefer Haskell semantics but couldn't mock up a version using reduce() that played nicely with it either.

I originally mocked up an alternative currying splat/explode operator that works like:

let mapped = SequenceOf(numbers)
  |> map* {$0.description}
  |> String.join(", ")

But for it to work with reduce() it should transform (S, U, F) -> U to U -> F -> (S -> U) (parens for emphasis) which doesn't seem very general (effectively curry but move the first parameter to the end for chaining):

let reduced = SequenceOf(numbers)
  |> reduce* ([])  {(xs, x) in xs + [x.description]}
  |> String.join(", ")

It also probably lacks sufficient precedence for Prelude.

That’s clever!

Maybe too clever :see_no_evil:

The cleanest solution I can imagine would be to make |> left associative like F# and rename the right associative version something reminiscent of $. I'll keep thinking about it...

texastoland commented 9 years ago

N.b. the Optional versions in my gist from @kristopherjohnson

texastoland commented 9 years ago

Thinking further: I don't see the general utility of left associativity.

I guess the utility I want is to treat Swift standard library functions like pipe-friendly curried functions. But I don't think we should pipe parameters into one another just to be functional :tm:. What about:

let reduced = SequenceOf(numbers)
  // SEE: http://msdn.microsoft.com/en-us/library/ee370400.aspx
  |> (reduce <|| ([],  {(xs, x) in xs + [x.description]}))
  |> String.join(", ")

It's not pretty but it's clean, more standard than my splatter, and feels more general (effectively curry all parameters after the first as one tuple e.g. maybe F#'s <||| too).

robrix commented 9 years ago

I think you’re confusing the semantics of <| and |>. a <| b applies a to b, i.e. a <| b == a(b). a |> b applies b to a, i.e. a |> b == b(a).

robrix commented 9 years ago

Note also that F#’s <|| and <||| aren’t doing partial function application; they’re (respectively) binary and ternary analogues of <|. We don’t use those in Swift because Swift functions are all unary functions of tuples (i.e. they’re uncurried in the Haskell sense).

texastoland commented 9 years ago

Relying via iPhone:

All my Haskell and F# comes from reading Scala academic papers but I'm pretty certain I have the semantics correct e.g. [1][2][3] etc.

But just like the links I agree with your design choice to mimic $. What did you think about <||? It exactly mirrors F# (except it also does partial application), works cleanly with reduce(), and applies generally to any function.

texastoland commented 9 years ago

Partial application is only a byproduct of Swift's higher order functions being uncurried. Without partial application we couldn't pipeline the sequence argument. AFAIK there's no formal operator describing partial application? My explode operator was kind of intended to explore currying as a solution without mucking with pipeline semantics.

robrix commented 9 years ago

I misunderstood the full semantics of <| in F#. With some experimentation, I’ve learned that F#’s <| can indeed take a binary function and does indeed only apply the leftmost operand. (Ditto |>, which is effectively just flip (<|).)

let f x y = x + y
f 1 2
let g = f <| 1
g 2

Prelude’s <| and |> therefore behave identically to F#’s (modulo the right-associativity of <|); and <||, <|||, ||>, and |||> aren’t currently available.

There’s still an important distinction between <|/|> and the operator which this branch is trying to define:

I’m going to experiment a bit to see how variations on this feel.

robrix commented 9 years ago

@dnalot Okay, I think I follow now. (I’m going to phrase a bunch of stuff you’ve already said in my own words now, just to make sure I’m on the same page.) There are two different uses of <| and |> in F#:

You can use |> for partial application of e.g. map, and you can use <| to reveal data flow, but they’re suboptimal for those purposes:

So, I propose:

  1. Implementing partially-applying overloads of <|.
  2. Changing the associativity of <| to left, matching F# and avoiding the need to parenthesize when chaining partial applications.
  3. Overloading flip for ternary functions.
  4. Using flip when you want to apply the rightmost parameter:

    flip(map) <| toString
    let sum: [Int] -> Int = flip(reduce) <| (+) <| 0

I’m not sure about the flip overload; your suggestion of <|| may be a better option for reduce here. <|| would still require ternary flip, or we’d need a special operator to flip from the right, which I don’t really like the sounds of.

robrix commented 9 years ago

@dnalot I’ve implemented the above proposal :point_up:

Note in particular that I’ve opted to just use flip instead of changing the operator’s semantics to work around Swift’s map, reduce, etc. being backwards.

Let me know what you think; and sorry for the confusion on my end!

robrix commented 9 years ago

@andymatuschak Same to you :smile: — would love to hear what you think.

texastoland commented 9 years ago

I love your proposal and look forward to reading the diffs tonight! Although I wish there were a less noisy way to flip() I appreciate the precedence. Your reply about pipeline use cases clarified the associative intent of those operators in a way the F# community hasn't captured :+1:

robrix commented 9 years ago

I’ve updated the README to match, now. flip does end up being noisy, but it’s not as bad as I feared: flip(reduce) <| (+) <| 0 isn’t so bad.

On the other hand, flip(flip(reduce) <| (+)) is pretty awful.

robrix commented 9 years ago

@dnalot I’m not going to merge this in for a bit yet (I want to mull it over and collect more feedback), so take your time :+1:

robrix commented 9 years ago

/cc @patrickt who knows things™

robrix commented 9 years ago

Okay, I think I’m pretty much content with this. Going to let it sit for a little longer and then merge.

texastoland commented 9 years ago

:+1: I love how Prelude et.al. fill a gap cleanly between the scopes of LlamaKit and swiftz while addressing practical usage!

robrix commented 9 years ago

:sparkles: