Haskell类型化介绍

182 阅读8分钟

想象一下,你的任务是在Haskell中写一个将一个值增加1的函数。

什么比较简单?最后,有一个地方可以利用以前的JavaScript经验。😅

你找到了有界整数的类型--Int ,然后就开始工作了。

plusOne :: Int ->  Int
plusOne a = a + 1

但事实证明,团队负责人还希望有一个能适用于浮点数的加号函数。

plusOneFloat :: Float -> Float
plusOneFloat a = a + 1

这两个要求肯定都可以由一个更通用的函数来覆盖。

plusOnePoly :: a -> a
plusOnePoly a = a + 1

不幸的是,上面的代码无法编译。

No instance for (Num a) arising from a use of '+'

在Haskell中,要通过+ 对同一类型的两个成员求和,他们的类型需要有一个Num 类型类的实例。

但什么是类型类,什么是类型类的实例?请进一步阅读以找到答案。

我将介绍:

  • 什么是类型库?
  • 如何使用它们。
  • 如何创建类型类的实例。
  • 诸如Eq,Ord,Num,Show, 和Read 的基本类型库。

推荐的前期知识:代数数据类型

什么是Haskell中的typeclass?

一个类型类定义了一组在多个类型中共享的方法。

一个类型要属于一个类型类,它需要实现该类型类的方法。这些实现是临时性的:方法对于不同的类型可以有不同的实现。

作为一个例子,让我们看一下Haskell中的Num 类型类。

class Num a where
  (+) :: a -> a -> a
  (-) :: a -> a -> a
  (*) :: a -> a -> a
  negate :: a -> a
  abs :: a -> a
  signum :: a -> a
  fromInteger :: Integer -> a

对于一个属于Num 类型的类型,它需要实现其方法。+,-,*, 等等。

如果你想使用它的一个方法,比如+ ,你只能在有Num 的实例的类型上使用它。

而一个使用+ 的函数需要限制自己,只接受Num 类型的成员。否则,它就不会被编译。

这可以通过在类型签名中加入一个类型约束(Num a => )来实现。

plusOnePoly :: Num a => a -> a
plusOnePoly a = a + 1

这与跨所有类型的多态性形成对比。例如,++ 将与两个相同类型的元素列表一起工作,无论该类型是什么。

Prelude> :t (++)
(++) :: [a] -> [a] -> [a]

类型库与Java接口、Rust特征和Elixir协议相似,但也有明显的区别

什么是类型类的实例?

如果一个类型实现了一个类型类的方法,那么它就有一个类型类的实例。

我们可以手工定义这些实例,但Haskell也可以通过自己派生出实现来为我们做很多工作。

我将在下面的章节中介绍这两种选择。

如何定义typeclass实例

让我们想象一下,我们有一个小精灵的数据类型,包括它们的名字、Pokedex编号、类型和能力。

data Pokemon = Pokemon
  { pokedexId   :: Int
  , name        :: String
  , pokemonType :: [String]
  , abilities   :: [String]
  }

我们有两个小精灵--Slowking和Jigglypuff--它们可以说是口袋妖怪宇宙中最好的产品。

*Main> slowking = Pokemon 199 "Slowking" ["Water", "Psychic"] ["Oblivious", "Own Tempo"]
*Main> jigglypuff = Pokemon 39 "Jigglypuff" ["Normal", "Fairy"] ["Cute Charm", "Competitive"]

出于某种原因,我们想知道它们的价值是否相等。

现在,GHCi无法回答这个问题。

*Main> slowking == jigglypuff

<interactive>:19:1: error:
    • No instance for (Eq Pokemon) arising from a use of '=='
    • In the expression: slowking == jigglypuff
      In an equation for 'it': it = slowking == jigglypuff

这是因为Pokemon 并没有一个Eq 类型的实例。

有两种方法可以使Pokemon 成为Eq 类型类的成员:派生一个实例或手动创建它。我将介绍这两种方法。

衍生Eq

