Shapeless 入門指南(二):自然數型別 Nat

ScalaCool發表於2017-10-18

本文由 Jilen 發表在 ScalaCool 團隊部落格。

上一篇文章介紹了 shapeless 的重要功能:自動派生 typeclass 例項。 本文將闡述一個看起來沒什麼作用,但實際上是 shapeless 關於泛型程式設計的重要基石: Nat (自然數)

皮亞諾公理(Peano axioms)

首先我們確定一下自然數只是一個符號系統,我們用 0,1,2,... 這些符號表示一些抽象的概念

皮亞諾公理告訴我們這個符號系統有三個組成元素

  • 一個初始元素(比如0) x
  • 一個集合 X
  • 一個 X 到自身的對映(後繼關係) f

並且這個系統滿足以下公理

  1. x 屬於 X (0是自然數)
  2. x 不在 f 的值域內 (0 不是任何數的後繼)
  3. 如果 f(a) = f(b)a = b (即 f 是一個單射)
  4. 若 a 屬於 X,則 f(a) 屬於 X
  5. 若 A 為 X 子集,並滿足
  • x 屬於 A, 且
  • 若 a 屬於 A,則 f(a) 屬於 A 則 A = X

基於上述公理就可以建立一階算術系統

Nat 型別與自然數對應關係

trait Nat {
  type N <: Nat
}

case class Succ[P <: Nat]() extends Nat {
  type N = Succ[P]
}

class _0 extends Nat with Serializable {
  type N = _0
}
複製程式碼

很容易就可以總結出 Nat 型別和自然數的對應關係

  • X 為 所有 Nat 子型別
  • Succ(後繼)為對映 f,注意一個型別構造器可以看作是一個對映
  • _0 為初始元素

Nat 與上述公理對應關係

  • 第 1 條,_0 是 Nat 子型別
  • 第 2 條,_0 顯然不是任何型別的 Succ,scala 編譯器的型別檢查可以保證 Succ[P] 不等於 _0
  • 第 3 條,假設存在 A, B 滿足 A != B 且 Succ[A] = Succ[B],同樣編譯器型別檢查可以保證如果 A != B,則 Succ[A] != Succ[B],即 Succ 是一個單射
  • 第 4 條,Succ 的定義直接指出如果 P 是 Nat,則 Succ 亦是 Nat
  • 第 5 條,A 就是 Nat 型別的定義,(這裡形式化證明過於困難,暫不做證明)

加法定義

有了上述公理之後,可以建皮亞諾算術系統,我們以加法為例 加法定義為滿足以下關係的對映

  1. a + 0 = 0
  2. a + Succ(b) = Succ(a + b)

在 shapeless 裡,加法定義如下(Aux 型別的作用參考此處)


trait Sum[A <: Nat, B <: Nat] extends Serializable { type Out <: Nat }
object Sum {
    type Aux[A <: Nat, B <: Nat, C <: Nat] = Sum[A, B] { type Out = C }
    // 對應 1 處定義
    implicit def sum1[B <: Nat]: Aux[_0, B, B] = new Sum[_0, B] { type Out = B }
    // 此處定義與 2 處略有不同
    implicit def sum2[A <: Nat, B <: Nat, C <: Nat]
      (implicit sum : Sum.Aux[A, Succ[B], C]): Aux[Succ[A], B, C] =
      new Sum[Succ[A], B] { type Out = C }
}

複製程式碼

這裡第 2 條規則定義為 Sum[A, Succ[B]].C = Sum[Succ[A] , B].C,而加法的第二個規則則要求 Sum[A, Succ[B]].C = Succ[Sum[A, B].C] shapeless 這裡定義實際上可以推匯出第 2 規則。

將上述型別轉換成命題: a + S(b) = S(a) + b => a + S(b) = S(a + b)

下面是證明過程 (S 為後繼對映,即 Succ)

  • b = 0 時 a + S(0) = S(a) + 0 = S(a) = S(a + 0)
  • 假設 b = x 時, a + S(x) = S(a + x) 成立,則 b = S(x) 時 a + S(S(x)) = S(a) + S(x) = S(S(a) + x) = S(a + S(x)),可以得出對於 b = S(x) ,a + S(b) = S(a + b) 也成立
  • 上述兩者歸納得出命題成立

現在來看看如何使用 Sum 來約束型別

object alias {
  type _1 = Succ[_0]
  type _2 = Succ[_1]
  type _3 = Succ[_2]
}

import alias._

def check[A <: Nat, B <: Nat](implicit sum: Sum.Aux[A, B, _3]) = {}

check[_0, _3]
check[_1, _2]
check[_2, _1]
check[_3, _0]
check[_1, _1] // 編譯錯誤

複製程式碼

上述 check 方法要求兩個型別的 Sum_3,可以看只有 Sum_3AB 型別才能通過編譯

總結

本文介紹了 shapeless 的重要基礎型別 Nat,理解該型別是掌握 shapeless 其他型別的重要前提

Shapeless 入門指南(二):自然數型別 Nat

相關文章