前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >深入typeclass_Haskell笔记4

深入typeclass_Haskell笔记4

作者头像
ayqy贾杰
发布于 2019-06-12 06:41:35
发布于 2019-06-12 06:41:35
54400
代码可运行
举报
文章被收录于专栏:黯羽轻扬黯羽轻扬
运行总次数:0
代码可运行

零.Typeclass与Class

Typeclass就是Haskell中的接口定义,用来声明一组行为

OOP中的Class是对象模板,用来描述现实事物,并封装其内部状态。FP中没有内部状态一说,所以Class在函数式上下文指的就是接口。派生自某类(deriving (SomeTypeclass))是说具有某类定义的行为,相当于OOP中的实现了某个接口,所以具有接口定义的行为

一.声明

class关键字用来定义新的typeclass:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Eq a where
 (==) :: a -> a -> Bool
 (/=) :: a -> a -> Bool
 x == y = not (x /= y)
 x /= y = not (x == y)

其中,a是个类型变量,在定义instance时给出具体类型。前两条类型声明是接口所定义的行为(通过定义函数类型来描述)。后两条函数实现是可选的,通过间接递归定义来描述这两个函数的关系,这样只需要提供一个函数的实现就够了(这种方式称为minimal complete definition,最小完整定义)

P.S.GHCi环境下,可以通过:info <typeclass>命令查看该类定义了哪些函数,以及哪些类型属于该类

二.实现

instance关键字用来定义某个typeclass的instance:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
instance Eq TrafficLight where
 Red == Red = True
 Green == Green = True
 Yellow == Yellow = True
 _ == _ = False

这里把class Eq a中的类型变量a换成了具体的TrafficLight类型,并实现了==函数(不用同时实现/=,因为Eq类中声明了二者的关系)

试着让自定义类型成为Show类成员:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
data Answer = Yes | No | NoExcuse
instance Show Answer where
 show Yes = "Yes, sir."
 show No = "No, sir."
 show NoExcuse = "No excuse, sir."

试玩一下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
> Yes
Yes, sir.

P.S.GHCi环境下,可以通过:info <type>命令查看该类型属于哪些typeclass

子类

同样,也有子类的概念,是指要想成为B类成员,必须先成为A类成员的约束:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class (Eq a) => Num a where
-- ...

要求Num类成员必须先是Eq类成员,从语法上来看只是多了个类型约束。类似的,另一个示例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
instance (Eq m) => Eq (Maybe m) where
 Just x == Just y = x == y
 Nothing == Nothing = True
 _ == _ = False

这里要求Maybe a中的类型变量a必须是Eq类的成员,然后,Maybe a才可以是Eq类的成员

三.Functor

函子(听起来很厉害),也是一个typeclass,表示可做映射(能被map over)的东西

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Functor f where
 fmap :: (a -> b) -> f a -> f b

fmap接受一个map a to b的函数,以及一个f a类型的参数,返回一个f b类型的值

看起来有点迷惑,f a类型是说带有类型参数的类型,比如MaybeList等等,例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
mapMaybe :: Eq t => (t -> a) -> Maybe t -> Maybe a
mapMaybe f m
 | m == Nothing = Nothing
 | otherwise = Just (f x)
 where (Just x) = m

其中,Maybe t -> Maybe a就是个f a -> f b的例子。试玩一下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
> mapMaybe (> 0) (Just 3)
Just True

map a to b在这里指的就是Maybe NumMaybe Bool

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Just 3 :: Num a => Maybe a
Just True :: Maybe Bool

所以,Functor定义的行为是保留大类型不变(f a,这里的a是类型变量),允许通过映射(fmap函数)改变小类型(f a变到f b,这里的ab是具体类型)

带入List的上下文,就是允许对List内容做映射,得到另一个List,新List的内容类型可以发生变化。但无论怎样,fmap结果都是List a(这里的a是类型变量)

听起来非常自然,因为List本就属于Functor类,并且:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
map :: (a -> b) -> [a] -> [b]

这不就是fmap :: (a -> b) -> f a -> f b类型定义的一个具体实现嘛,实际上,这个map就是那个fmap

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
instance Functor [] where
 fmap = map

MaybeList都属于Functor类,它们的共同点是什么?

都像容器。而fmap定义的行为恰恰是对容器里的内容(值)做映射,完了再装进容器

还有一些特殊的场景,比如Either

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
data Either a b = Left a | Right b  -- Defined in ‘Data.Either’

Either的类型构造器有两个类型参数,而fmap :: (a -> b) -> f a -> f bf只接受一个参数,所以,Eitherfmap要求左边类型固定:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
mapEither :: (t -> b) -> Either a t -> Either a b
mapEither f (Right b) = Right (f b)
mapEither f (Left a) = Left a

左边不做映射,因为映射可能会改变类型,而Either a(即fmap :: (a -> b) -> f a -> f bf)是不能变的,所以当Nothing一样处理。例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
> mapEither show (Right 3)
Right "3"
> mapEither show (Left 3)
Left 3

