惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

B
Blog RSS Feed
Spread Privacy
Spread Privacy
T
Threatpost
C
Cisco Blogs
P
Palo Alto Networks Blog
AI
AI
Cyberwarzone
Cyberwarzone
NISL@THU
NISL@THU
P
Privacy & Cybersecurity Law Blog
G
GRAHAM CLULEY
Simon Willison's Weblog
Simon Willison's Weblog
T
Tor Project blog
Latest news
Latest news
AWS News Blog
AWS News Blog
D
Docker
S
SegmentFault 最新的问题
博客园 - 聂微东
WordPress大学
WordPress大学
Vercel News
Vercel News
S
Securelist
爱范儿
爱范儿
J
Java Code Geeks
Know Your Adversary
Know Your Adversary
S
Schneier on Security
Hugging Face - Blog
Hugging Face - Blog
F
Fortinet All Blogs
Last Week in AI
Last Week in AI
D
DataBreaches.Net
宝玉的分享
宝玉的分享
D
Darknet – Hacking Tools, Hacker News & Cyber Security
MongoDB | Blog
MongoDB | Blog
Engineering at Meta
Engineering at Meta
K
Kaspersky official blog
美团技术团队
博客园 - 叶小钗
阮一峰的网络日志
阮一峰的网络日志
量子位
博客园_首页
Attack and Defense Labs
Attack and Defense Labs
S
Secure Thoughts
Google Online Security Blog
Google Online Security Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
腾讯CDC
T
Threat Research - Cisco Blogs
雷峰网
雷峰网
有赞技术团队
有赞技术团队
www.infosecurity-magazine.com
www.infosecurity-magazine.com
P
Privacy International News Feed
S
Security Affairs

Jiajun的技术笔记

你好,2026! TiDB 源码阅读(六):TiDB Coprocessor 源码解析 性能优化的核心思想 TiDB 源码阅读(五):索引 TiDB 源码阅读(四):AST、逻辑计划、物理计划 CockroachDB Serverless Architecture podman 无故退出 Cursor Control-L (CTRL-L) Keyboard Shortcuts in Terminal Replace docker with podman Using xmonad with xfce4 A RC script for freebsd frpc 自己动手写一个k8s controller AI 会取代你的(编程)岗位吗? 自建DERP服务器提升Tailscale连接速度(使用Nginx转发) 自动升级Docker容器 再读《程序员修炼之道-从小工到专家》 让浏览器下载文件 再读《软件随想录》/《黑客与画家》/《软技能》 HTTP 压力测试中的 Coordinated Omission 2的补码 编程语言中的 context 是什么? flutter macOS 构建出错 Flatpak 使用小记 Golang CAS 操作是怎么实现的 PostgreSQL 当MQ来使用 Clash 结合 工作VPN 的网络设计 使用 PostgreSQL 搭建 JuiceFS PostgreSQL 配置优化和日志分析 有GitHub Copilot?那就可以搭建你的ChatGPT4服务 窗口函数的使用(以PG为例) 读《为什么学生不喜欢上学》 OpenAI Prompt Engineering 摘录和总结 读《打造真正的新产品》 VueJS 总结 Linux 自动挂载 alist 提供的webdav FreeBSD 使用 vm-bhyve 安装Debian虚拟机 FreeBSD 和 Linux 网卡聚合实现提速 GPT 帮我搞定了时区转换问题 长任务系统如何处理? macOS/Linux 编译 InputLeap 使用开源软KVM - synergy-core 解决 macOS 终端hostname一直变化问题 KVM 共享 Intel 集成显卡 PromQL 备忘 读《格鲁夫给经理人的第一课》 读《打开心智》 为什么要把复杂的联表操作拆成多个单表查询? 红包系统的设计 MySQL Index Condition Pushdown Optimization Go mod 简明教程 OpenWRT 使用 Android/iOS USB 网络 搭建旁路由 Golang gRPC 错误处理 编写可维护的单元测试代码 OAuth 2 详解(六):Authorization Code Flow with PKCE OAuth 2 详解(五):Device Authorization Flow OAuth 2 详解(三):Resource Owner Password Credentials Grant OAuth 2 详解(四):Client Credentials Flow OAuth 2 详解(二):Implict Grant Flow OAuth 2 详解(一):简介及 Authorization Code 模式 ElasticSearch 学习笔记 三种git流程以及发版模型 错误处理实践 权限模型(RBAC/ABAC) OIDC(OpenID Connect) 简介 任务队列简介 PostgreSQL 操作笔记 使用Drone CI构建CI/CD系统 Golang migrate 做数据库变更管理 使用PostgreSQL做搜索引擎 Nginx 源码阅读(三): 连接池、内存池 Nginx 源码阅读(二): 请求处理 Nginx 源码阅读(一): 启动流程 Go 泛型简明教程 KVM 显卡穿透给 Windows 使用 HTTP Router 处理 Telegram Bot 按钮回调 使用反射(reflect)对结构体赋值 GIN 是如何绑定参数的 你好 2022(2021 年终总结) 用Go导入大型CSV到PostgreSQL 使用 OpenWRT 搭建软路由 使用软KVM切换器 barrier 共享键鼠 SQL 防注入及原理 使用 gomock 测试 Go 代码 gevent不是黑魔法(二): gevent 实现 gevent不是黑魔法(一): greenlet 实现 用 entgo 替代 gorm 应用内使用crontab不是那么方便 单测时要不要 mock 数据库? Sentry 自建指南 用selenium完成自动化任务 用闲置的安卓手机做垃圾电话短信过滤 推荐三个时间管理工具 一次事故反思 当JS遇到uint64:JS整数溢出问题 SQLite3 存储以及ACID原理 Redis源码阅读:pub/sub实现 Redis源码阅读:zset实现 Redis源码阅读:bitmap 位图的运算 Redis源码阅读:set是怎么做交并集运算的?
Haskell简明教程(四):Monoid, Applicative, Monad
Jiajun Huang · 2017-09-25 · via Jiajun的技术笔记

