Haskell #15: Newtype

Chương 16. Monoid

16.1. Dẫn nhập

Trong Haskell, các typeclass trừu tượng (nhận tham số kiểu) là interface cho các kiểu có chung một hành vi nào đó. Những typeclass đơn giản như Eq, là kiểu có thể so sánh ngang bằng, và Ord dành cho kiểu có thể sắp xếp; tiếp theo là đến những lớp thú vị hơn như Functor và Applicative.

Khi tạo một kiểu, ta nghĩ về những hành vi mà kiểu dữ liệu này cho phép rồi dựa vào đó để quyết định xem sẽ đặt kiểu dữ liệu này vào typeclass. Giả dụ kiểu đang xét so sánh ngang bằng được, ta sẽ kế thừa kiểu này từ Eq.

16.2. Monoid

Hãy xét vấn đề sau: * là hàm nhận vào hai số rồi trả về tích của chúng. Nếu ta nhân một số với phần tử trung hòa $1$, kết quả sẽ luôn là số ban đầu, $1 * x = x * 1 = x$. Tương tự, ++ cũng là hàm nhận hai list rồi nối chúng lại. Và rất giống với *, nó cũng có một phần tử trung hòa là list rỗng [].

ghci> 4 * 1
4
ghci> 1 * 9
9
ghci> [1,2,3] ++ []
[1,2,3]

* và $1$, ++[] đều có chung một số thuộc tính:

  1. Hàm nhận vào hai tham số.
  2. Tham số và giá trị trả về cùng kiểu.
  3. Có phần tử trung hòa.

Còn một điểm chung nữa mà có thể không dễ thấy, đó là khi ta có nhiều hơn $2$ giá trị và muốn dùng toán tử hai ngôi để tính toán và rút gọn thì thứ tự sẽ không quan trọng. Như $(3 * 4) * 5$ và $3 * (4 * 5)$ sẽ không khác nhau, kết quả luôn là $60$. Tương tự đối với ++:

ghci> "la" ++ ("di" ++ "da")
"ladida"
ghci> ("la" ++ "di") ++ "da"
"ladida"

Ta gọi tính chất này là kết hợp. * và ++ có tính kết hợp, nhưng $-$, chẳng hạn, thì không. Hai biểu thức $(5 - 3) - 4$ và $5 - (3 - 4)$ cho kết quả khác nhau.

Qua những thuộc tính này, chúng ta sẽ làm quen với khái niệm Monoid. Monoid xuất hiện khi bạn có một hàm nhị phân, có tính kết hợp, và một phần tử trung hòa. Phần tử trung hoà là khi ta gọi hàm đó với phần tử này cùng một giá trị nào đó khác, kết quả luôn là giá trị kia. Trong Haskell tồn tại lớp Monoid dành cho những kiểu dữ liệu có những tính chất của Monoid. Lớp này được định nghĩa như sau:

class Monoid m where
    mempty :: m
    mappend :: m -> m -> m
    mconcat :: [m] -> m
    mconcat = foldr mappend mempty

Trước hết, ta thấy rằng chỉ những kiểu cụ thể mới có thể trở thành instance của Monoid, vì m trong không nhận tham số kiểu. Điều này khác với Functor và Applicative, những lớp này yêu cầu instance của chúng phải là constructor kiểu và nhận vào một tham số.

Hàm thứ nhất là mempty. Nó là hàm hằng, biểu diễn phần tử trung hoà của Monoid.

Tiếp theo, ta có mappend là hàm hai ngôi. Nó nhận vào hai giá trị cùng kiểu rồi trả về một giá trị cũng cùng kiểu đó.

Hàm sau cùng là mconcat. Nó nhận vào một list các giá trị Monoid rồi rút gọn chúng về một giá trị bằng cách dùng mappend. Chúng ta có thể thấy thiết lập mặc định, trong đó giá trị khởi tạo là mempty rồi foldr bằng mappend.

mconcat hiệu quả với hầu hết các Monoid nên chúng ta chỉ cần quan tâm đến memptymappend.

Trước khi xét đến một số Monoid cụ thể, ta hãy điểm qua các định luật Monoid. Ta đã đề cập rằng phải có một giá trị đóng vai trò phần tử trung hoà ứng với hàm hai ngôi và hàm hai ngôi này phải có tính kết hợp:

mempty `mappend` x = x
x `mappend` mempty = x
(x `mappend` y) `mappend` z = x `mappend` (y
`mappend` z)

