Applicative の値に関数を適用したいとき、liftA2 関数を用いると演算子 <$> を使用するより計算コストを抑えられることがあります。
意味的には liftA2 f x y = f <$> x <*> y です (※定義ではなく、実際の計算方法は異なります) 。
参考「liftA2 - Control.Applicative」
参考「[Haskell] モナド 演算子 まとめ - Qiita」
1. liftA2 関数および liftA3 関数
ここでは説明のため各演算子および関数の型クラス制約を略しますが、型変数 f の型は Functor および Applicative クラスのインスタンスです。
liftA2 関数および liftA3 関数の型は以下のようになっています。
-- Functor (比較用)
(<$>) :: (a -> b ) -> f a -> f b
-- Applicative
liftA2 :: (a -> b -> c ) -> f a -> f b -> f c
liftA3 :: (a -> b -> c -> d) -> f a -> f b -> f c -> f d
liftA3 関数の定義は以下のようになっています。
※説明のためソースコードを一部改変。
liftA3 f x y z = liftA2 f x y <*> z
参考「liftA3 - Control.Applicative」
1.1. 使い方
インスタンス Applicative f に関して、liftA2 関数は a -> b -> c 型の関数から f a -> f b -> f c 型の関数へ変換します。
liftA3 関数は a -> b -> c -> d 型の関数から f a -> f b -> f c - f d 型の関数へ変換します。
さらに引数が多い場合は演算子 <*> を用います。
import Control.Applicative (liftA2, liftA3)
f :: Int -> Int -> Int
f = (+)
g :: Int -> Int -> Int -> Int
g x y z = x + y + z
h :: Int -> Int -> Int -> Int -> Int
h x y z w = x + y + z + w
main :: IO ()
main = do
let x = Just 10 :: Maybe Int
let y = Just 20 :: Maybe Int
let z = Just 30 :: Maybe Int
let w = Just 40 :: Maybe Int
--
print (f <$> x <*> y :: Maybe Int)
print (liftA2 f x y :: Maybe Int)
--
print (g <$> x <*> y <*> z :: Maybe Int)
print (liftA3 g x y z :: Maybe Int)
print (liftA2 g x y <*> z :: Maybe Int)
--
print (h <$> x <*> y <*> z <*> w :: Maybe Int)
print (liftA3 h x y z <*> w :: Maybe Int)
print (liftA2 h x y <*> z <*> w :: Maybe Int)
1.2. 計算コスト
1.2.1. Maybe 型の場合
Maybe 型における演算子 <$> および <*> の定義は以下のようになっています。
※説明のためソースコードを一部改変。
fmap _ Nothing = Nothing
fmap f (Just x) = Just (f x)
(<$>) = fmap
Just f <*> x = fmap f x
Nothing <*> _ = Nothing
よって、Maybe 型における f <$> x <*> y は以下のような計算を行います:
- 値
xがJustかNothingか判断して関数fを適用する - 関数
f <$> xがJustかNothingか判断して値yに適用する - 値
yがJustかNothingか判断して関数f <$> xを適用する
一方で、Maybe 型における liftA2 関数の定義は以下のようになっています。
liftA2 f (Just x) (Just y) = Just (f x y)
liftA2 _ _ _ = Nothing
よって、Maybe 型における liftA2 f x y は以下のような計算を行います:
-
xがJustかNothingか判断して関数fを適用する -
yがJustかNothingか判断して関数liftA2 f xを適用する
上記のことから f <$> x <*> y より liftA2 f x y の方が計算量が減っていることが分かります。
1.2.2. ZipList 型の場合
ZipList 型における演算子 <$> および <*> の定義は以下のようになっています。
※説明のためソースコードを一部改変。
-- List
fmap = map
-- ZipList
newtype ZipList a = ZipList { getZipList :: [a] }
deriving Functor
fmap f (ZipList xs) = ZipList (fmap f xs)
(<$>) = fmap
id x = x
-- List
zipWith f = go
where
go [] _ = []
go _ [] = []
go (x:xs) (y:ys) = f x y : go xs ys
-- ZipList
liftA2 f (ZipList xs) (ZipList ys) = ZipList (zipWith f xs ys)
(<*>) = liftA2 id
演算子 <*> の定義を整理すると以下のようになります。
id x = x
-- List
zipWith f = go
where
go [] _ = []
go _ [] = []
go (x:xs) (y:ys) = f x y : go xs ys
-- ZipList
(<*>) (ZipList fs) (ZipList xs) = ZipList (zipWith id fs xs)
よって、ZipList 型における f <$> xs <*> ys は以下のような計算を行います:
-
xsの各要素の値に関数fを適用する -
ysの各要素の値にf <$> xsの各要素の関数を適用する
一方で、ZipList 型における liftA2 関数の定義は以下のようになっています。
id x = x
-- List
zipWith f = go
where
go [] _ = []
go _ [] = []
go (x:xs) (y:ys) = f x y : go xs ys
-- ZipList
liftA2 f (ZipList xs) (ZipList ys) = ZipList (zipWith f xs ys)
よって、ZipList 型における liftA2 f xs ys は以下のような計算を行います:
-
xsとysそれぞれの各要素の値に関数fを適用する
上記のことから f <$> xs <*> ys より liftA2 f xs ys の方が計算量が減っていることが分かります。
参考「ZipList - Control.Applicative」
2.liftM2 関数等
liftM2 関数は liftA2 関数と計算結果が同じになりますが、実際の計算方法は異なります。
インスタンス Monad m に関して、liftM2 関数は a -> b -> c 型の関数から m a -> m b -> m c 型の関数へ変換します。
※ liftM3, liftM4 および liftM5 関数も同様。
参考「Monadic lifting operators - Control.Monad」
2.1. liftM2 関数の定義
liftM2 関数の定義は以下のようになっています。
※説明のためソースコードを一部改変。
liftM2 f x y = do
x' <- x
y' <- y
pure $ f x' y'
liftM2 関数の定義を整理すると以下のようになります。
liftM2 f x y = x >>= \x' -> y >>= \y' -> pure $ f x' y'
2.2. 計算コスト
Maybe 型における演算子 >>= の定義は以下のようになっています。
※説明のためソースコードを一部改変。
(Just x) >>= f = f x
Nothing >>= _ = Nothing
よって、Maybe 型における liftM2 関数は以下のような計算を行います:
- 値
xに関数\x' -> y >>= \y' -> pure $ f x' y'を適用する - 値
yに関数\y' -> pure $ f x' y'を適用する
上記のことから f <$> x <*> y より liftM2 f x y の方が計算量が減る可能性はありますが、liftA2 関数のように各インスタンスごとに定義されるものではないため、計算コストが重要であれば liftM2 関数でなく liftA2 関数を使用します。
計算コストを気にしない、または十分な場合に liftA2 = liftM2 のように liftA2 関数の定義に用いられることがあります。
参考「Maybe - Data.Maybe」
参考「liftM2 - Control.Monad」
3. liftA 関数および liftM 関数
liftA 関数および liftM 関数は (<$>) = fmap と同じ意味です。
fmap 関数は Functor のインスタンスごとに最適な定義がされているはずのため、liftA 関数および liftM 関数は不要です。
fmap = liftA のように fmap 関数の定義に用いられることがあります。
参考「liftA - Control.Applicative」
参考「liftM - Control.Monad」
4. ap 関数
ap 関数は (<*>) と同じ意味です。
演算子 <*> は Applicative のインスタンスごとに最適な定義がされているはずのため、ap 関数は不要です。
(<*>) = ap のように演算子 <*> の定義に用いられることがあります。
5. まとめ
モナドの値に関数を適用できるように変換する関数は以下のようなものがありますが、重要なのは liftA2 関数です:
-
Applicative-
liftA,liftA2,liftA3
-
-
Monad-
liftM,liftM2,liftM3,liftM4,liftM5 ap
-