函数式编程课程练习中的 FileIO.hs 中的一行
getFile :: FilePath -> IO (FilePath, Chars)
getFile = lift2 (<$>) (,) readFile
根据其类型签名,getFile
返回IO ( FilePath, Chars)
,这意味着文件名及其内容的元组。
但我就是想不通为什么会变成这样。
为什么FilePath
左边没有变化,而readFile
文件名填在右边?
(,)
也是应用实例吗?(,)
不是IO
,那么lift2
举起了什么?
而且,有没有办法派生这些类型签名并得到证明?
我知道的语法是,一个函数后面跟着它的参数,它吃掉右手的一个参数,成为一个新函数。但是当涉及到这样的代码时,它对我来说就像一个魔方......
谢谢你帮助我!
附言:额外信息如下
instance Functor IO where
(<$>) =
P.fmap
lift2 ::
Applicative f =>
(a -> b -> c)
-> f a
-> f b
-> f c
lift2 f a b =
f <$> a <*> b
getFiles :: List FilePath -> IO (List (FilePath, Chars))
getFiles = sequence . (<$>) getFile
让我们看看
lift2 (<$>) (,) readFile
这确实是一个简单的函数应用程序:
((lift2 (<$>)) (,)) readFile
(或应用于三个参数lift2
)。
所涉及的类型(使用唯一重命名的类型变量以减少混淆)是:
lift2 :: (Applicative f) => (a -> b -> c) -> f a -> f b -> f c
(<$>) :: (Functor g) => (j -> k) -> g j -> g k
(,) :: m -> n -> (m, n)
readFile :: FilePath -> IO Chars
我们表达式中的第一件事是将lift2
应用于(<$>)
。这意味着我们需要以某种方式统一a -> b -> c
(lift2
的第一个参数的类型)和(Functor g) => (j -> k) -> g j -> g k
(<$>
的类型)。
那是:
a -> b -> c = (j -> k) -> g j -> g k
-- where g is a Functor
a = j -> k
b = g j
c = g k
这就行了。结果类型为
f a -> f b -> f c
-- where f is an Applicative
这是
f (j -> k) -> f (g j) -> f (g k)
现在这个表达式(lift2 (<$>)
)应用于(,)
。同样,我们必须使类型对齐:
f (j -> k) = m -> n -> (m, n)
在这里,我们利用->
是右关联的属性(即a -> b -> c
表示a -> (b -> c)
),我们可以在类型中使用(柯里)前缀表示法(即a -> b
与(->) a b
相同,与((->) a) b
相同)。
f (j -> k) = ((->) m) (n -> (m, n))
f = (->) m
j = n
k = (m, n)
这也行得通。结果类型为
f (g j) -> f (g k)
其中(替换后)成为
((->) m) (g n) -> ((->) m) (g (m, n))
(m -> g n) -> (m -> g (m, n))
这个表达式(lift2 (<$>) (,)
)适用于readFile
。再次,使类型对齐:
m -> g n = FilePath -> IO Chars
m = FilePath
g = IO
n = Chars
并替换为结果类型:
m -> g (m, n)
FilePath -> IO (FilePath, Chars)
这是整个lift2 (<$>) (,) readFile
表达式的类型。正如预期的那样,它与getFile :: FilePath -> IO (FilePath, Chars)
的声明相匹配。
但是,我们仍然需要验证我们的类约束(Functor g
,Applicative f
)是否已解决。
g
是IO
,这确实是一个Functor
(以及Applicative
和Monad
)。这里没有什么大的惊喜。
f
更有趣:f = (->) m
,所以我们需要为(->) m
寻找一个Applicative
实例。这样的实例确实存在,它的定义包含了getFile
实际做什么的答案。
我们可以通过查看lift2
的类型(如getFile
中使用的)来推导出实例必须是什么样子:
lift2 :: (Applicative f) => (a -> b -> c) -> f a -> f b -> f c
lift2 :: (a -> b -> c) -> ((->) m) a -> ((->) m) b -> ((->) m) c
lift2 :: (a -> b -> c) -> (m -> a) -> (m -> b) -> (m -> c)
lift2 :: (a -> b -> c) -> (m -> a) -> (m -> b) -> m -> c
即lift2
需要
- 将
a
和b
组合成c
的函数, - 将
m
转换为a
的函数, - 将
m
转换为b
的函数, - 和
m
,
并产生c
.
它能做到这一点的唯一方法是将m
传递到第二个和第三个函数中,并使用第一个函数组合它们的结果:
lift2 f g h x = f (g x) (h x)
如果我们在getFile
中内联这个定义,我们得到
getFile = lift2 (<$>) (,) readFile
getFile = x -> (<$>) ((,) x) (readFile x)
getFile = x -> (,) x <$> readFile x
读者心目:
lift2
实际上是根据<$>
和<*>
来定义的。(->) m
Applicative
实例中的<$>
和<*>
有哪些类型?他们的定义必须是什么样子的?