这一系列是我学习 Learn You a Haskell For Great Good 之后,总结,编写的学习笔记。

这个系列主要分为五个部分:

回忆TypeClass

TypeClass,我们在第二篇中就讲过,与命令式编程不同,Haskell中的class不是类,而是更像 “接口”这个概念,或者说,”类型类”。比如我们有个接口是能比较是否相等:

class Equalable a where
    equal :: a -> a -> Bool
    uneuqal :: a -> a -> Bool

    equal x y = not $ uneuqal x y
    uneuqal x y = not $ equal x y

首先我们可以看到 Equalable 针对一个类a,其中类型声明,equal :: a -> a -> Bool表示 equal这个函数接受两个a类型的参数,然后返回一个布尔类型的值。并且我们提供了默认实现, equal就是 uneuqal 的反,uneuqal就是equal的反。我们来看看Int是怎么实现这个接口的:

instance Equalable Int where
    equal x y = x == y

运行一下:

Prelude> :load Demo.hs
[1 of 1] Compiling Main             ( Demo.hs, interpreted )
Ok, 1 module loaded.
*Main> let a = 1 :: Int
*Main> let b = 1 :: Int
*Main> let c = 2 :: Int
*Main> a `equal` b
True
*Main> a `equal` c
False

热身完毕,接下来我们将要开始讲Monoid这个 class

Monoid

在讲述 Monoid 之前,我们需要先看看 FunctorApplicative热热身。

Functor和Applicative

上定义:

