Почему эта простая композиция не работает? - PullRequest
0 голосов
/ 24 сентября 2018

Недавно мне нужно было поместить head между двумя монадическими операциями.Вот SSCCE:

module Main where

f :: IO [Int]
f = return [1..5]

g :: Int -> IO ()
g = print

main = do
    putStrLn "g <$> head <$> f"
    g <$> head <$> f

    putStrLn "g . head <$> f"
    g . head <$> f

    putStrLn "head <$> f >>= g"
    head <$> f >>= g

Эта программа правильно сформирована и компилируется без предупреждений.Тем не менее, только одна версия из 3 выше работает 1 .Почему это так?

И, в частности, как лучше всего связать f и g вместе с head в середине?Я закончил тем, что использовал 3-й (в форме do нотации), но мне это не очень нравится, поскольку это должен быть тривиальный однострочный 2 .


1 Оповещение о спойлере: только третий печатает 1;два других молчат, оба под runhaskell и repl.

2 Я действительно понимаю, что все это однострочники, но порядок операций кажется действительно запутанным вработает только один.

Ответы [ 2 ]

0 голосов
/ 24 сентября 2018

Вероятно, лучший способ написать это:

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.

0 голосов
/ 24 сентября 2018

Первое и второе точно совпадают, потому что <$> является левоассоциативным, а head является функцией, а <$> равно . в монаде функции.Тогда

    g . head <$> f

    =  fmap (print . head) (return [1..5] :: IO [Int])
    =  do { x <- (return [1..5] :: IO [Int])
          ; return ( print (head x) ) }
    =  do { let x = [1..5] 
          ; return ( print (head x) ) } :: IO _whatever
    =       
            return ( print 1 )   :: IO (IO ())

У нас там слишком много return с.Фактически,

    =  fmap (print . head) (return [1..5] :: IO [Int])
    =  return (print (head [1..5]))
    =  return (print 1)

является более коротким производным.

Третий -

    (head <$> f) >>= g
  = (fmap head $ return [1..5]) >>= print
  = (return (head [1..5])) >>= print
  = (return 1) >>= print

, что, очевидно, в порядке.

...