内容简介:Clojure 集合管道函数练习
TDD讨论组里的 申导
最近在B站直播了Martin Fowler的经典文章
Refactoring with Loops and Collection Pipelines
中谈到的 利用集合管道对循环进行函数式重构
。视频地址在 这里
,申导的翻译在 这里
。组织者 小波(Seaborn Lee)
趁机出了一道关于集合管道函数 题目
。我就想啊,论函数式编程,舍 Clojure
其谁?而且我在 Clojure
很少能写出 loop... recur
这样偏底层的循环代码。话不多说,撸起袖子开工。
题目
一家澡堂有 m 个房间,每个房间有 n 个时段,现在要给用户推荐「最早的可以预约的时段」。
例子:
rooms: [ { room_id:1, periods: [ { time:'17:00-18:00', status:'available' }, { time:'18:00-19:00', status:'occupied' } ] }, { room_id:2, periods: [ { time:'17:00-18:00', status:'occupied' }, { time:'18:00-19:00', status:'available' } ] } ]
期望返回:
{ room_id:1, time: '17:00-18:00' }
解析
题目很简单,基本思路:首先过滤出每个房间 periods
中 status
为 available
的时间段,然后取第一个也就是最早的时间段(默认为递增 排序 的),接着将 room_id
和这个时间段以 期望返回
的形式合并。再然后对所有合并的结果依据时间段进行一次排序( sort
),最后取第一个结果即可。
1. Clojure 解法
转换数据格式
原题中给的是json的格式,不适合在 Clojure 中处理,所以我们手工转换成需要的形式,如下:
清单1-1数据定义
(defrooms [{:room-id1 :periods[{:time"17:00-18:00" :status:available} {:time"18:00-19:00" :status:occupied}]} {:room-id2 :periods[{:time"17:00-18:00" :status:occupied} {:time"18:00-19:00" :status:available}]}])
代码
清单1-2房间最早可预约时间段
(defn the-earliest-available-room [rooms] (->> rooms (map (juxt first (fn [{:keys [periods]}] (->> periods (filter #(= (:status %) :available)) (ffirst))))) (map #(into {} %)) (sort-by :time) (first))) (the-earliest-available-room rooms) -> {:room-id 1, :time "17:00-18:00"}
这段代码和上面的解析是一一对应的关系。为了让程序清晰,符合管道的用法,这里使用了thread last宏( ->>
),它的作用是把前面一个 form
作为后一个 form
的最后一个参数。与之呼应的是thread first宏( ->
),它的作用类似,不过会传成第一个参数。
我们先看 (map (juxt ...) ...)
这一段代码。 juxt
是一个非常有意思的函数,而且超级实用。它的文档描述如下:
(doc juxt) -> clojure.core/juxt [f] [f g] [f g h] [f g h & fs] Added in 1.1 Takes a set of functions and returns a fn that is the juxtaposition of those fns. The returned fn takes a variable number of args, and returns a vector containing the result of applying each fn to the args (left-to-right). ((juxt a b c) x) => [(a x) (b x) (c x)]
它的神奇之处在于可以对同一个参数应用不同的函数,而且还能将应用的结果全部收集起来。想想题目的解析中提及的以 期望返回
的形式合并,如果我们应用 juxt
函数,就能得到 [(:room-id 1) (:time "17:00-18:00")]
这样的中间结果。
(juxt first (fn ...))
中 first
用于提取 :room-id
,而后面的 lambda
表达式则用于提取 :time
。解法很直观,筛选出 :status
是 :available
的时间段,然后使用 (ffirst)
取第一个map的首个entry。如: {:time "17:00-18:00" :status :available}
,那么应用 (ffirst)
的结果就是 [:time "17:00-18:00"]
。
接下来,又进行了一次map操作,这次的目的是把元组的entries,转换为map。举个例子: [[:room-id 1] [:time "17:00-18:00"]]
=> {:room-id 1 :time "17:00-18:00"}
。转换成map之后,方便以 :time
对结果进行排序 (sort-by :time)
,最后取出第一个元素 (first)
,即我们期望的返回。
写完之后,我很想再写个TDD版本的。话不多说,继续撸袖子。
2. Clojure TDD 解法
环境准备
-
生成工程
进入命令行,输入
lein new midje the-earliest-available-period-of-bathroom
,leiningen会生成基于midje
这个测试框架的工程。 -
Git
git init > .gitignore .lein* .nrep* target/ 这里ctrl-c退出 git add. git commit --message "init commit"
我使用了 zsh
和 oh-my-zsh
,自带了很多git操作的alias,可以通过 alias |grep git
查看。后续的git操作都会使用alias。
-
自动测试
输入
lein repl
,然后(use 'midje.repl)
,最后输入(autotest)
。这样一旦文件修改保存,测试就会自动触发。 -
Emacs
用来写代码的。
Tasking(任务拆分)
先不急着敲代码,我们先从测试的角度看看完成这个需求需要哪几步?
- [ ] 单间澡堂有一个可用时间段
- [ ] 单间澡堂有多个可用时间段
- [ ] 所有澡堂(包含输入为空)没有可用时间段
- [ ] 多间澡堂都有可用时间段
- [ ] 多间澡堂中有的有可用时间段,有得没有可用时间段
第1个任务
- [ ] 单间澡堂有一个可用时间段
1. 写测试
(def room-1 {:room-id 1 :periods [{:time "17:00-18:00" :status :available} {:time "18:00-19:00" :status :occupied}]}) (def room-2 {:room-id 2 :periods [{:time "17:00-18:00" :status :occupied} {:time "18:00-19:00" :status :available}]}) (facts "about `the-earliest-avaible-period-of-bathroom`" (fact "should recommand if there is only one room with available period" ;; 1号 (the-earliest-available-recommand [room-1]) => {:room-id 1 :time "17:00-18:00"}) ;; 2号 (the-earliest-available-recommand [room-2]) => {:room-id 2 :time "18:00-19:00"}))
2. 写实现
(defnthe-earliest-available-recommand [rooms] {:room-id1:time"17:00-18:00"})
针对1号测试,这个实现有点“荒诞”,术语 hard code
说的就是这个,但是眼下足够了。不过此时,应该再写一个类似的测试来去除 hard code
,即2号测试。
相应地,我们要修改实现。
defn the-earliest-available-recommand [rooms] (let [{:keys [room-id periods]} (first rooms) available-periods (filter #(#{:available} (:status %)) periods)] (merge {:room-id room-id} (select-keys (first available-periods) [:time]))))
3. 关闭并提交
-
[x] 单间澡堂有一个可用时间段
ga . gcmsg "one available room"
第2个任务
- [ ] 单间澡堂有多个可用时间段
1. 写测试
(def room-3 {:room-id 3 :periods [{:time "17:00-18:00" :status :occupied} {:time "18:00-19:00" :status :available} {:time "19:00-20:00" :status :available}]}) ... (fact "should recommand the earliest one if there is only one room with multiple available periods" (the-earliest-available-recommand [room-3]) => {:room-id 3 :time "18:00-19:00"})
保存,发现测试还是跑过了。原因在于我们默认了 period
是递增排序的。看看有没有重构点?实现太简单了,那就欢欢喜喜地跳过实现步骤。
2. 关闭并提交
-
[x] 单间澡堂有多个可用时间段
ga . gcmsg "one room with multiple available periods"
第3个任务
-
[ ] 所有澡堂(包含输入为空)没有可用时间段
1. 写测试
(def non-available-room {:room-id 4 :periods [{:time "17:00-18:00" :status :occupied} {:time "18:00-19:00" :status :occupied} {:time "19:00-20:00" :status :occupied}]}) (fact "should show `:no-available-room` if there is no available room" (the-earliest-available-recommand []) => :no-available-room (the-earliest-available-recommand [non-available-room]) => :no-available-room))
这回肯定挂掉。
2. 写实现
(defnthe-earliest-available-recommand [rooms] (let[{:keys[room-id periods]} (firstrooms) available-periods (filter#(#{:available} (:status%)) periods)] (if(seqavailable-periods) (merge{:room-idroom-id} (select-keys(firstavailable-periods) [:time])) :no-available-room)))
这里使用了 Clojure
中判断集合是否为空较为常用的手法 (seq )
,如果集合非空,那么返回集合本身;反之,返回nil,nil在逻辑上是false。测试通过。
3. 关闭并提交
-
[x] 所有澡堂(包含输入为空)没有可用时间段
ga . gcmsg "no available room"
第4个任务
- [ ] 多间澡堂都有可用时间段
1. 写测试
(fact"should recommand the earliest if there has more than one room and each has available periods" (the-earliest-available-recommand[room-1 room-2]) => {:room-id1:time"17:00-18:00"} (the-earliest-available-recommand[room-2 room-1]) => {:room-id1:time"17:00-18:00"} (the-earliest-available-recommand[room-2 room-3]) => {:room-id2:time"18:00-19:00"} (the-earliest-available-recommand[room-1 room-2 room-3]) => {:room-id1:time"17:00-18:00"})
2. 写实现
(defnthe-earliest-available-recommand [rooms] (if(seqrooms) (first(sort-by:time (map(fn[room] (let[{:keys[room-id periods]} room available-periods (filter#(#{:available} (:status%)) periods)] (if(seqavailable-periods) (merge{:room-idroom-id} (select-keys(firstavailable-periods) [:time])) :no-available-room))) rooms))) :no-available-room))
到这里,我们开始使用 (map )
函数处理多个房间的内容。注意,当输入房间是空集合的时候,这里需要相应地做 (seq rooms)
判空处理,否则会返回nil,而不是我们想要的 :no-available-room
。
3. 关闭并提交
-
[x] 多间澡堂都有可用时间段
ga . gcmsg "more than one room"
4. 重构
代码写到这里,再不重构就说不过去了。另外,管道没看到,倒是看到一堆括号。
我们使用thread last (->> )
做一次重构:
(defnthe-earliest-available-recommand [rooms] (if(seqrooms) (->>rooms (map(fn[room] (let[{:keys[room-id periods]} room available-periods (filter#(#{:available} (:status%)) periods)] (if(seqavailable-periods) (merge{:room-idroom-id} (select-keys(firstavailable-periods) [:time])) :no-available-room)))) (sort-by:time) first) :no-available-room))
还行,至少没那么多嵌套了。提交一次。
ga . gcmsg "[refactor] usemacrothread-last->>topipe"
继续重构,使用我们的 juxt
函数。
(defnthe-earliest-available-recommand [rooms] (letfn[(period[{:keys[periods]}] (->>periods (filter#(#{:available} (:status%))) ffirst (#(or% [:time::non-available]))))] (->>rooms (map(fn[room] (applyconj {} ((juxtfirst period) room)))) (remove#(#{::non-available} (:time%))) (sort-by:time) first (#(or%:no-available-room)))))
看上去还行,不过不爽的是 (#(or % [:time ::non-available]))
,为了迎合 (->> )
给这个方法包了一层,因为我是想将前面表达式的结果放到 (or )
第一个参数的位置,但是 (->> )
会让它出现在最后一个参数的位置。有没有什么好看的方法解决呢?当然有!
(defnthe-earliest-available-recommand [rooms] (letfn[(period[{:keys[periods]}] (->periods (->>(filter#(#{:available} (:status%))) ffirst) (or[:time::non-available])))] (->rooms (->>(map(fn[room] (applyconj {} ((juxtfirst period) room)))) (remove#(#{::non-available} (:time%))) (sort-by:time) first) (or:no-available-room))))
我们可以使用 (-> )
来做到这点,顿时觉得世界干净了不少。再提交一次。
ga . gcmsg "[refactor] usejuxttoextractneededfields"
第5个任务
-
[ ] 多间澡堂中有的有可用时间段,有得没有可用时间段
1. 写测试
(fact"should recommand the earliest available room even if there has non available room" (the-earliest-available-recommand[room-1 non-available-room]) => {:room-id1:time"17:00-18:00"} (the-earliest-available-recommand[room-2 non-available-room]) => {:room-id2:time"18:00-19:00"})
测试直接通过,又可以跳过实现代码了。不过,这也预示着我们的测试是有覆盖的,也需要花时间整理这些测试用例。在那之前,先提交一下。
2. 关闭并提交
-
[x] 多间澡堂中有的有可用时间段,有得没有可用时间段
ga . gcmsg "mixed non-available and available rooms"
为第3个任务补上测试用例
- [x] 所有(包含多个)澡堂(包含输入为空)没有可用时间段
(fact "should show `:no-available-room` if there is no available room" (the-earliest-available-recommand []) => :no-available-room (the-earliest-available-recommand [non-available-room]) => :no-available-room (the-earliest-available-recommand [non-available-room non-available-room]) => :no-available-room))
这里的第3个用例其实和第2个有重叠,我们待会整理掉。先提交一下。
ga . gcmsg "multiple non-available rooms"
整理测试
在前面进行的任务当中,我们发现有两次没有写实现测试就通过的情况。这说明测试用例是有覆盖的。
- 第2个任务的测试用例其实覆盖了第1个任务的测试用例,所以可以直接删去后者;
- 第5个任务的测试用例覆盖了第4个任务的部分测试用例,所以可以合并到一起。
整理下来,最终的测试变成下面这样:
(facts"about `the-earliest-avaible-period-of-bathroom`" (fact"should recommand the earliest one if there is only one room with multiple available periods" (the-earliest-available-recommand[room-3]) => {:room-id3:time"18:00-19:00"}) (fact"should show `:no-available-room` if there is no available room" (the-earliest-available-recommand[]) =>:no-available-room (the-earliest-available-recommand[non-available-room non-available-room]) =>:no-available-room) (fact"should recommand the earliest if there has more than one room and each may have available periods" (the-earliest-available-recommand[room-1 room-2]) => {:room-id1:time"17:00-18:00"} (the-earliest-available-recommand[room-2 room-1]) => {:room-id1:time"17:00-18:00"} (the-earliest-available-recommand[room-2 room-3]) => {:room-id2:time"18:00-19:00"} (the-earliest-available-recommand[room-1 room-2 room-3]) => {:room-id1:time"17:00-18:00"} (the-earliest-available-recommand[room-1 non-available-room]) => {:room-id1:time"17:00-18:00"}))
文档
The final goal of any engineering activity is some type of documentation.
更新README.md文件,其中描述程序解决的问题以及运行步骤,当然包含设计思路那更好了。提交一下。
ga . gcmsg "update readme"
美化代码
代码是诗行 - by lambeta
什么是好看的代码?除了清晰明了,格式也必须产生美感。
(defnthe-earliest-available-recommand [rooms] (letfn[(period[{:keys[periods]}] (->periods (->>(filter#(#{:available} (:status%)))) (ffirst); 统一套上括号 (or[:time::non-available])))] (->rooms (->>(map(juxtfirst period)) (map#(into{} %)); 合并单独提出来 (remove#(#{::non-available} (:time%))) (sort-by:time) (first)); 统一套上括号 (or:no-available-room))))
顺眼不少,最后提交一下。
ga . gcmsg "[refactor] beautify pipe format"
[1] 本文样例
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 函数式编程之组合与管道
- Java 8 习惯用语,第 2 部分: 函数组合与集合管道模式
- 我可以使用F#中的管道运算符将参数传递给构造函数吗?
- 速度不够,管道来凑——Redis管道技术
- Golang pipline泛型管道和类型管道的性能差距
- Linux 管道那些事儿
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。