Prelude> :i Functor
class Functor (f :: * -> *) where
  fmap :: (a -> b) -> f a -> f b
  (<$) :: a -> f b -> f a
  {-# MINIMAL fmap #-}
  	-- Defined in ‘GHC.Base’
instance Functor (Either a) -- Defined in ‘Data.Either’
instance Functor [] -- Defined in ‘GHC.Base’
instance Functor Maybe -- Defined in ‘GHC.Base’
instance Functor IO -- Defined in ‘GHC.Base’
instance Functor ((->) r) -- Defined in ‘GHC.Base’
instance Functor ((,) a) -- Defined in ‘GHC.Base’
Prelude> :i Applicative
class Functor f => Applicative (f :: * -> *) where
  pure :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b
  GHC.Base.liftA2 :: (a -> b -> c) -> f a -> f b -> f c
  (*>) :: f a -> f b -> f b
  (<*) :: f a -> f b -> f a
  {-# MINIMAL pure, ((<*>) | liftA2) #-}
  	-- Defined in ‘GHC.Base’
instance Applicative (Either e) -- Defined in ‘Data.Either’
instance Applicative [] -- Defined in ‘GHC.Base’
instance Applicative Maybe -- Defined in ‘GHC.Base’
instance Applicative IO -- Defined in ‘GHC.Base’
instance Applicative ((->) a) -- Defined in ‘GHC.Base’
instance Monoid a => Applicative ((,) a) -- Defined in ‘GHC.Base’

fmap?似曾相识,不就是map吗?map是函数,但是 Functor是能应用到map函数 的东西抽象出来的接口。我们暂且把被map应用的东西叫做 “容器” 或者 “盒子”。

比如最典型的,List能被map(之后的例子我们都用List):

Prelude> map (+1) [1 .. 3]
[2,3,4]

<$?看样子是指把一个类型放到容器里,便能反推出该类型所对应的有容器的值, 说起来好绕,看一个具体例子好了:

Prelude> fmap (+1) [1 .. 3]
[2,3,4]
Prelude> (<$) 1 [3]
[1]

果然是这样。瞧,这就是类型声明的好处,读代码靠看类型就能猜个大概出来 :joy:

至于 Applicative,我们则可以看到,首先Applicative是Functor,此外:

class Functor f => Applicative (f :: * -> *) where
  pure :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b
  GHC.Base.liftA2 :: (a -> b -> c) -> f a -> f b -> f c
  (*>) :: f a -> f b -> f b
  (<*) :: f a -> f b -> f a
  • pure 应该是拿一个具体的值,便能给出在容器中的值
  • <*> 则是把从a到b的函数丢到容器里,然后取一个容器,并且将函数引用到那个容器
  • *> 则是取两个容器但是舍弃前面一个容器
  • <* 则是取两个容器但是舍弃后面一个容器
  • GHC.Base.liftA2 则是一个从a和b到c的函数,然后取两个容器,其中容器内类型分别为a和b,产生装有c的容器

是不是更晕?没关系,Haskell就是这样,每一行代码你都需要仔细考虑。我们对这上面的讲解分别看下面五个例子:

Prelude> pure 1 :: [Int]
[1]
Prelude> (<*>) [\x -> x + 1] [1]
[2]
Prelude> (*>) [1] [2]
[2]
Prelude> (<*) [1] [2]
[1]
Prelude> GHC.Base.liftA2 (\a b -> a + b) [1] [2]
[3]

瞧,有感觉了吗?如果没有的话,我想可能需要重新一步一步跟着来,再读一遍此前的内容。接下来我们看Monoid

Monoid

首先我们打开ghci看看定义:

Prelude> :m Data.Monoid
Prelude Data.Monoid> :i Monoid
class Monoid a where
  mempty :: a
  mappend :: a -> a -> a
  mconcat :: [a] -> a
  {-# MINIMAL mempty, mappend #-}
  	-- Defined in ‘GHC.Base’
instance Num a => Monoid (Sum a) -- Defined in ‘Data.Monoid’
instance Num a => Monoid (Product a) -- Defined in ‘Data.Monoid’
instance Monoid (Last a) -- Defined in ‘Data.Monoid’
instance Monoid (First a) -- Defined in ‘Data.Monoid’
instance Monoid (Endo a) -- Defined in ‘Data.Monoid’
instance Monoid a => Monoid (Dual a) -- Defined in ‘Data.Monoid’
instance Monoid Any -- Defined in ‘Data.Monoid’
instance GHC.Base.Alternative f => Monoid (Alt f a)
  -- Defined in ‘Data.Monoid’
instance Monoid All -- Defined in ‘Data.Monoid’
instance Monoid [a] -- Defined in ‘GHC.Base’
instance Monoid Ordering -- Defined in ‘GHC.Base’
instance Monoid a => Monoid (Maybe a) -- Defined in ‘GHC.Base’
instance Monoid a => Monoid (IO a) -- Defined in ‘GHC.Base’
instance Monoid b => Monoid (a -> b) -- Defined in ‘GHC.Base’
instance (Monoid a, Monoid b, Monoid c, Monoid d, Monoid e) =>
         Monoid (a, b, c, d, e)
  -- Defined in ‘GHC.Base’
instance (Monoid a, Monoid b, Monoid c, Monoid d) =>
         Monoid (a, b, c, d)
  -- Defined in ‘GHC.Base’
instance (Monoid a, Monoid b, Monoid c) => Monoid (a, b, c)
  -- Defined in ‘GHC.Base’
instance (Monoid a, Monoid b) => Monoid (a, b)
  -- Defined in ‘GHC.Base’
instance Monoid () -- Defined in ‘GHC.Base’
class Monoid a where
  mempty :: a
  mappend :: a -> a -> a
  mconcat :: [a] -> a

我们看类型来推测:

  • mempty 不取值,给出一个类型的 “空值”
  • mappend 取一个一个值和另一个值,返回同类型的值
  • mconcat 取一个列表的值,合成一个值

说实话,这些都是我猜的,所以得验证一下:

Prelude Data.Monoid> mempty :: [Int]
[]
Prelude Data.Monoid> mappend [1] [2]
[1,2]
Prelude Data.Monoid> mconcat [[1], [2, 3], [4]]
[1,2,3,4]

原来是这样,第一个如我所说,第二个是把两个容器连起来,第三个是把容器的容器打散组合成 一个新的容器。原来是这样,那么有哪些类型实现了这个接口呢?我们可以看到上面,Maybe a[a]等都实现了,因为 Monoid 是针对操作容器自身的,所以感觉有些抽象,有点像 Python 里的 metaclass。这一节得要仔细消化。

Monad

Monad和Monoid有什么关系吗?说实话,我个人认为是雷锋和雷峰塔的关系。国际惯例,我们来看看定义:

Prelude> :i Monad
class Applicative m => Monad (m :: * -> *) where
  (>>=) :: m a -> (a -> m b) -> m b
  (>>) :: m a -> m b -> m b
  return :: a -> m a
  fail :: String -> m a
  {-# MINIMAL (>>=) #-}
  	-- Defined in ‘GHC.Base’
instance Monad (Either e) -- Defined in ‘Data.Either’
instance Monad [] -- Defined in ‘GHC.Base’
instance Monad Maybe -- Defined in ‘GHC.Base’
instance Monad IO -- Defined in ‘GHC.Base’
instance Monad ((->) r) -- Defined in ‘GHC.Base’
instance Monoid a => Monad ((,) a) -- Defined in ‘GHC.Base’

我们继续猜测Monad的几个接口:

  • return 是取一个具体值,然后把它封到容器里
  • fail 是取一个字符串,然后生成一个容器
  • >>= 是取一个容器,取一个具体值到盒子的映射函数,生成一个容器
  • >> 是取两个容器,舍弃前一个

看看具体例子:

Prelude> return 1 :: [Int]
[1]
Prelude> fail "hello" :: [Int]
[]
Prelude> (>>=) [1] (\x -> [x + 1])
[2]
Prelude> (>>) [1] [2]
[2]

没了,这就是Monad。更深的Monad的内容需要到以后实际用到才更好理解。这里先讲到这个程度。

为什么总是讲到盒子?容器?抽象?

我们很久之前就讲到过抽象的好处,抽象使得我们不必关心具体实现细节,只需要知道有这么一个 方法,我们只要这样用就好。而所谓的盒子,所谓的容器其实是同样的想法,为了抽象。

什么是Monad?实现了这几个接口就可以是一个Monad。XMonad就因此得名, 因为他把核心实现了Monad这个接口(类型类)。