8

I want something like J's fork feature, I guess. Is there any way to do this?

  • 1
    By asking the `pointfree` program: `pointfree "m h f g x = h (f x) (g x)"` -> `m = liftM2`. @kqr's answer is equivalent and might be better since the Applicative instance is often more efficient than the Monad instance. – bheklilr Nov 22 '13 at 20:11
  • @bheklilr Isn't the only reason `pointfree` suggests `liftM*` that it was written before `liftA*` existed? As soon as applicative is a superclass of monad, we can throw `liftM2` out of the window, can't we? – kqr Nov 22 '13 at 20:17
  • @kqr I would imagine so, or it could just be that `pointfree` looks in `Control.Monad` before `Control.Applicative`. Would there potentially be Monads for which `liftA*` has a different implementation than `liftM*`? My guess is that the behavior would be identical, but since Applicatives can sometimes add parallelism where Monads can't I wouldn't chuck `liftM*` out of the window just yet. – bheklilr Nov 22 '13 at 20:27
  • @bheklilr It depends on whether or not it's "allowable" for them to differ. `Monoid e => Either e` admits a neat `Applicative` which cannot form a `Monad`, but once the Applicative-Monad Proposal hits it's going to be even weirder for an the instances to differ... – J. Abrahamson Nov 22 '13 at 20:51

2 Answers2

15

This is, using so-called applicative style,

h <$> f <*> g

using <$> and <*> from Control.Applicative.


An alternative is to lift h into the (->) r applicative functor with

liftA2 h f g

The intuition behind that is that if

h        ::    a     ->    b     ->   c
-- then --
liftA2 h :: (r -> a) -> (r -> b) -> r -> c

so the lifted version takes two functions r -> something instead of the actual somethings, and then feeds an r to get the somethings out of the functions.


The liftA* and the corresponding combination of <$> and <*> are equivalent.

kqr
  • 14,791
  • 3
  • 41
  • 72
12

While @kqr has the more practical solution based on the Applicative instance for ((->) a), we can also talk about it in the "pipey" method

        +----- f ------+
       /                \
<---- h                  +------< x
       \                /
        +----- g ------+

which provides a very compositional kind of pointfree program. We'll create this program with tools from Control.Arrow.

First we get the rightmost part of our diagram using a common missing function in Haskell called diag or dup

--+
   \
    +----- x        dup :: x -> (x, x)
   /                dup x = (x, x)
--+

then the middle is created using the (***) combinator from Control.Arrow

----- f -----     (***)   :: (a -> b) -> (c -> d) -> (a, c) -> (b, d)
                  f       :: (a -> b)
                  g       ::             (c -> d)
----- g -----     f *** g ::                         (a, c) -> (b, d)

then the left side is exactly what uncurry does for us

  +--    uncurry   :: (a -> b -> c) -> (a, b) -> c
 /       h         :: (a -> b -> c)
h        uncurry h ::                  (a, b) -> c
 \   
  +--

Then wiring them all together we can erase the x points with a very compositional style.

m :: (a -> b -> c) -> (x -> a) -> (x -> b) -> x -> c
m h f g = uncurry h . (f *** g) . dup

                  +------ f ----+
                 /               \
         <----- h                 +-----------< x (eta reduced)
                 \               /
                  +------ g ----+
J. Abrahamson
  • 72,246
  • 9
  • 135
  • 180
  • This was a very neat way of thinking about the problem. Out of curiosity: how does it scale for the equivalent of `liftA3 f g1 g2 g3`? – kqr Nov 22 '13 at 20:42
  • 1
    Not terribly well. You have to do more more work packing and unpacking nested pairs. I'll introduce the standard `Control.Arrow` combinator `f &&& g = (f *** g) . dup` and then we have `liftA3 f g1 g2 g3 = uncurry ($) . first (uncurry f) . first (g1 &&& g2) . second g3 . dup`. Blech. – J. Abrahamson Nov 22 '13 at 20:47
  • For cross referencing, I often hear this kind of programming called programming in the "pair calculus". – J. Abrahamson Nov 22 '13 at 20:48
  • @J.Abrahamson Out of curiosity, are there any Arrow like libraries that can gracefully handle the `liftA3` case? – bheklilr Nov 22 '13 at 21:55
  • I'm not sure. I think Conal Eliott is currently working on writing a lambda calculus compiler which produces arrow-like code. I also see generalizations in papers sometimes where families of functions and irregular, but suggestive syntax works fine. Neither of which is practical, of course. – J. Abrahamson Nov 22 '13 at 22:11
  • Looks like concatenative programming. In Factor this is spelled `x g f bi h` where `bi` is implemented as `[ keep ] dip call`. It moves `f` out of the way for a moment, applies `g` keeping `x` around, then applies `f` and combines the results with `h`. (In Kitten it’s called `bothTo`, which I think is a better name.) – Jon Purdy Nov 23 '13 at 00:04
  • I think it very much is similar! I know you've been looking at typed concatenative programming... I think I owe you an email. – J. Abrahamson Nov 23 '13 at 01:01
  • 1
    Wow, I flubbed the `liftM3` version a bit. It's not quite as bad as I made it out to be: `liftM3 f g1 g2 g3 = uncurry ($) . (uncurry f . (g1 &&& g2)) &&& g3`. We can even use `apply :: uncurry ($)` in the `Arrow (->)` instance to write `apply . ((uncurry f . (g1 &&& g2)) &&& g3)` or even `apply . (liftA2 f g1 g2 &&& g3)` which gives a nice recursion relationship letting us generate `[liftAn | n <- [1..]]`. – J. Abrahamson Nov 24 '13 at 22:14
  • @J.Abrahamson That makes it sound a little like `(<*>)` can be defined in terms of `apply .: (&&&)`, which is cool. – kqr Nov 26 '13 at 11:29