常用函数

join

join 函数通常用于处理嵌套的 Monad 结构,例如 Maybe (Maybe a) 或 IO (IO a)。它将一个嵌套的 Monad “拉平” 成一个单层 Monad,类型签名如下:

1
join :: Monad m => m (m a) -> m a

在 Maybe 中使用 join

1
2
3
4
5
6
7
import Control.Monad (join)

example1 :: Maybe Int
example1 = join (Just (Just 5)) -- 结果为 Just 5

example2 :: Maybe Int
example2 = join (Just Nothing) -- 结果为 Nothing

在 List 中使用 join

1
2
example3 :: [Int]
example3 = join [[1, 2], [3, 4], [5]] -- 结果为 [1, 2, 3, 4, 5]

在 IO 中使用 join

1
2
example4 :: IO ()
example4 = join (putStrLn <$> getLine) -- 用户输入内容后将被打印出来

这里 getLine 的类型为 IO String,putStrLn <$> getLine 的类型为 IO (IO ()),使用 join 可以得到 IO () 类型,从而直接执行打印操作。

traverse

traverse 函数的作用是将一个函数应用于数据结构中的每个元素,并返回一个包含应用结果的新的数据结构。该函数的签名如下:

1
traverse :: Traversable t => (a -> f b) -> t a -> f (t b)

以下是列表的Traversable的实现

1
2
3
instance Traversable [] where
traverse :: Applicative f => (a -> f b) -> [a] -> f [b]
traverse f = foldr (\x ys -> (:) <$> f x <*> ys) (pure [])

(:) <$> f x <*> ys的解释如下

  • f参数:(a -> f b)
  • f x:x是[a]的一个元素,所以f x生成f b
  • ys:是f [b]
  • (:) <$> f x:表示(:)应用于f内部的x
  • (:) <$> f x <*> ys:的意思就是(f (x :))里面的x :应用于ys的内部也就是[b]

容易误解的地方有两个

  • 一个是函数声明中的f和函数体的参数f不是同一个f,容易弄混了,函数声明中的f是一个Applicative,而函数体的参数的f是一个(a -> f b)的函数。
  • 另外一个是(:) <$> f x <*> ys容易误解为f x : ys也可以,后面的写法是将f x直接放入ys中,而第一种写法是将f xx放到ys等同于f [b]中的[b]。其实当你看到<$>和<*>同时使用的时候就可以想到lift提升了,(:)的签名是a -> [a] -> [a],提升后的签名是f a -> f [a] -> f [a]

例子 1: 把偶数转换为字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Data.Traversable (traverse)

-- 定义一个函数,只转换偶数,奇数会返回 Nothing
evenToString :: Int -> Maybe String
evenToString x = if even x then Just (show x) else Nothing

-- 用 traverse 把这个函数应用到一个列表里
result :: Maybe [String]
result = traverse evenToString [2, 4, 6] -- 每个元素都是偶数
-- result 的值是 Just ["2", "4", "6"]

result2 :: Maybe [String]
result2 = traverse evenToString [2, 3, 6] -- 包含奇数,返回 Nothing
-- result2 的值是 Nothing

例子 2: 和 IO 一起使用

1
2
3
4
5
6
7
8
9
import System.IO (readFile)

-- 用 traverse 读取多个文件的内容
readFiles :: [FilePath] -> IO [String]
readFiles paths = traverse readFile paths

-- 使用 readFiles 读取多个文件
result3 :: IO [String]
result3 <- readFiles ["file1.txt", "file2.txt", "file3.txt"]

总结

traverse 的应用场景通常是:

你有一个数据结构(比如列表、树、Maybe 等)。
你想对每个元素执行一个带有副作用的操作,比如返回 Maybe、IO 或者其他 Applicative 类型。
traverse 帮助你保持原数据结构的形状,同时把每个操作的结果组合成一个整体的结果。