如何用 Swift 优雅地写一个 JSON 解析函数?(下)—— 函数式编程以及协议

如何用 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 的各种特性好好利用的一种可能性,我相信还有更好的解决方法,也欢迎大家交流。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335

推荐阅读更多精彩内容