另一个类似的是Map

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
-- 给Data.Map起了别名Map
data Map.Map k a -- ...

Map k v做映射时,k不应该变,所以只对值做映射:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
mapMap :: Ord k => (t -> a) -> Map.Map k t -> Map.Map k a
mapMap f m = Map.fromList (map (\(k ,v) -> (k, f v)) xs)
 where xs = Map.toList m

例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
> mapMap (+1) (Map.insert 'a' 2 Map.empty)
fromList [('a',3)]
> mapMap (+1) Map.empty
fromList []

P.S.这些简单实现可以通过与标准库实现做对比来验证正确性,例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
> fmap (+1) (Map.insert 'a' 2 Map.empty )
fromList [('a',3)]

P.S.另外,实现Functor时需要遵循一些规则,比如不希望List元素顺序发生变化,希望二叉搜索树仍保留其结构性质等等

四.Kind

参与运算的是值(包括函数),而类型是值的属性,所以值可以按类型分类。通过值携带的这个属性,就能推断出该值的一些性质。类似的,kind是类型的类型,算是对类型的分类

GHCi环境下,可以通过:kind命令查看类型的类型,例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
> :k Int
Int :: *
> :k Maybe
Maybe :: * -> *
> :k Maybe Int
Maybe Int :: *
> :k Either
Either :: * -> * -> *
> :k Either Bool
Either Bool :: * -> *
> :k Either Bool Int
Either Bool Int :: *

Int :: *表示Int是个具体类型,Maybe :: * -> *表示Maybe接受一个具体类型参数,返回一个具体类型,而Either :: * -> * -> *表示Either接受2个具体类型参数,返回一个具体类型,类似于函数调用,也有柯里化特性,可以进行部分应用(partially apply)

还有一些更奇怪的kind,例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
data Frank a b  = Frank {frankField :: b a} deriving (Show)

对值构造器Frank的参数frankField限定了类型为b a,所以b* -> *a是具体类型*,那么Frank类型构造器的kind为:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Frank :: * -> (* -> *) -> *

其中第一个*是参数a,中间的* -> *是参数b,最后的*是说返回具体类型。可以这样填充:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
> :t Frank {frankField = Just True}
Frank {frankField = Just True} :: Frank Bool Maybe
> :t Frank {frankField = "hoho"}
Frank {frankField = "hoho"} :: Frank Char []

回过头来看EitherFunctor实现:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
> :k Either
Either :: * -> * -> *
> :t fmap
fmap :: Functor f => (a -> b) -> f a -> f b

Either的kind是* -> * -> *(需要两个具体类型参数),而fmap想要的(a -> b)* -> *(只要一个具体类型参数),所以应该对Either部分应用一下,填充一个参数使之成为* -> *,那么mapEither的实现就是:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
mapEither :: (t -> b) -> Either a t -> Either a b
mapEither f (Right b) = Right (f b)
mapEither f (Left a) = Left a

Either a就是个标准的* -> *,例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
> :k Either Int
Either Int :: * -> *

P.S.也可以对着typeclass来一发,例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
> :k Functor
Functor :: (* -> *) -> Constraint
> :k Eq
Eq :: * -> Constraint