Hai định luật đầu phát biểu rằng mempty phải đóng vai trò của phần tử trung hoà trong mappend và định luật thứ ba quy định rằng thứ tự mà ta dùng mappend để rút gọn nhiều giá trị Monoid không quan trọng. Haskell không áp đặt các định luật này, vì vậy chúng ta phải cẩn thận đảm bảo rằng những instance phải tuân theo các định luật này.

16.3. List

Đúng vậy, list là Monoid. Như đã thấy, hàm ++[] hình thành một Monoid. Instance này rất đơn giản:

instance Monoid [a] where
    mempty = []
    mappend = (++)

list là một instance của lớp Monoid bất kể kiểu các phần tử chứa trong đó có là gì đi nữa. Lưu ý rằng ta Monoid yêu cầu [a] chứ không phải [], vì nó yêu cầu instance phải có kiểu cụ thể.

ghci> [1,2,3] `mappend` [4,5,6]
[1,2,3,4,5,6]
ghci> ("one" `mappend` "two") `mappend` "tree"
"onetwotree"
ghci> "one" `mappend` ("two" `mappend` "tree")
"onetwotree"
ghci> "pang" `mappend` mempty
"pang"
ghci> mconcat [[1,2],[3,6],[9]]
[1,2,3,6,9]
ghci> mempty :: [a]
[]

Lưu ý rằng ở dòng cuối cùng, ta đã phải viết một chú thích vì chỉ mempty thì GHCi sẽ không biết phải dùng instance nào. Ta có thể dùng kiểu tổng quát [a] (thay vì viết rõ là [Int] hay [String]) vì list rỗng không quan tâm đến kiểu cụ thể bên trong.

Với trường hợp list, mconcat chính là concat. Nó nhận một list chứa các list rồi duỗi thẳng list lớn, tương đương với ++ giữa tất cả các phần tử liền kề trong một list.

Định luật Monoid thật ra không đúng cho list. Khi ta có nhiều list và thực hiện mappend (hoặc ++) thì thứ tự list không ảnh hưởng vì đằng nào chúng cũng được nối lại. Đồng thời, list rỗng đóng vai trò phần tử trung hòa nên mọi thứ đều ổn. Lưu ý rằng các Monoid không yêu cầu a `mappend` b phải bằng b `mappend` a. Trong trường hợp với list, chúng rõ ràng là không như nhau:

ghci> "one" `mappend` "two"
"onetwo"
ghci> "two" `mappend` "one"
"twoone"

Việc các phép nhân $3 * 5$ và $5 * 3$ cho kết quả như nhau chỉ là một thuộc tính của phép nhân, nhưng không dùng được cho hầu hết các Monoid.

16.4. Product & Sum

Một cách để tạo ra Monoid là sử dụng hàm hai ngôi $+$ và phần tử trung hòa $0$:

ghci> 0 + 4
4
ghci> (1 + 3) + 5
9
ghci> 1 + (3 + 5)
9

Định luật Monoid được thỏa mãn, bởi nếu cộng 0 vào số bất kì thì kết quả chính là số đó, phép cộng cũng có tính kết hợp vì vậy ta không gặp rắc rối gì ở đây.

Hãy nhớ rằng đã có vài cách để một kiểu trở thành instance của lớp, ta có thể gói kiểu đó vào trong một newtype và kiểu mới trở thành instance của typeclass theo một cách khác.

Module Data.Monoid export hai kiểu dữ liệu, là ProductSum. Product được định nghĩa như sau:

newtype Product a =  Product { 
	getProduct :: a 
} deriving (Eq, Ord, Read, Show, Bounded)

Đơn giản, chỉ là một gói bọc newtype cùng một tham số kiểu. instance đối với Monoid có dạng như sau:

instance Num a => Monoid (Product a) where
    mempty = Product 1
    Product x `mappend` Product y = Product (x * y)

mempty chỉ là giá trị $1$ được gói trong constructor Product. mappend nhân hai số lại với nhau rồi bọc kết quả lại. Có một ràng buộc lớp Num a, Product a là instance của Monoid với mọi a (với a là instance của Num):

ghci> getProduct $ Product 3 `mappend` Product 9
27
ghci> getProduct $ Product 3 `mappend` mempty
3
ghci> getProduct $ Product 3 `mappend` Product 4 `mappend` Product 2
24
ghci> getProduct . mconcat . map Product $ [3,4,2]
24

Sum được định nghĩa tương tự như Product:

ghci> getSum $ Sum 2 `mappend` Sum 9
11
ghci> getSum $ mempty `mappend` Sum 3
3
ghci> getSum . mconcat . map Sum $ [1,2,3]
6

