Swift函數式編程五(QuickCheck)

代碼地址

QuickCheck是一個用於隨機測試的Haskell庫,相對於獨立的單元測試,QuickCheck描述函數抽象特性並生成測試來驗證這些特性。

生成隨機數

定義一個表達如何生成隨機數的協議:

protocol Arbitrary {
    static func arbitrary() -> Self
}

擴展Int類型,實現Arbitrary協議,在這裏使用了arc4random函數來生成隨機數然後轉換成Int類型,事實上應該要能夠生成負整數的:

extension Int: Arbitrary {
    static func arbitrary() -> Int {
        Int(arc4random())
    }
    static func random(from: Int, to: Int) -> Int {
        from + arbitrary()%(to - from)
    }
}

生成隨機字符,僅限大寫字母:

extension Character: Arbitrary {
    static func arbitrary() -> Character {
        Character(Unicode.Scalar(Int.random(from: 65, to: 90))!)
    }
}

生成隨機字符串,在這裏只生成長度0~40的隨機大寫字母的字符串:

extension String: Arbitrary {
    static func arbitrary() -> String {
        let randomLength = Int.random(from: 0, to: 40)
        let randomCharacters = (0..<randomLength).map { _ in Character.arbitrary() }
        
        return String(randomCharacters)
    }
}

實現check函數

實現第一個版本的檢驗函數:

func check1<A: Arbitrary>(message: String, property: (A) -> Bool) {
    let numberOfIterations = 100
    for _ in 0..<numberOfIterations {
        let value = A.arbitrary()
        guard property(value) else {
            print("\(message) 沒有通過測試:\(value)")
            return
        }
    }
    print("\(message) 通過了\(numberOfIterations)次測試")
}

下面使用check1函數來檢查:

extension CGSize: Arbitrary {
    var area: CGFloat { width*height }
    static func arbitrary() -> CGSize {
        CGSize(width: Int.arbitrary(), height: Int.arbitrary())
    }
}
check1(message: "CGSize的面積最小是0") { (size: CGSize) in size.area >= 0 }

縮小範圍

通常反例所處的範圍越小越容易定位到失敗的原因,所以原則上對失敗的輸入進行不斷縮減,並重新測試。

因此需要定義一個Smaller協議用於縮減範圍:

protocol Smaller {
    static func smaller() -> Self?
}

由於縮小數據範圍本身不是很明確,例如無法對空數組縮小隻能返回nil。

對所需數據類型實現Smaller協議:

extension Int: Smaller {
    func smaller() -> Int? {
        self == 0 ? nil : self/2
    }
}
extension String: Smaller {
    func smaller() -> String? {
        isEmpty ? nil : String(dropFirst())
    }
}

爲了使check函數中隨機生成的數據能夠縮小,可以將重新定義Arbitrary協議繼承Smaller協議:

protocol Arbitrary: Smaller {
    static func arbitrary() -> Self
}

反覆縮小範圍

定義一個遞歸函數iterateWhile,只要條件成了就反覆調用自身:

func iterateWhile<A>(condition: (A) -> Bool, inital: A, next: (A) -> A?) -> A {
    guard let vaule = next(inital) else {
        return inital
    }
    
    return iterateWhile(condition: condition, inital: vaule, next: next)
}

通過iterateWhile函數反覆縮小檢查函數中反例數據的範圍:

func check2<A: Arbitrary & Smaller>(message: String, property: (A) -> Bool) {
    let numberOfIterations = 100
    for _ in 0..<numberOfIterations {
        let value = A.arbitrary()
        guard property(value) else {
            let smaller = iterateWhile(condition: { !property($0) }, inital: value, next: { $0.smaller() })
            print("\(message) 沒有通過測試:\(smaller)")
            return
        }
    }
    print("\(message) 通過了\(numberOfIterations)次測試")
}

隨機數組

寫一個快速排序的函數:

func qsort(array: [Int]) -> [Int] {
    var data = array
    if data.isEmpty {
        return []
    }
    let pivot = data.removeFirst()
    let lesser = data.filter { $0 >= pivot }
    let gretter = data.filter { $0 < pivot }
    
    return qsort(array: lesser) + [pivot] + qsort(array: gretter)
}

現在想要使用check2函數來判斷這個快速排序函數與內置的sort函數是否有區別,那麼需要讓[Int]遵守Arbitrary、Smaller這兩個協議:

extension Array: Arbitrary, Smaller where Element: Arbitrary {
    static func arbitrary() -> Array<Element> {
        let randomLength = Int.random(from: 0, to: 50)
        return (0..<randomLength).map { _ in Element.arbitrary() }
    }
    
    func smaller() -> Array<Element>? {
        if self.isEmpty { return nil }
        return Array(self.dropFirst())
    }
}

調用check2函數對前邊的快速排序函數進行驗證:

check2(message: "qsort函數和Array內置sort函數效果一致") { (array: [Int]) in qsort(array: array) == array.sorted(by: <) }

check2函數需要類型A遵守Arbitrary、Smaller兩個協議。換一種方式,可以將必要的smaller、arbitrary函數作爲參數傳入。

首先定義包含必要的smaller、arbitrary函數的輔助結構體:

struct ArbitraryInstance<T> {
    let arbitrary: () -> T
    let smaller: (T) -> T?
}

接着寫一個接受ArbitraryInstance作爲參數的輔助函數checkHelper,這個函數的定義參照了check2函數,不同點就是arbitrary和smaller函數定義的位置。check2中被泛型的協議所約束,而checkHelper中則通過ArbitraryInstance結構體顯式傳遞:

func checkHelper<A>(arbitraryInstance: ArbitraryInstance<A>, message: String, property: (A) -> Bool) {
    let numberOfIterations = 100
    for _ in 0..<numberOfIterations {
        let value = arbitraryInstance.arbitrary()
        guard property(value) else {
            let smallerValue = iterateWhile(condition: property, inital: value, next: arbitraryInstance.smaller)
            print("\(message) 沒有通過測試:\(smallerValue)")
            return
        }
    }
    print("\(message) 通過了\(numberOfIterations)次測試")
}

顯式將所需信息作爲參數傳遞,而不是定義於協議中。這樣做靈活性更高,不依賴Swift推斷所需信息,完全自己控制這一切。

可是使用checkHelper函數重新定義check2函數:

func check3<X: Arbitrary>(message: String, property: ([X]) -> Bool) {
    let instance = ArbitraryInstance(arbitrary: Array<X>.arbitrary) { $0.smaller() }
    checkHelper(arbitraryInstance: instance, message: message, property: property)
}

終於可以運行check3函數驗證快速排序了:

check3(message: "qsort函數和Array內置sort函數效果一致") { (x: [Int]) -> Bool in qsort(array: x) == x.sorted(by: <) }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章