Я думаю, стоит отметить, что ваше первоначальное предположение,
liftM2 f a b = liftM (_) (ap _ a)
не так уж далеко. Но ap
не совсем то место, с которого нужно начинать. Скорее рассмотрим
pairIO :: IO a -> IO b -> IO (a, b)
pairIO m n = do
a <- m
b <- n
return (a, b)
Теперь вы можете написать
liftM2 :: (a -> b -> c) -> IO a -> IO b -> IO c
liftM2 f m n = liftM _ (pairIO m n)
GHC скажет вам, что ему нужно
_ :: (a, b) -> c
и вы сможете заполнить это очень легко.
Это фактически отражает распространенную альтернативную формулировку понятия «аппликативный функтор»:
class Functor f => Monoidal f where
pur :: a -> f a
pair :: f a -> f b -> f (a, b)
Этот класс по мощности эквивалентен стандартному Applicative
классу.
Оказывается, что вы действительно можете комбинировать действия различными способами, благодаря Monoidal
ассоциативному закону. Это выглядит как
xs `pair` (ys `pair` zs) = jigger <$> ((xs `pair` ys) `pair` z's)
where
jigger ((x, y), z) = (x, (y, z))