其中Constraint也是一种kind,表示必须是某类的instance(即类型约束,经常在函数签名的=>左边看到),例如Num,具体见What does has kind ‘Constraint’ mean in Haskell

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-05-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端向后 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Haskell 自定义type与typeclass
那么如何解读上面的表达式呢? 首先data关键字后边的BookInfo是新类型的名字,我们称BookInfo为*类型构造器*。类型构造器用于指代(refer)类型。类型名字的首字母必须大写,因此类型构造器的首字母也必须大写。 接下来的Book是*值构造器*(或者称:*数据构造器*)的名字,类型的值就是由值构造器创建的。 Book之后的Int String [String] 是类型的组成部分 在这个例子中,Int表示书ID, String表示书名,[String]表示作者
Orlion
2024/09/02
1530
类型_Haskell笔记3
::读作“类型为”(has type),告诉编译器变量name是String类型(即[Char]类型)的
ayqy贾杰
2019/06/12
9750
类型_Haskell笔记3
模块_Haskell笔记2
hiding语法能够缓解命名冲突问题,但不很方便,对于存在大量命名冲突的模块,可以通过qualified保留命名空间来避免冲突
ayqy贾杰
2019/06/12
1.8K0
Monad_Haskell笔记10
从类型来看,Functor到Applicative再到Monad是从一般到特殊的递进过程(Monad是特殊的Applicative,Applicative是特殊的Functor)
ayqy贾杰
2019/06/12
8320
Functor与Applicative_Haskell笔记7
常见的Functor类实例似乎都可以比作盒子(或者叫容器),比如Maybe/Either,List([]):
ayqy贾杰
2019/06/12
6170
Functor与Applicative_Haskell笔记7
来看看几种 Monad来看看几种 Monad
https://learnyoua.haskell.sg/content/zh-cn/ch12/a-fistful-of-monads.html
一个会写诗的程序员
2018/12/12
1.1K0
Monoid_Haskell笔记9
数学世界里,0是加法单位元,1是乘法单位元(identity element),例如:
ayqy贾杰
2019/06/12
8650
Monadic Function_Haskell笔记12
只是把context换成了Monad而已,此外没什么区别。并且对于遵守Functor laws和Monad laws的类型,这两个函数是完全等价的,例如:
ayqy贾杰
2019/06/12
9660
Haskell Type与Typeclass
我们可以这样解读这个函数的类型:removeNonUppercase这个函数接收一个Char List类型的参数返回一个Char List类型的返回值
Orlion
2024/09/02
1390
newtype_Haskell笔记8
在List场景,xs <*> ys表示从左侧xs中取出函数作用于右侧ys中的每一项,有两种实现方式:
ayqy贾杰
2019/06/12
6910
当我们谈论Monad的时候(二)
在上一篇文章中,我通过几个Java的例子简单的说明了Monad的本质和一些工程中常见的用途。接下来的文章就不再侧重于工程了,而是要慢慢向理论转换。而作为过渡,我选择了Haskell来代替Java进行说明。本篇文章默认读者已经对Haskell的基本语法有所了解,因此对此类内容我不会再做赘述。
KAAAsS
2022/01/14
9060
铁定不纯的IO_Haskell笔记5
一直有个疑惑,Haskell号称纯函数式语言,那么铁定不纯的场景(肯定有副作用,或者操作本身就是副作用)如何解决?
ayqy贾杰
2019/06/12
1.4K0
Functors, Applicatives, And Monads In PicturesFunctors, Applicatives, And Monads In Pictures
原文: http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html 参考文章: http://homepages.inf.ed.ac.uk/wadler/papers/marktoberdorf/baastad.pdf
一个会写诗的程序员
2018/12/12
6130
从 Java 和 JavaScript 来学习 Haskell 和 Groovy(类型系统)
接上文 《从 Java 和 JavaScript 来学习 Haskell 和 Groovy(引子)》。
四火
2022/07/19
6320
learn-haskell
引言 Haskell不同于Scala,是一门纯函数式语言,它强制使用者使用函数式语法而没有妥协。 是一门强类型定义的静态类型语言。它的**类型模型基于推断理论(in-ferred)**并被公认为是函数语言中最高效的类型系统之一。你会发现该类型系统支持多态语义并有助于人们作出十分整洁清晰的设计。 支持Erlang风格的模式匹配(pattern matching)和哨兵表达式。你也能在Haskell中发现Clojure风格的惰性求值(lazyevaluation)以及与Clojure和Erlang相同的列表推导
刘笑江
2018/05/28
1.2K0
Scalaz(6)- typeclass:Functor-just map
  Functor是范畴学(Category theory)里的概念。不过无须担心,我们在scala FP编程里并不需要先掌握范畴学知识的。在scalaz里,Functor就是一个普通的typecla
用户1150956
2018/01/05
8450
热爱函数式的你,句句纯正的 Haskell【类型篇】
---- theme: github 每次看到干尸鬼鲛起舞,都有一种说不出的难受,不行,发出来,让大家一起难受难受~🐶 Haskell 是一门纯的函数式语言。 也就是说计算机主要是通过函数来完成的(像在数学中一样),而不是通过“先做这个,再做那个”的命令式操作顺序进行的(像在主流的编程语言中一样)。—— Simon Peyton Jones 初见😀 什么是 Haskell ?我们从 wiki 上可以找到以下要点: Haskell 是一种标准化的,通用的纯函数式编程语言,有惰性求值和强静态类型; 在H
掘金安东尼
2022/09/19
1.1K0
热爱函数式的你,句句纯正的 Haskell【类型篇】
从惰性IO说起_Haskell笔记6
从硬盘读文件时并不会一次性全读入内存,而是一点一点的流式读取。文本文件的话,默认buffer是line-buffering,即一次读一行,二进制文件的话,默认buffer是block-buffering,一次读一个chunk,其具体大小取决于操作系统
ayqy贾杰
2019/06/12
2.4K0
Scalaz(8)- typeclass:Monoid and Foldable
该文章介绍了如何使用Monoid类型进行并行计算和并行序列化。首先介绍了Monoid的概念,以及如何使用Scalaz库中的Monoid类型进行并行计算。然后介绍了如何使用Monoid类型进行并行序列化,并提供了几个例子。最后还介绍了如何使用Monoid类型进行并行处理,并提供了几个例子。
用户1150956
2018/01/05
1.1K0
用 350 行代码从零开始,将 Lisp 编译成 JavaScript
我们将会在本篇文章中看到从零开始实现的编译器,将简单的类 LISP 计算语言编译成 JavaScript。完整的源代码在 这里。
用户8639654
2021/10/25
1.1K0
相关推荐
Haskell 自定义type与typeclass
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验