美文网首页
Swift函数式编程五(QuickCheck)

Swift函数式编程五(QuickCheck)

作者: 酒茶白开水 | 来源:发表于2021-07-12 15:06 被阅读0次

    代码地址

    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), condition(vaule) 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: <) }
    

    相关文章

      网友评论

          本文标题:Swift函数式编程五(QuickCheck)

          本文链接:https://www.haomeiwen.com/subject/odnrpltx.html