16.5. Any & All

Một kiểu khác có thể đóng vai trò như Monoid theo hai cách riêng biệt nhưng tương đương và đều hợp lệ, đó là kiểu Bool. Cách thứ nhất là dùng hàm $or$ || đóng vai trò hàm hai ngôi và False là phần tử trung hòa. $or$ nếu nhận ít nhất một giá trị True thì sẽ trả về True, còn không thì trả về False. Vì vậy, nếu ta dùng False làm phần tử trung hòa thì nó sẽ trả về False khi được || với FalseTrue khi được || với True.

constructor newtype có tên Any là instance của Monoid theo cách này. Constructor này được định nghĩa như sau:

newtype Any = Any { 
	getAny :: Bool 
} deriving (Eq, Ord, Read, Show, Bounded)

Và instance của nó:

instance Monoid Any where
    mempty = Any False
    Any x `mappend` Any y = Any (x || y)
ghci> getAny $ Any True `mappend` Any False
True
ghci> getAny $ mempty `mappend` Any True
True
ghci> getAny . mconcat . map Any $ [False, False, False, True]
True
ghci> getAny $ mempty `mappend` mempty
False

Một cách khác để Bool trở thành instance của Monoid là dùng hàm $and$ ($&&$) làm hàm hai ngôi và True là phần tử trung hòa. && sẽ trả về True chỉ khi cả hai tham số đều là True:

newtype All = All { 
	getAll :: Bool 
} deriving (Eq, Ord, Read, Show, Bounded)
instance Monoid All where
    mempty = All True
    All x `mappend` All y = All (x && y)

Khi ta mappend các giá trị của kiểu All thì kết quả sẽ là True chỉ khi tất cả các giá trị là True:

ghci> getAll $ mempty `mappend` All True
True
ghci> getAll $ mempty `mappend` All False
False
ghci> getAll . mconcat . map All $ [True, True, True]
True
ghci> getAll . mconcat . map All $ [True, True, False]
False

16.6. Monoid Ordering

Ordering được dùng khi ta so sánh các thứ với nhau và nó có thể mang ba giá trị: LT, EQGT. Đối với list, giá trị số hoặc giá trị boole thì việc tìm ra Monoid chỉ đơn giản là nhìn vào những hàm thông dụng và xem liệu chúng có biểu hiện gì như Monoid không. Đối với Ordering, ta phải nhìn kĩ hơn một chút mới phát hiện ra Monoid, nhưng hóa ra là instance Monoid của nó cũng trực quan:

instance Monoid Ordering where
    mempty = EQ
    LT `mappend` _ = LT
    EQ `mappend` y = y
    GT `mappend` _ = GT

Khi ta mappend hai giá trị Ordering, thì vế trái được giữ lại, trừ khi giá trị bên trái là EQ, lúc này thì giá trị bên phải sẽ là kết quả. phần tử trung hòa là EQ. Thoạt đầu, dường như cách định nghĩa này có vẻ tùy tiện, thực ra nó giống với cách mà ta so sánh các từ theo bảng chữ cái. Ta so sánh hai chữ cái đầu tiên và nếu chúng khác nhau thì sẽ biết được ngay rằng từ nào sẽ đứng trước trong từ điển. Tuy nhiên, nếu hai chữ cái đầu tiên bằng nhau, thì ta chuyển đến cặp chữ cái tiếp theo rồi lặp lại quá trình.

Chẳng hạn, nếu ta phải dựa vào bảng chữ cái để so sánh hai từ “ox” và “on”, trước hết ta so sánh hai chữ cái đứng đầu mỗi từ, để thấy được chúng bằng nhau và tiếp đến là so sánh các chữ cái thứ hai. Ta thấy rằng trong bảng chữ cái ‘x’ lớn hơn (tức là đứng sau) ‘n’, việc so sánh hai từ đã rõ. EQ là phần tử trung hòa vì dễ thấy rằng nếu phải nhồi cùng một chữ cái vào trong cùng vị trí ở cả hai từ thì sẽ chẳng làm thay đổi thứ tự sắp xếp của hai từ đó. “oix” vẫn lớn hơn (xếp sau) “oin”.

Cần lưu ý rằng trong instance Monoid với Ordering, x `mappend` y khác y `mappend` x. Vì tham số thứ nhất được giữ nguyên trừ khi nó là EQ, LT `mappend` GT sẽ cho kết quả là LT, còn GT `mappend` LT sẽ cho kết quả GT:

ghci> LT `mappend` GT
LT
ghci> GT `mappend` LT
GT
ghci> mempty `mappend` LT
LT
ghci> mempty `mappend` GT
GT

Monoid có lợi ích gì? Giả sử bạn phải viết một hàm nhận vào hai chuỗi, so sánh chiều dài của chúng, rồi trả về Ordering. Nhưng nếu chuỗi có cùng độ dài thì phải so sánh thứ tự xếp theo bảng chữ cái. Một cách làm sẽ là như sau:

lengthCompare :: String -> String -> Ordering
lengthCompare x y = let a = length x `compare` length y 
                        b = x `compare` y
                    in if a == EQ then b else a

Nhưng nếu biết Ordering là một Monoid thì hàm này có thể viết đơn giản hơn:

import Data.Monoid

lengthCompare :: String -> String -> Ordering 
lengthCompare x y = (length x `compare` length y) `mappend` (x `compare` y)
ghci> lengthCompare "zen" "ants"
LT
ghci> lengthCompare "zen" "ant"
GT

Hãy nhớ rằng, khi dùng mappend, tham số vế trái của nó luôn được giữ lại trừ khi tham số này là EQ. Điều này lý giải tại sao ta lại đặt phép so sánh mà ta xét trước tiên, cái tiêu chuẩn quan trọng hơn, vào vị trí tham số vế trái. Nếu ta muốn mở rộng hàm này để so sánh với số lượng nguyên âm và coi đây là tiêu chí quan trọng thứ hai để so sánh, thì có thể sửa hàm lại thành như sau:

import Data.Monoid

lengthCompare :: String -> String -> Ordering
lengthCompare x y = (length x `compare` length y) `mappend` (vowels x `compare` vowels y) `mappend` (x `compare` y) where vowels = length . filter (`elem` "aeiou")

Hàm vowels nhận một chuỗi và trả về số lượng nguyên âm.

ghci> lengthCompare "zen" "anna"
LT
ghci> lengthCompare "zen" "ana"
LT
ghci> lengthCompare "zen" "ann"
GT

Ở đây, ta đã thấy được cách Haskell phát hiện hai chiều dài là khác nhau và LT đã được trả về, bởi chiều dài của “zen” thì kém chiều dài của “anna”. Ở ví dụ thứ hai, các chiều dài là bằng nhau, nhưng chuỗi thứ hai có nhiều nguyên âm hơn, vì vậy LT một lần nữa được trả về. Ở ví dụ thứ ba, cả hai chuỗi đều cùng độ dài và cùng số nguyên âm, chúng được so sánh theo thứ tự bảng chứ cái và “zen” thắng cuộc.

Monoid Ordering cho phép ta dễ dàng so sánh theo nhiều tiêu chí khác nhau và đặt các tiêu chí đó theo một trật tự quan trọng nhất đến ít quan trọng.

16.7. Monoid Maybe

Maybe a là một Monoid chỉ khi tham số a của nó cũng là Monoid, mappend được thực thi với các giá trị được gói trong Just. Nothing là phần tử trung hòa và nếu một trong hai giá trị đang được mappendNothing thì ta sẽ giữ giá trị còn lại.

instance Monoid a => Monoid (Maybe a) where
    mempty = Nothing
    Nothing `mappend` m = m
    m `mappend` Nothing = m
    Just m1 `mappend` Just m2 = Just (m1 `mappend` m2)

Hãy chú ý ràng buộc về lớp. Ràng buộc này phát biểu rằng Maybe a là một instance của Monoid chỉ khi a là instance của Monoid. Nếu ta mappend một thứ gì đó với Nothing, thì kết quả sẽ là chính nó. Nếu ta mappend hai giá trị trong Just, thì cả hai giá trị được lấy ra, tính toán và được gói trở lại vào Just. Ta có thể làm được điều này vì ràng buộc về lớp đảm bảo rằng kiểu của thứ bên trong Just là một instance của Monoid.

ghci> Nothing `mappend` Just "andy"
Just "andy"
ghci> Just LT `mappend` Nothing
Just LT
ghci> Just (Sum 3) `mappend` Just (Sum 4)
Just (Sum {getSum = 7})

Cách này phát huy tác dụng khi bạn phải xử lý những Monoid có kết quả tính toán có thể thất bại. Nhờ instance này, ta không cần kiểm tra xem liệu các tính toán có thất bại không bằng cách xem liệu chúng có phải là Nothing hay giá trị Just không; ta chỉ việc tiếp tục coi chúng như những Monoid thông thường.

