5

我无法理解这个 Haskell 表达式的工作原理:

import Control.Monad
import System.IO
(forM_ [stdout, stderr] . flip hPutStrLn) "hello world"

. flip hPutStrLn部分到底在做什么?类型签名看起来很复杂:

ghci> :type flip
flip :: (a -> b -> c) -> b -> a -> c
ghci> :type (.)
(.) :: (b -> c) -> (a -> b) -> a -> c
ghci> :type (. flip)
(. flip) :: ((b -> a -> c1) -> c) -> (a -> b -> c1) -> c
ghci> :type (. flip hPutStrLn)
(. flip hPutStrLn) :: ((Handle -> IO ()) -> c) -> String -> c

(.)计算表达式时,运算符的左操作数和右操作数是什么?

提出我的问题的另一种方法是,顶部的表达式的左侧部分如何以这样的类型签名结束:

(forM_ [stdout, stderr] . flip hPutStrLn) :: String -> IO ()
4

3 回答 3

13

的左右操作数(.)

forM_ [stdout, stderr]

flip hPutStrLn

分别。

的类型hPutStrLn

hPutStrLn :: Handle -> String -> IO ()

flip hPutStrLn类型也是如此

flip hPutStrLn :: String -> Handle -> IO ()

正如类型系统告诉你的那样,flip它是一个交换另一个函数参数的顺序的组合子。在摘要中指定

flip       :: (a -> b -> c) -> b -> a -> c
flip f x y =  f y x

ghci你已经知道的类型(. flip hPutStrLn)

ghci> :type (. flip hPutStrLn)
(. flip hPutStrLn) :: ((Handle -> IO ()) -> c) -> String -> c

从另一个方向工作,左侧的类型是

ghci> :type forM_ [stdout, stderr]
forM_ [stdout, stderr] :: Monad m => (Handle -> m b) -> m ()

观察这些类型是如何组合在一起的。

(. flip hPutStrLn)     ::            ((Handle -> IO ()) -> c   ) -> String -> c
forM_ [stdout, stderr] :: Monad m =>  (Handle -> m  b ) -> m ()

结合两者(称第一个和第二个)给出

ghci> :type forM_ [stdout, stderr] . flip hPutStrLn
forM_ [stdout, stderr] . flip hPutStrLn :: String -> IO ()

在您的问题中,组合的结果应用于 a String,并产生一个产生的 I/O 操作(),我们主要对写入标准输出和错误流的副作用感兴趣。

使用无点样式(例如您问题中的定义),程序员通过将它们组合成更小、更简单的函数来定义更复杂的函数(.)。组合器对于重新排序参数很有用,flip以便使重复的部分应用程序适合在一起。

于 2013-02-27T16:16:46.230 回答
7

flip反转输入函数的参数,即:

flip hPutStrLn == \a b -> hPutStrLn b a

.是一个函数组合运算符(或中缀函数),它可以让你很好地将函数链接在一起。如果没有此运算符,您的表达式可以重写如下:

forM_ [stdout, stderr] ((flip hPutStrLn) "hello world")

这与以下内容相同:

forM_ [stdout, stderr] (flip hPutStrLn "hello world")

或者,使用应用程序运算符:

forM_ [stdout, stderr] $ flip hPutStrLn "hello world"

关于.操作数问题。考虑 的类型签名.

(.) :: (b -> c) -> (a -> b) -> a -> c

您可以将其视为来自 3 个参数的函数:一个函数b -> c、一个函数a -> b和一个值a- 到一个结果值c,而且由于Currying,您可以将它视为一个来自两个参数的函数:b -> ca -> b- 到一个类型的结果函数a -> c. 这就是您的示例中发生的情况:您将两个函数(forM_ [stdout, stderr]flip hPutStrLn,它们本身是柯里化的结果)传递给.并得到一个类型的函数String -> IO ()作为结果。

于 2013-02-27T15:38:34.227 回答
2

这是该类型的一个较短的推导(如 Nikita Volkov 的回答的第二部分所暗示的)。

知道(.) :: (b -> c) -> (a -> b) -> a -> c(f . g) x = f (g x),所以

(f . g) :: a -> c      where  g :: (a -> b)  and  f :: (b -> c)

bina -> b和在执行统一b -> c后消失,给出类型)并且因为a -> c

flip hPutStrLn         ::    String -> (Handle -> IO ())             -- g
forM_ [stdout, stderr] :: (Monad m) => (Handle -> m  b ) -> m ()     -- f

(我们在第一种类型中加上括号,使用类型是右结合Handle -> IO ()的事实->),将第二种与第一种组合的结果类型(通过函数组合运算符)是

(Monad m) => String -> m ()     where  m ~ IO  and b ~ ()
                                (found by unification of
                                        Handle -> IO ()  and
                                        Handle -> m  b       )

String -> IO ()

参数的顺序(.)需要一点时间来适应;它首先启动它的第二个参数函数,然后使用结果调用它的第一个参数函数。如果我们导入Control.Arrow,我们就可以使用>>>运算符,就像(.)相反的那样,带有函数:(f . g) x == (g >>> f) x

于 2013-02-28T18:53:10.603 回答