美文网首页iOS 开发 iOS Developer
如何用 Swift 优雅地写一个 JSON 解析函数?(下)——

如何用 Swift 优雅地写一个 JSON 解析函数?(下)——

作者: Frain | 来源:发表于2016-06-28 15:09 被阅读0次

    如何用 Swift 优雅地写一个 JSON 解析函数?(上)—— 泛型及扩展
    阅读这一篇文章,你需要对函数式编程和协议有一定的了解。
    这里用到的代码我都写在了 playground 里,放到了 Github 上。
    如果你对函数式编程不甚了解,可以看一看我之前的文章:
    Swift 函数式编程探索(1)—— Functor 和 Monad
    Swift 函数式编程探索(2)——实践中使用 Functor 和 Monad
    Swift 函数式编程探索(3)—— Applicative 和 Curry

    我们接着往下说。

    5、使用 faltMap 、 Applicative 、以及 Curry

    这一部分,许多来自 @chriseidhof 大神的这篇文章 :Parsing JSON in Swift

    我们先来看看这一步之后的最终结果:

    func parseUser5(json: JSON?) -> User? {
        let curryUser = curry(User.init)
        return json >>- {
            curryUser <*> ("uid".fatch($0) >>- toInt64)
                <*> "nam".fatch($0)
                <*> "ada".fatch($0)
                <*> "sgt".fatch($0)
                <*> ("rgt".fatch($0) >>- toDate)
        }
    }
    let user5 = parseUser5(userJSON)
    

    相比之下,是不是简洁了很多?
    如果你觉得看不懂,那么我来一步一说明:

    let curryUser = curry(User.init)
    

    这是使用 curry 方法接收 User 的初始化器,返回一个柯里化的初始化方法,curry 是这样的:

    func curry<A,B,C,D,E,R>(fun: (A,B,C,D,E) -> R) -> A -> B -> C -> D ->E -> R {
        return { a in { b in { c in { d in { e in fun(a,b,c,d,e) } } } } }
    }
    

    curryUser其实是一个Int64 -> String -> Bool? -> String? -> NSDate? -> User类型
    继续往下看,>>-也是 flatMap ,是这样定义的 :

    infix operator  >>- { associativity left }
    
    public func >>- <U,T> (a : U? , @noescape f: U throws -> T?) rethrows -> T? {
        return try a.flatMap(f)
    }
    

    parseUser5 中 return 的那一行,表示的是 json flatMap 了一个闭包

    {
        curryUser <*> ("uid".fatch($0) >>- toInt64)
            <*> "nam".fatch($0)
            <*> "ada".fatch($0)
            <*> "sgt".fatch($0)
            <*> ("rgt".fatch($0) >>- toDate)
    } //(JSON) -> User?
    

    <*> 符号是一个 Apply ,它是这样定义的:

    extension Optional {
        func apply<T>(f: (Wrapped -> T)?) -> T? {
            if let f = f {
                return self.map(f)
            }
            return nil
        }
    }
    
    infix operator <*> { associativity left }
    
    func <*><U, T>(f: (U -> T)?, a: U?) -> T? {
        return a.apply(f)
    }
    

    ("uid".fatch($0) >>- toInt64) 表示将闭包接收到的参数进行 fatch 操作后(可能得到 nil) flatMap 函数 toInt64

    func toInt64(input: Int?) -> Int64? {
        return input.map{ Int64($0) }
    }
    

    上面的那个闭包中,curryUser 先是接受了一个可能为空的 Int64 (空就返回 nil ),得到一个 String -> Bool? -> String? -> NSDate? -> User类型。
    如此类推,这个闭包接收一个 JSON 类型的参数,返回一个 User 或者 nil
    但是这里其实是有一点问题的。
    在接受了 Int64 和 String 各表示 userId 和 name 之后,再往下的参数,是可以为空的,但是我们这里实现的 apply ,只要是任意一边为空,就会返回空。

    let otherJSON  = [
        "uid": 2,
        "nam": "FrainTest",
    //  "sgt": "buzy",
        "ada": true,
        "rgt": "2016-03-04"
    ]
    
    let other = parseUser5(otherJSON)  // nil
    

    换句话说,只要返回的 JSON 中不含有某一个参数即便是可以为空的参数,整个都会是空。
    怎么解决这个问题呢?
    我们可以这样再定义一个 apply 吗?

    extension Optional {
        func apply<T>(f: (Wrapped? -> T)?) -> T? {
            if let f = f {
                return f(self)
            }
            return nil
        }
    }
    
    func <*><U, T>(f: (U? -> T)?, a: U?) -> T? {
        return a.apply(f)
    }
    

    理论上来说,这是可以的。为什么说是理论上?
    经过测试,如果单一使用,是可以的。
    但是实际上,在这种情况,编译器并不能很容易地区分两个 apply ,在运行一段时间后,就会说这里 too complex
    所以我们定义一个新的操作符执行这种特定的 Apply:

    infix operator <%> { associativity left }
    
    func <%><U, T>(f: (U? -> T)?, a: U?) -> T? {
        return a.apply(f)
    }
    

    这样使用:

    func parseUser6(json: JSON?) -> User? {
        let curryUser = curry(User.init)
        return json >>- {
            curryUser <*> ("uid".fatch($0) >>- toInt64)
                <*> "nam".fatch($0)
                <%> "ada".fatch($0)
                <%> "sgt".fatch($0)
                <%> ("rgt".fatch($0) >>- toDate)
        }
    }
    
    let user6 = parseUser6(otherJSON)
    

    就没有问题了。
    接下来,我们就可以着手解析整个 json 了,同样使用函数式编程,这一次我们需要用到 SequenceType 的 flatMap ,补充上:

    public func >>- <S: SequenceType, T>(
        a: S, @noescape f: S.Generator.Element throws -> T?) rethrows -> [T] {
        return try a.flatMap(f)
    }
    

    于是我们就可以这样解析:

    func parseJSON(json: JSON?) -> [User]?  {
        let jsons: [JSON]? = json >>- "data".fatch >>- "users".fatch
        return jsons  >>- { $0 >>- parseUser6 }
    }
    
    parseJSON(json)
    

    这样,是不是足够优雅了呢?
    其实还可以再优雅一点,我们结合前面提到的泛型和扩展:

    6. 使用 Protocol

    我们定义一个这样的协议:

    protocol JSONDeserializable {
        init?(json: JSON?)
    }
    

    这个解析要求实现一个可失败的接收 JSON 的初始化器,我们让 User 类 adopt 这个协议:

    extension User: JSONDeserializable {
        init?(json: JSON?) {
            let curryUser = curry(User.init)
            guard let user = json >>- {
                curryUser <*> ("uid".fatch($0) >>- toInt64)
                    <*> "nam".fatch($0)
                    <%> "ada".fatch($0)
                    <%> "sgt".fatch($0)
                    <%> ("rgt".fatch($0) >>- toDate)
            } else { return nil }
            self = user
        }
    }
    

    这有什么用呢?我们可以结合扩展和泛型,这样写:

    extension String {
        func fromParse<T: JSONDeserializable>(json: JSON?) -> [T]?  {
            let jsons: [JSON]? = json >>- "data".fatch >>- self.fatch
            return jsons >>- { $0 >>- T.init }
        }
    }
    

    然后这样用:

    func doSomeThingWithUser(users: [User]) {
        print(users)
    }
    
    "users".fromParse(json) >>- doSomeThingWithUser
    

    假如我们以后又有了新的数据类型希望能够通过 JSON 创建,只要实现这个协议,就能通过这个泛型函数接受 JSON 来创建(毕竟一般JSON格式相同)

    最后

    到这里,我们的解析函数已经很简洁,也足够的优雅
    就像我一开始说的,这里的目的,是分享一种将 Swift 的各种特性好好利用的一种可能性,我相信还有更好的解决方法,也欢迎大家交流。

    相关文章

      网友评论

        本文标题:如何用 Swift 优雅地写一个 JSON 解析函数?(下)——

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