robrix / Prelude

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

Curried flip? #38

Closed sharplet closed 9 years ago

sharplet commented 9 years ago

Take this (not so) hypothetical implementation of a Schwartzian transform that makes use of curried, flipped versions of some standard library functions:

func sortedBy<S: SequenceType, Key>(seq: S, key: S.Generator.Element -> Key) -> [S.Generator.Element] {
  return lazy(seq)
    |> map { i in (i, key(i)) }
    |> sorted { a, b in a.1 < b.1 }
    |> map { i, _ in i }
}

Currently I need to hand-write those curried, flipped versions of map and sorted.

Given that Prelude already provides |> and <|, making this kind of chaining possible—would it also sense to have a combined curry & flip function? Then this would be achievable using only Prelude, like so:

func sortedBy<S: SequenceType, Key>(seq: S, key: S.Generator.Element -> Key) -> [S.Generator.Element] {
  return lazy(seq)
    |> flip(map) { i in (i, key(i)) }
    |> flip(sorted) { a, b in a.1 < b.1 }
    |> flip(map) { i, _ in i }
}

I'm not sure if you'd really want to overload the existing flip function as implied here, but perhaps there's an alternative name?

robrix commented 9 years ago

Early on I avoided overloading to provide both curried & uncurried functions since the result could be achieved by composition. Case in point, curry(flip(map)) { i in (i, key(i)) } should work today, I think? (At some cost to elegance.)

Somewhat damningly, overloading flip to curry as well is overloading strictly on the return type, which can cause some frustrating ambiguities.

Ultimately, I need to decide to what extent Prelude wants to make up for Swift’s design flaws vs. providing simple, unambiguous tools that you can apply to work around them. Phrasing it like that, neither of those sound very good :sob:

As you note, I’ve already gone some distance towards the former with <| and |>; I would typically write:

func sortedBy<S: SequenceType, Key>(seq: S, key: S.Generator.Element -> Key) -> [S.Generator.Element] {
  return lazy(seq)
    |> (flip(map) <| { i in (i, key(i)) })
    |> (flip(sorted) <| { a, b in a.1 < b.1 })
    |> (flip(map) <| { i, _ in i })
}

(Again, at some cost to elegance!)

I’m torn. I’d be very curious to know what consumers of the API think. Should Prelude 2.0 break backwards-compat to attempt to make functional Swift more elegant by e.g. currying by default?

kazmasaurus commented 9 years ago

My preference would be to leave flip alone, and put curry(flip(f)) (which did indeed work correctly @robrix, at least in all my limited testing over here and with your sorted |> map |> String.join example in the README) into a separate function. I want to call it flurry, because :sunglasses:

The other option would be a postfix operator (which it turns out has actually been tried before), and while this comes with all the overhead of operators, I still love it.

func sortedBy<S: SequenceType, Key>(seq: S, key: S.Generator.Element -> Key) -> [S.Generator.Element] {
  return lazy(seq)
    |> map~ { i in (i, key(i)) }
    |> sorted~ { a, b in a.1 < b.1 }
    |> map~ { i, _ in i }
}

Amusingly, I actually discovered this morning that I had written the operator version, but had just not committed or pushed it yet. I'll go throw that in a PR in case you want it.

robrix commented 9 years ago

Having given this more thought, I don’t think I want Prelude to offer a combined curry/flip operator. Prelude has not thus far been in the business of fixing Swift’s design decisions, and I think that’s a reasonable charter for it to continue with, at least for the present.

sharplet commented 9 years ago

Sounds like a reasonable approach. Appreciate you taking the time to consider it! ✨

On Mon, 6 Apr 2015 at 23:16 Rob Rix notifications@github.com wrote:

Closed #38 https://github.com/robrix/Prelude/issues/38.

— Reply to this email directly or view it on GitHub https://github.com/robrix/Prelude/issues/38#event-274119802.