Haskell学习笔记3 -- Monoid

首先先比较一下data,type和newtype

type关键字用于创建类型别名,仅仅是给已知类型赋予一个新名字
比如

type IntList = [Int]


并没有创建值构造器,[Int]和IntList只是引用同一个类型的两种方式

newtype关键字用来将已知类型包裹为新类型
 

newtype CharList = CharList {getCharList :: [Char]}


我们不能用++来拼接一个CharList和一个类型为[Char]的列表,也不能用++来拼接两个CharList列表
因为++只对列表有效,而CharList类型并不是列表,尽管它包含了一个列表
我们可以把两个CharList转换成列表,用++拼接它们,最后转换回CharList

可以把newtype声明想象成只有一个值构造器和一个字段的data声明

data关键字用来创建你自己的数据类型

data profession = Fighter | Archer | Accountant

newtype 比 data速度快
如果用data,程序运行时会有一个包裹和解开包裹的开销
但如果用newtype,Haskell知道你只是把一个现有类型包裹成一个新的,
因为你想让它的内部实现和现有类型相同,只是想换个不同的类型
newtype根据已有类型创建新类型时,只能有一个值构造器,一个字段
而data可以有多个值构造器,每个值构造器也可以有零个或多个字段

关于newtype特性

这里用data关键字定义了一个普通的代数数据类型。它有一个值构造器,具有一个类型为Bool的字段
让我们创建一个函数对CoolBool做模式匹配,不管CoolBool里面是True还是False,都返回"hello"

helloMe :: CoolBool -> String
helloMe (CoolBool _) = "hello"
ghci> helloMe undefined
"*** Exception"

用data关键字定义的类型可以有多个值构造器
Haskell必须对这个值求值知道可以发现使用的值构造器位置
如果求值过程中牵涉到undefined,就会出现异常

data CoolBool = CoolBool (getCoolBool :: Bool)
helloMe :: CoolBool -> String
helloMe (CoolBool _) = "hello"

ghci> helloMe undefined
"hello"

工作了,为什么呢?
使用newtype时,Haskell内部把新类型的值和旧类型的值用同一种方式表示,它并没有添加额外的容器,
只是记住了新的值具有不同的类型,因为newtype只有一个值构造器,且只有一个字段
对newtype值进行模式匹配不是从容器里取出东西(data是这样),二十转换值的类型

好了,回归主题

关于monoid类型类

Monoid类型类定义在Data.Monoid模块里
 

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

mempty

mempty实际上不是一个真正的函数,因为它不接受参数,只是个多态常量

mappend

它是一个二元函数,接受同一个类型的两个值,返回那个类型的另一个值
并不是一定叠加两个值,而是一个根据两个monoid值返回第三个值的二元函数

mconcat

它取一个monoid值组成的列表,通过mappend将其中的元素相连,归约成一个值
它有一个默认的实现,把mempty作为初始值,从右边开始mappend折叠这个列表

当把一个类型变成monoid实例时,只定义mempty和mappend就足够了

monoid 定律

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

前两条规定mempty在mappend的定义中必须是单位元
第三条规定必须满足结合律,不管顺序如何,结果都必须相同

列表是monoid

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

这里用的是instance Monoid [a],而不是instance Monoid []
因为Monoid要求实例是一个具体的类型

Any和All

Bool也可以以两种不同但是同等有效的方式成为monoid
第一种方式是用令表示逻辑或的函数||为二元函数,False作为单位元
两个参数有一个为True,结果就是True

newtype Any = Any {getAny :: Bool}
   deriving (Eq,Ord,Read,Show,Bounded)
   
instance Monoid Any where
   mempty = Any False 
   Any x `mappend` Any y = Any (x || y)
   
   
ghci> getAny $ Any True `mappend` Any False
True

All 就是把||换成&&
只有所有值都为True时,mappend的结果才为True

Ordering monoid

ghci> 1 `compare` 2
LT
ghci> 2 `compare` 2
EQ
ghci> 3 `compare` 2
GT

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

当我们把两个Ordering值mappend到一起时,如果左边的值不是EQ就保留
否则就返回右边的值

比较两个字符串的长度,如果等长就比较字母序

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


-- 如果用monoid,就可以更加简单的写

lengthCompare' :: String -> String -> Ordering
lengthCompare' x y = (length x `compare` length y) `mappend`
                    (x `compare` y)

现在拓展一下,把比较元音的个数作为第二重要的准则

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")

Maybe monoid

当类型参数a为monoid时,有一种把Maybe a看作Monoid并据此实现mappend的方式:
Nothing是单位元,当两个参数都不是Nothing时,mappend把计算任务转交给下层a的mappend

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)

如果有一个值为Nothing,结果就是那个值
如果mappend两个Just值,Just的内容就会被mappend起来,结果重新包裹在Just里面
Just里面的值的类型也是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})
newtype First a = First {getFirst :: Maybe a}
   deriving (Eq,Ord,Read,Show)
   
instance Monoid (First a) where
   mempty = First Nothing
   First (Just x) `mappend` _ = First (Just x)
   First Nothing `mappend` x = x 

mempty只是一个包裹在First newtype构造器里的Nothing
如果mappend的第一个参数是Just值,那就忽略第二个参数
如果第一个参数是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'

如果有很多Maybe值,仅仅想知道其中是不是有Just时

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

monoid的折叠

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

F.foldr接受任何可以被折叠的类型

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

参考来源:

https://hoogle.haskell.org/

Haskell趣学指南

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值