Haskell类型类专题1

Haskell类型类既不是”类型”, 也不是”类型的类型”, 此处中文的”类型类”与英文的”Typeclass”都很confusing, 以下整理一些常见的问题及其解答(FAQ):{以下理解都非常粗浅, 因为我学这个不到2星期}

类型类究竟是什么? 和类型有什么联系?

  • 类型类与接口相似, 是类型的标杆.”入乡随俗”
    • 显示了类型的一些属性
    • 赋予了类型一些功能
    • 限制了类型的一些行为
    • 不限死某种特定的”类型”
  • 虽然类型类与接口相似, 但是类型类本身可以完成缺省的函数实现, 也可以多继承, 给类型定义带来福利.
    • 给既有类型的福利:
      • (这点存疑, 不确定)因为可以完成缺省函数实现(也就是Haskell内部已经定义而非仅仅声明), 所以这个抽象的”接口”实际上可以变成实际的”汽油机”, 也即我们不需要对接口进行自定义的Implement, 可以直接使用Haskell自身定义好的类型类.
    • 给新类型的福利:
      • 因为可以多继承, 所以newtype时候就可以直接detriving, 省下很多(像什么友元声明,单例继承一堆破事)的代码.
  • 类似于C++的模板, 可以默认也可以特化.请看这里

类型类能用来干什么?

  • 给予某个新类型一些函数(或者算子,都一样), 例如这里定义的Color类型.
    • 因为Color是新定义类型(且字面值中没有数字或字符串这些东西,所以)没法被Haskell”类型推断机制”识别, 所以默认是不给Color任何类型类的, 那么”判断相等或不等”这件事就需要自己写函数, 3个颜色9种组合, 只有{RR}{GG}{BB}3种正确, 所以给出相等的定义.
      • 这种操作是低效的,按照C++观点就是没有对”相等”这件事进行重载, 事实上这个思想是共通的, 只是Haskell是用类型类来把”重载”这件事也给提取并抽象为类型类了
      • 为什么不能不重载? 不重载的坏处是:
        • 假设我们是一个宠物专卖店, 有100种狗, 并且由于生理因素的划分必须每种狗都建立一种类型, 那么所有的判断”(狗的种类)是否相等”的函数就会有100种(类似”京巴狗_Eq”, “拉布拉多犬_Eq”, “金毛败犬_Eq”…)
      • 如果我的”相等”判定比较特殊(奇葩), 我判定的标准是: 例如{“拉布拉多犬”, “拉布拉多狗”, “拉布拉多”}三个字符串是可以认为他们”相等”的, 那这种情况要怎么处理?
        • 如果在C++中:
          • 在重载算子的时候请谨慎一点(推荐尽量不要重载算子)因为你到时候这个”=”一旦被继承, 根本不知道bug出在”犬”和”狗”的身上.
        • 如果在Haskell中,
          • 推荐单独写函数, 因为重载的算子实际上就是一种函数, 并且函数全局下只有一种定义(?存疑, 这点待求证), 我不确定会不会重载掉”=”之后全局的等号都会被污染.
    • 粗浅理解就是:
      • 引入算子:
        • 如果实现类型时要用到”=”, 那么请引入”Eq”类型类.
      • 注意类型类之间的依赖:
        • 如果要实现”>”,”≥”, 那么请引入”Ord”的同时, 引入”Eq”, 因为”Ord”依赖着”Eq”
  • 对某些”类型划分不单一”的字面量进行合理限制, 例子见《Haskell函数式编程入门-第二版-第一卷》51-52页
    • 举例: :type 5 -> 5 :: Num a => a
    • 首先, 字面量分两种, 一种是具有明确类型的,一类是不具有明确类型的.
      • 具有单一类型的字面量:
        • {‘a’, ‘b’, ‘c’}, 一定是Char类型
        • {True, False}, 一定是Bool类型
      • 类型划分不单一的字面量:
        • 数字5: 只能说一定得满足Num类型类要求, 但具体是哪种类型不确定.(视情况, 既可以是Int{8,16,32,64}, 也可以是Integer, 也可以是Int, 也可以是Float or Double)
        • 字符列表”abc”: 只能说一定得满足[Char]类型类要求, 但具体是哪种类型不确定.(既可以是”字符串”也可以是”字节串”(ByteString))
          • (和python2类似的老毛病)
          • (当然这个在C++和python3里面已经可以通过开头加一个b解决了)
    • 为了对这些字面量进行约束, 给字面量施加类型类约束, 这样字面值5既可以在各种场合以各种类型出现(而不需要类型转换), 又使得字面量得到了一定的限制, 例如5+”abc”会报错这样.
    • 顺带一提, 因为5的类型划分不单一, 所以一些Haskell专家(见《Haskell函数式编程入门-第二版-第一卷》51-52页)直接把数字字面量视作”重载了的字面值”(overloaded literals)
      • 类似重载的行为:
        • 5作为一个”数字字面量”, 具有很多种类型, 相当于5在不同场合下自动进行了”类型重载”, 因此我们称5为”可重载的字面值”, 抑或”重载了的字面值”, 举例, 字面值5自身就可以作为Int和Integer两种类型, 不同场合就像被”自动化地重载”了一样
      • 类型类Num是从”类型集合”中提取出来的:
        • 虽然5没有确定的类型, 或者说5存在一个可供特定场合挑选的”(很多)类型(的)集合”, (集合长度>=3), 但是这个集合中的每一个”类型”, 不管你是Int还是Float还是Integer, 都必须给我实现Num类型类接口, 因为这个集合中所有的元素(也即5可以挑选的所有的”类型”)都必须实现Num类型类接口, 所以5就因此必须实现Num类型类接口.

目前存在的疑问

  • 定义函数时, 限定语句f :: Num a => a -> a存在的意义是”双重保险”吗? 能不能删除?

    • 在不严谨的场合下, 是否可以省略?
      • 例子, 我自己写斐波那契函数没写这行, runghc也不报错
    • 在严谨的场合下, 到底要多严谨?
      • 因为”Haskell类型推断系统”肯定很厉害, 那么他应该会帮我检查出类型的错误才对, 那么我再写一遍这个”限定语句”有两种情况:
        1. 我菜, 比推断系统想得少, 没有很好地限制死函数类型, 导致多态应用出错, 而如果我装傻不写, 这个错误就会在编译时被检查出来(存疑?不知道是不是编译时检查?), 怎样才是好的做法?
        2. 我自作聪明, (在特定需求下定制参数类型)比推断系统想得多, 但是如果限制得比较死(比如RGB参数限制在Word8这种比较小的区间下, 这个倒是Best Pratice, 我举不出那种”取值区间本没必要限制得那么死”例子, 反正就是这个意思),据说编译器特定优化后效率可得到提升, 这样要是以后需要”泛化版本”会不会带来麻烦?
          • 我不知道这个”范围限制”到底是我的责任还是Haskell推断系统的责任?
  • 既然声明类型变量(type-variables)时可以随意选用字母, 为什么在这里没有出一套naming conventions这样的限制来约束代码, 增强函数的可读性?

参考网站 & 参考书