Nếu kiểu giá trị trong Maybe không phải là instance của Monoid thì ta không thể thực thi mappend giữa chúng, vì vậy ta phải bỏ giá trị thứ hai đi và giữ lại giá trị thứ nhất. Theo đó, kiểu First a có tồn tại và sau đây là lời định nghĩa:

newtype First a = First { 
    getFirst :: Maybe a 
} deriving (Eq, Ord, Read, Show)

Ta lấy Maybe a rồi bọc nó lại bằng newtype. instance Monoid là như sau:

instance Monoid (First a) where
    mempty = First Nothing
    First (Just x) `mappend` _ = First (Just x)
    First Nothing `mappend` x = x

Như ta đã nói, mempty chỉ là Nothing được gói bằng constructor newtype có tên First. Nếu tham số thứ nhất của mappend là giá trị Just thì ta bỏ qua tham số thứ hai. Nếu tham số thứ nhất là Nothing thì ta lấy tham số thứ hai làm kết quả, bất kể nó có là Just hay Nothing:

ghci> getFirst $ First (Just 'a') `mappend` First (Just 'b')
Just 'a'
ghci> getFirst $ First Nothing `mappend` First (Just 'b')
Just 'b'
ghci> getFirst $ First (Just 'a') `mappend` First Nothing
Just 'a'

First có ích khi có một loạt Maybe và muốn biết xem trong số chúng có cái nào là Just không. Hàm mconcat trở nên có ích:

ghci> getFirst . mconcat . map First $ [Nothing, Just 9, Just 10]
Just 9

Nếu ta muốn một Monoid của Maybe a sao cho tham số thứ hai được giữ lại nếu cả hai tham số của mappend đều là giá trị Just, thì Data.Monoid cung cấp kiểu Last a, như First a, chỉ khác là giá trị khác Nothing cuối cùng được giữ lại trong quá trình dùng mappendmconcat:

ghci> getLast . mconcat . map Last $ [Nothing, Just 9, Just 10]
Just 10
ghci> getLast $ Last (Just "one") `mappend` Last (Just "two")
Just "two"

16.8. Cấu trúc dữ liệu fold

Một trong những cách hay hơn để bắt Monoid làm việc là để chúng định nghĩa các phép fold đối với những CTDL khác nhau. Ví dụ như list, cây.

Vì có nhiều CTDL có tính chất trên nên Haskell có tồn tại lớp Foldable. Rất giống với Functor dành cho những CTDL ánh xạ được, Foldable thì dành cho những CTDL có thể fold.

import qualified Foldable as F

Chúng ta sẽ dùng F để rút ngọn quá trình sử dụng lớp Foldable. Như đã giới thiệu trong những chương đầu, trong F tồn tại một số hàm đặc biệt như foldr, foldl, foldr1foldl1. Tuy nhiên các hàm foldr thuộc Foldable và hàm foldr thuộc Prelude có một số điểm khác nhau:

ghci> :t foldr
foldr :: (a -> b -> b) -> b -> [a] -> b
ghci> :t F.foldr
F.foldr :: (F.Foldable t) => (a -> b -> b) -> b -> t a -> b

Như vậy trong khi foldr nhận một list rồi fold nó lại, thì F.foldr chấp nhận bất kể kiểu dữ liệu nào fold được. Như ta dự đoán, cả foldr đều hoạt động như nhau đối với list:

ghci> foldr (*) 1 [1,2,3]
6
ghci> F.foldr (*) 1 [1,2,3]
6

ghci> F.foldl (+) 2 (Just 9)
11
ghci> F.foldr (||) False (Just True)
True

Nhưng thực hiện fold đối với giá trị Maybe không hay vì khi fold, nó giống như list với đúng một phần tử nếu nó là giá trị Just và như list rỗng nếu nó là Nothing. Vì vậy ta hãy xem xét một cấu trúc dữ liệu phức tạp hơn đôi chút.

data Tree a = Empty | Node a (Tree a) (Tree a) deriving (Show, Read, Eq)

Ở chương về Functor, chúng ta có thể biến Tree thành một instance bằng cách định nghĩa hàm fmap. Bây giờ, ta sẽ biến nó thành instance của Foldable bằng cách định nghĩa hàm foldr. Nhưng một cách khác dễ hơn là định nghĩa hàm foldMap, vốn cũng thuộc về lớp Foldable. Hàm foldMap có kiểu như sau:

foldMap :: (Monoid m, Foldable t) => (a -> m) -> t a -> m

Tham số thứ nhất là hàm nhận vào giá trị kiểu (kí hiệu a) và trả về Monoid. Tham số thứ hai là CTDL chứa các giá trị kiểu a. Nó ánh xạ hàm này lên CTDL, từ đó tạo ra một CTDL fold được có chứa các giá trị Monoid. Sau đó, bằng cách mappend giữa những giá trị Monoid, nó nối tất cả lại thành một Monoid duy nhất. Bây giờ thì hàm này có vẻ kì quặc, nhưng hay ở chỗ là định nghĩa hàm này thì sẽ tự nhiên có được foldr và foldl đối với kiểu đó!

Đây là cách mà ta biến Tree thành instance của Foldable:

instance F.Foldable Tree where
    foldMap f Empty = mempty
    foldMap f (Node x l r) = F.foldMap f l `mappend`
                             f x           `mappend`
                             F.foldMap f r

Ta hình dung như sau: nếu được cấp cho một hàm nhận vào một phần tử của cây đang xét và trả về một giá trị Monoid, thì ta sẽ rút gọn cả cây về một Monoid duy nhất. Khi fmap lên cây thì ta ánh xạ hàm lên một điểm nút rồi bằng đệ quy, ánh xạ hàm lên cây con bến trái và cây con bên phải. Ở đây, ta có nhiệm vụ không chỉ ánh xạ một hàm, mà còn phải nối các kết quả lại thành một Monoid bằng cách dùng mappend. Đầu tiên, ta xét trường hợp cây rỗng thì nó sẽ trả về giá trị Monoid là mempty.

Trường hợp cây không rỗng thì nó chứa hai cây con và cả một giá trị nữa. Trong trường hợp này, bằng cách đệ quy ta foldMap cùng hàm $f$ lên các cây con bên trái và bên phải. Hãy nhớ rằng, foldMap đang xét sẽ cho kết quả là một giá trị Monoid. Ta cũng ánh xạ f cho giá trị ở điểm nút. Bây giờ ta có ba giá trị Monoid (hai giá trị ở cây con và một sau khi ánh xạ f lên giá trị điểm nút) và ta chỉ việc dồn chúng lại vào một giá trị duy nhất. Để làm điều này ta dùng mappend, và tự nhiên là cây con bên trái sẽ ra trước tiên, sau đó đến điểm nút và tiếp theo là cây con bên phải.

Bây giờ khi đã có một instance Foldable cho kiểu dữ liệu cây, foldr và foldl sẽ được tự động định nghĩa. Xét cây sau đây:

testTree = Node 5
            (Node 3
                (Node 1 Empty Empty)
                (Node 6 Empty Empty)
            )
            (Node 9
                (Node 8 Empty Empty)
                (Node 10 Empty Empty)
            )

Với một instance Foldable, ta có thể thực hiện tất cả những phép fold từng làm được với list:

ghci> F.foldl (+) 0 testTree
42
ghci> F.foldl (*) 1 testTree
64800

Đồng thời, foldMap còn tiện dụng để rút gọn CTDL về một giá trị Monoid. Chẳng hạn, nếu muốn biết liệu có số nào trong cây bằng $3$ hay không, thì ta có thể làm như sau:

ghci> getAny $ F.foldMap (\x -> Any $ x == 3) testTree
True

Ở đây, \x -> Any $ x == 3 là hàm nhận vào một số rồi trả về một giá trị Monoid, cụ thể là một Bool được gói trong Any. foldMap áp dụng hàm này cho từng phần tử trong cây rồi rút gọn các Monoid thu được về một Monoid duy nhất bằng mappend. Nếu ta làm như sau:

ghci> getAny $ F.foldMap (\x -> Any $ x > 15) testTree
False

thì tất cả các nút trên cây sẽ chứa giá trị Any False. Nhưng để có kết quả True, mappend với Any phải có ít nhất là một giá trị True làm tham số. Điều này lý giải tại sao kết quả cuối cùng là False, và có lý, vì không có giá trị nào trên cây lớn hơn 15.

Ta cũng dễ dàng chuyển cây thành list bằng cách foldMap với hàm \x -> [x]. Mỗi phần tử sẽ là list đơn phần tử. Thao tác mappend giữa các list này sẽ cho kết quả một list duy nhất chứa tất cả những phần tử có trong cây:

ghci> F.foldMap (\x -> [x]) testTree
[1,3,6,5,8,9,10]

Kết thúc bài 16

Haskell #17: Monad