当创建一个类型时,你可以添加deriving 关键字和一个你想要实例的类型的元组,例如(Show, Eq) 。然后编译器将尝试为你找出实例。

这可以节省很多花在输入明显实例上的时间。

Eq 的例子中,我们通常可以推导出该实例。

data Pokemon = Pokemon
  { pokedexId   :: Int
  , name        :: String
  , pokemonType :: [String]
  , abilities   :: [String]
  } deriving (Eq)

派生的实例将通过比较每个单独的字段来比较两个小精灵是否相等。如果所有的字段都相等,那么记录也应该是相等的。

现在我们可以回答我们的问题了。

*Main> slowking == jigglypuff
False

定义Eq

口袋妖怪号码应该唯一地识别一个口袋妖怪(如果我们使用国家口袋妖怪索引)。因此,从技术上讲,我们不需要比较两个小精灵的所有字段来知道它们是同一个小精灵。只比较索引就足够了。

要做到这一点,我们可以创建一个自定义的实例Eq

首先,我们需要删除deriving (Eq) 子句。

data Pokemon = Pokemon
  { pokedexId   :: Int
  , name        :: String
  , pokemonType :: [String]
  , abilities   :: [String]
  }

然后我们可以为Pokemon 类型类定义一个Eq 的实例。

--       {1}   {2}
instance Eq Pokemon where
  --       {3}
  pokemon1 == pokemon2 = pokedexId pokemon1 == pokedexId pokemon2

-- {1}: Typeclass whose instance we are defining.
-- {2}: The data type for which we are defining the instance for.
-- {3}: Method definitions.

尽管Eq 有两个方法:==/= ,我们只需要定义其中一个就可以满足实例的最小要求。

定义一个实例的最低要求

通常没有必要定义类型的所有方法。

例如,Eq 有两个方法:==/= 。如果你定义了== ,就可以合理地认为/= 将是not ==

事实上,这就在类型类的定义中。

class Eq a where
    (==) :: a -> a -> Bool
    (/=) :: a -> a -> Bool
    x == y = not (x /= y)
    x /= y = not (x == y)

由于这个原因,Eq 的最小定义是定义这些方法中的一个。

要查看一个类型类的最小定义,你可以使用:info

