sanctuary-js / sanctuary

:see_no_evil: Refuge from unsafe JavaScript
https://sanctuary.js.org
MIT License
3.04k stars 94 forks source link

Consider flipping the argument order of ap #645

Closed Avaq closed 3 years ago

Avaq commented 5 years ago

I would argue that the current argument order does not favour composition with partial application. I would want the following two programs to be equivalent.

S.lift3 (f) (ma) (mb) (mc)
S.pipe ([
  S.map (f),
  S.ap (mb),
  S.ap (mc),
]) (ma)

However, they are not, because S.ap takes the function argument first, rather than last, necessitating a flip before ap is useful:

S.pipe ([
  S.map (f),
  S.flip (S.ap) (mb),
  S.flip (S.ap) (mc),
]) (ma)

Furthermore, the observation that ap is the only Fantasy Land function in Sanctuary that does not follow the convention that its last argument is the this to the Fantasy Land method reinforces my view that the arguments should be flipped. This has also led me to flip the arguments in the latest version of Fluture.

Avaq commented 5 years ago

I realized later that from the perspective of combinatory logic, flipping the current ap is a bad idea. It would no longer be equal to the common S combinator.

davidchambers commented 5 years ago

On the subject of combinators, I now believe Sanctuary should provide all the common combinators, even though several are specializations of functions Sanctuary already provides.

If you are interested in pursuing this change, Aldwin, please create a branch named avaq/ap, flip the argument order, update the corresponding test cases, and finally create experimental branches for at least two projects that depend on Sanctuary (pointing to avaq/ap). I realize that this would be a significant amount of work, but we should not change a crucial function unless we are convinced that a change is warranted.

davidchambers commented 4 years ago

Are you still in favour of this change, Aldwin?

Avaq commented 4 years ago

The arguments in favour of and those against cancel each other out for me. I prefer to leave things as is, but only by a small margin.

davidchambers commented 3 years ago

Consider it considered!

Avaq commented 3 years ago

Actually why did we never consider just adding ap_ or flap (:grin:*) as I call it in my code-bases?

* What makes flap a great name is that its primary purpose is to use it when lift3 is not big enough, so in my code, it only ever appears in pipelines with 4 or more consecutive flaps. The name is derived from flip and ap, of course.

davidchambers commented 3 years ago

I like the idea of adding ap_. :)

dotnetCarpenter commented 2 years ago

Just did some local tests of this to better understand the effect of ap_ to liftN. Perhaps it is useful for others too.

liftA4 :: Applicative f => (a -> b -> c -> d -> e) -> f a -> f b -> f c -> f d -> f e
liftA4 f a b c d = liftA3 f a b c <*> d

https://hackage.haskell.org/package/base-4.16.1.0/docs/src/GHC-Base.html#liftA3

//    lift4 :: Apply f => (a -> b -> c -> d -> e) -> f a -> f b -> f c -> f d -> f e
const lift4 = f => a => b => c => d => S.ap (S.lift3 (f) (a) (b) (c)) (d)

//    lift5 :: Apply f => (a -> b -> c -> d -> e -> f) -> f a -> f b -> f c -> f d -> f e -> f f
const lift5 = f => a => b => c => d => e => S.ap (lift4 (f) (a) (b) (c) (d)) (e)

//    ap_ :: Apply f => f a -> f (a -> b) -> f b
const ap_ = S.flip (S.ap)

//    lift4_ :: Apply f => (a -> b -> c -> d -> e) -> f a -> f b -> f c -> f d -> f e
const lift4_ = f => a => b => c => d => ap_ (d) (S.lift3 (f) (a) (b) (c))

//    lift5_ :: Apply f => (a -> b -> c -> d -> e -> f) -> f a -> f b -> f c -> f d -> f e -> f f
const lift5_ = f => a => b => c => d => e => ap_ (e) (lift4_ (f) (a) (b) (c) (d))

const verify = a => b => c => d => (
  a === 1 && b === 2 && c === 3 && d === 4
    ? a + b + c + d
    : new TypeError ('Argument mismatch')
)

const verify2 = a => b => c => d => e => (
  e === 5
    ? verify (a) (b) (c) (d) + e
    : new TypeError ('Argument mismatch')
)

const one   = S.Just (1)
const two   = S.Just (2)
const three = S.Just (3)
const four  = S.Just (4)
const five  = S.Just (5)

console.debug (
  lift4  (verify) (one) (two) (three) (four), // Just (10)
  lift4_ (verify) (one) (two) (three) (four), // Just (10)

  lift5  (verify2) (one) (two) (three) (four) (five), // Just (15)
  lift5_ (verify2) (one) (two) (three) (four) (five), // Just (15)
)