Вероятно, лучший способ написать это:
f >>= g . head
или в более подробной форме:
f >>= (g . head)
, поэтому мы в основном выполняем fmap
для значения для f
(таким образом, мы берем head
значений, заключенных в монаду IO
), а затем переходим затем к g
, например:
(head <$> f) >>= g
семантически одинаков.
Но что теперь произойдет, если мы используем g <$> head <$> f
?Давайте сначала проанализируем типы:
f :: IO [Int]
g :: Int -> IO ()
(<$>) :: Functor m => (a -> b) -> m a -> m b
(я использовал m
здесь, чтобы избежать путаницы с функцией f
)
Каноническая форма этого:
((<$>) ((<$>) g head) f)
Второй (<$>)
принимает g :: Int -> IO ()
и head :: [c] -> c
в качестве параметров, что означает, что a ~ Int
, b ~ IO ()
и m ~ (->) [c]
.Таким образом, результат:
(<$>) g head :: (->) [c] (IO ())
или менее подробный:
g <$> head :: [c] -> IO ()
Первая функция (<$>)
, таким образом, принимает в качестве параметров g <$> head :: [c] -> IO ()
и IO [Int]
, так что это означает, чтоm ~ IO
, a ~ [Int]
, c ~ Int
, b ~ IO ()
, и, следовательно, мы получаем тип:
(<$>) (g <$> head) f :: IO (IO ())
Таким образом, мы не выполняем никаких реальных действий: мы fmap
список [Int]
на действие IO
(заключенное в IO
).Вы можете видеть это как return (print 1)
: вы не «оцениваете» print 1
, но вы return
завернули в IO
.
Вы, конечно, можете «поглотить» внешнее IO
здесь, а затем используйте внутренний IO
, например:
evalIO :: IO (IO f) -> IO f
evalIO res = do
f <- res
f
или короче:
evalIO :: IO (IO f) -> IO f
evalIO res = res >>= id
(это можно обобщить на все виды Monad
с, ноздесь это не имеет значения).
evalIO
также известен как join :: Monad m => m (m a) -> m a
.