*Main> :info Eq
...
  {-# MINIMAL (==) | (/=) #-}
...

如果你提供了一个类型类的最小实现,编译器可以找出其他的方法。

这是因为它们可以被。

  • 用你已经提供的方法来定义。
  • 以该类型的超类所具有的方法来定义(我将在后面介绍超类)。
  • 默认提供。

不过,出于性能的考虑,你可能想提供你自己的实现。

订购小精灵

让我们想象一下,我们想对我们的小精灵进行排序。

为了比较一个类型的两个值,该类型需要有一个Ord 类型类的实例。

class Eq a => Ord a where
  compare :: a -> a -> Ordering
  (<) :: a -> a -> Bool
  (<=) :: a -> a -> Bool
  (>) :: a -> a -> Bool
  (>=) :: a -> a -> Bool
  max :: a -> a -> a
  min :: a -> a -> a

Ord 在Haskell中, 和 是相辅相成的,因为 是 的一个超类。Eq Eq Ord

class Eq a => Ord a where           

让我们快速浏览一下超类,以便我们理解这意味着什么。

超类

在Haskell中,类型类有一个与OOP中的类相似的层次结构。

如果一个类型类x 是另一个类y 的超类,你需要在实现y 之前实现x

在我们的例子中,我们需要先实现Eq (我们做到了),然后再实现Ord.

此外,类型类经常依赖于它们的超类的方法定义。所以你需要小心,当你定义类型类和它的超类时,你 "意味着同样的事情"。

例如,Ord 类型依赖于Eq 类型的默认值,所以让它们互相兼容是一条经验法则。

这意味着我们的Ord 实例应该以a <= ba >= b 暗示a == b 的方式排序。是的,Haskell有时候就是这样的。😂


既然我们用Pokedex数字来定义平等,我们也将用它来定义顺序。

Ord 的情况下,最小的定义是<=compare 。我们将定义其中的第一个。

下面是实例定义的样子。

instance Ord Pokemon where
  pokemon1 <= pokemon2 = pokedexId pokemon1 <= pokedexId pokemon2

在这一点上,很容易看出为什么我们的Ord 实例需要与我们的Eq 实例相一致。由于我们只提供了最小的实现,Haskell将使用Eq 的方法--== --和我们对<= 的定义来创建<> 的实现。

现在我们可以比较小精灵了。

*Main> jigglypuff < slowking
True
*Main> jigglypuff > slowking
False

我们还可以创建第三个小精灵,并使用sort 函数对小精灵的列表进行排序。

为了在GHCi中看到排序后的列表,我们需要为我们的数据类型派生出Show 类型。

data Pokemon = Pokemon
  { pokedexId   :: Int
  , name        :: String
  , pokemonType :: [String]
  , abilities   :: [String]
  } deriving (Show)

现在我们可以看到结果了。

*Main> chansey = Pokemon 113 "Chansey" ["Normal"] ["Natural Cure", "Serene Grace"]
*Main> import Data.List

*Main Data.List> sort([chansey, jigglypuff, slowking])
[Pokemon {name = pokedexId = 39, "Jigglypuff", pokemonType = ["Normal","Fairy"], abilities = ["Cute Charm","Competitive"]},Pokemon {pokedexId = 113, name = "Chansey", pokemonType = ["Normal"], abilities = ["Natural Cure","Serene Grace"]},Pokemon {pokedexId = 199, name = "Slowking", pokemonType = ["Water","Psychic"], abilities = ["Oblivious","Own Tempo"]}]

基本的Haskell类型类

让我们来看看你在阅读这篇文章时遇到的基本的Haskell类型类。

Eq

Eq 提供了一个用于测试平等性的接口。它有两个方法: 和 ,分别用于平等和不平等。== /=

class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool

Eq 的最小定义是提供==/=

你一般可以在定义你的数据类型时派生出这个类型。

Ord

OrdEq 的一个子类,用于具有总排序的数据类型(每个值都可以与另一个值进行比较)。

class Eq a => Ord a where
  compare :: a -> a -> Ordering
  (<) :: a -> a -> Bool
  (<=) :: a -> a -> Bool
  (>) :: a -> a -> Bool
  (>=) :: a -> a -> Bool
  max :: a -> a -> a
  min :: a -> a -> a

它提供了以下功能:

  • compare,它比较两个值并给出一个Ordering ,它是三个值中的一个。LT,EQ, 或GT
  • 用于比较的操作符。<,<=,>,>= ,取两个值并返回一个Bool
  • max 和 ,分别返回两个值中最大和最小的值。min

Ord 类型的最小定义是compare<=

ShowRead

Show 是一个用于转换为字符串的类型类。 是它的反面:它是用于从字符串转换为数值的类型类。实现应该遵循 的规律。Read read . show = id

对于初学者来说,show 的方法对于调试是很重要的。如果你正在使用GHCi,并且需要在终端打印出你的自定义类型,你需要派生出Show 。否则,你将无法打印它们。

有一件很重要的事情,一些初学者会感到困惑:这些typeclasses不应该被用于漂亮的打印和解析复杂的值。在这方面有更好的工具。

Num

Num 是数字的typeclass。

class Num a where
  (+) :: a -> a -> a
  (-) :: a -> a -> a
  (*) :: a -> a -> a
  negate :: a -> a
  abs :: a -> a
  signum :: a -> a
  fromInteger :: Integer -> a

Num 的最小定义包括。(+),(*),abs,signum,fromInteger, 和negate(-)

它提供了所有你在处理整数时预期需要的算术操作。

总结

这个关于Haskell中类型化的介绍涵盖了什么是类型化以及如何通过派生或定义它们来创建你自己的实例。