[英]Converting an imperative algorithm into functional style
我编写了一个简单的过程来计算Java项目中某些特定软件包的测试覆盖率的平均值。 巨大的html文件中的原始数据如下所示:
<body>
package pkg1 <line_coverage>11/111,<branch_coverage>44/444<end>
package pkg2 <line_coverage>22/222,<branch_coverage>55/555<end>
package pkg3 <line_coverage>33/333,<branch_coverage>66/666<end>
...
</body>
例如,给定指定的软件包“ pkg1”和“ pkg3”,平均行覆盖范围是:
(11 + 33)/(111 + 333)
平均分支覆盖率为:
(44 + 66)/(444 + 666)
我编写了以下步骤来获得结果,并且效果很好。 但是如何以功能样式实现此计算呢? 类似于“(如果...,对于...,对于...,对于...,对于...,b)。 我对Erlang,Haskell和Clojure有所了解,因此也欢迎使用这些语言的解决方案。 非常感谢!
from __future__ import division
import re
datafile = ('abc', 'd>11/23d>34/89d', 'e>25/65e>13/25e', 'f>36/92f>19/76')
core_pkgs = ('d', 'f')
covered_lines, total_lines, covered_branches, total_branches = 0, 0, 0, 0
for line in datafile:
for pkg in core_pkgs:
ptn = re.compile('.*'+pkg+'.*'+'>(\d+)/(\d+).*>(\d+)/(\d+).*')
match = ptn.match(line)
if match is not None:
cvln, tlln, cvbh, tlbh = match.groups()
covered_lines += int(cvln)
total_lines += int(tlln)
covered_branches += int(cvbh)
total_branches += int(tlbh)
print 'Line coverage:', '{:.2%}'.format(covered_lines / total_lines)
print 'Branch coverage:', '{:.2%}'.format(covered_branches/total_branches)
在下面,您可以找到我的Haskell解决方案。 我将尝试解释我在撰写本文时所经历的重点。
首先,您会发现我为coverage数据创建了一个数据结构。 创建数据结构以表示要处理的任何数据通常是一个好主意。 这部分是因为它使您可以轻松地设计代码,从而可以根据自己的设计进行思考–与函数式编程理念密切相关;部分原因是,它可以消除一些您认为自己在做某事的错误,但是实际上正在做其他事情。
与之前的观点有关:我要做的第一件事是将字符串表示的数据转换为我自己的数据结构。 在进行函数式编程时,通常会以“扫描”方式进行操作。 您没有一个将数据转换为格式,过滤掉不需要的数据并汇总结果的函数。 对于这些任务,您具有三种不同的功能,并且一次只能完成一项!
这是因为功能非常容易组合 ,也就是说,如果您有三个不同的功能,则可以将它们粘在一起以形成一个单一的功能。 如果您从一个开始,很难将其分解成三个不同的部分。
除非您专门进行Haskell,否则转换函数的实际工作实际上并不有趣。 它所做的只是尝试将每个字符串与一个正则表达式匹配,如果成功,它将覆盖率数据添加到结果列表中。
再一次,疯狂的创作即将发生。 我没有创建用于遍历coverage列表并将其汇总的函数。 我创建了一个函数来汇总两个 coverage,因为我知道我可以将其与专门的fold
循环(类似于类固醇的for
循环)一起使用,以汇总列表中的所有coverage。 我不需要自己重新发明轮子并自己创建一个循环。
此外,我的sumCoverages
函数可用于许多专门的循环,因此我不必编写大量函数,只需将单个函数粘贴到大量预制库函数中!
在main
功能中,您将看到我对数据进行“扫描”或“传递”编程的含义。 首先,我将其转换为内部格式,然后过滤掉不需要的数据,然后总结剩余的数据。 这些是完全独立的计算。 那是函数式编程。
您还将注意到,我在那里使用了两个专用循环, filter
和fold
。 这意味着我不必自己编写任何循环,我只需要将函数粘贴到这些标准库循环中,然后从那里接管即可。
import Data.Maybe (catMaybes)
import Data.List (foldl')
import Text.Printf (printf)
import Text.Regex (matchRegex, mkRegex)
corePkgs = ["d", "f"]
stats = [
"d>11/23d>34/89d",
"e>25/65e>13/25e",
"f>36/92f>19/76"
]
format = mkRegex ".*(\\w+).*>([0-9]+)/([0-9]+).*>([0-9]+)/([0-9]+).*"
-- It might be a good idea to define a datatype for coverage data.
-- A bit of coverage data is defined as the name of the package it
-- came from, the lines covered, the total amount of lines, the
-- branches covered and the total amount of branches.
data Coverage = Coverage String Int Int Int Int
-- Then we need a way to convert the string data into a list of
-- coverage data. We do this by regex. We try to match on each
-- string in the list, and then we choose to keep only the successful
-- matches. Returned is a list of coverage data that was represented
-- by the strings.
convert :: [String] -> [Coverage]
convert = catMaybes . map match
where match line = do
[name, cl, tl, cb, tb] <- matchRegex format line
return $ Coverage name (read cl) (read tl) (read cb) (read tb)
-- We need a way to summarise two coverage data bits. This can of course also
-- be used to summarise entire lists of coverage data, by folding over it.
sumCoverage (Coverage nameA clA tlA cbA tbA) (Coverage nameB clB tlB cbB tbB) =
Coverage (nameA ++ nameB ++ ",") (clA + clB) (tlA + tlB) (cbA + cbB) (tbA + tbB)
main = do
-- First we need to convert the strings to coverage data
let coverageData = convert stats
-- Then we want to filter out only the relevant data
relevantData = filter (\(Coverage name _ _ _ _) -> name `elem` corePkgs) coverageData
-- Then we need to summarise it, but we are only interested in the numbers
Coverage _ cl tl cb tb = foldl' sumCoverage (Coverage "" 0 0 0 0) relevantData
-- So we can finally print them!
printf "Line coverage: %.2f\n" (fromIntegral cl / fromIntegral tl :: Double)
printf "Branch coverage: %.2f\n" (fromIntegral cb / fromIntegral tb :: Double)
以下是适用于您的代码的一些速成,未经测试的想法:
import numpy as np
import re
datafile = ('abc', 'd>11/23d>34/89d', 'e>25/65e>13/25e', 'f>36/92f>19/76')
core_pkgs = ('d', 'f')
covered_lines, total_lines, covered_branches, total_branches = 0, 0, 0, 0
for pkg in core_pkgs:
ptn = re.compile('.*'+pkg+'.*'+'>(\d+)/(\d+).*>(\d+)/(\d+).*')
matches = map(datafile, ptn.match)
statsList = [map(int, match.groups()) for match in matches if matches]
# statsList is a list of [cvln, tlln, cvbh, tlbh]
stats = np.array(statsList)
covered_lines, total_lines, covered_branches, total_branches = stats.sum(axis=1)
好了,正如您所看到的,我没有费心去完成剩余的循环,但是我认为到此为止。 当然,实现这一目标的方法不止一种。 我选择炫耀map()
(有些人会说这会使效率降低,而且可能确实如此),以及NumPy来完成(公认的轻量级)数学。
这是相应的Clojure解决方案:
(defn extract-data
"extract 4 integer from a string line according to a package name"
[pkg line]
(map read-string
(rest (first
(re-seq
(re-pattern
(str pkg ".*>(\\d+)/(\\d+).*>(\\d+)/(\\d+)"))
line)))))
(defn scan-lines-by-pkg
"scan all string lines and extract all data as integer sequences
according to package names"
[pkgs lines]
(filter seq (for [pkg pkgs
line lines]
(extract-data pkg line))))
(defn sum-data
"add all data in valid lines together"
[pkgs lines]
(apply map + (scan-lines-by-pkg pkgs lines)))
(defn get-percent
[covered all]
(str (format "%.2f" (float (/ (* covered 100) all))) "%"))
(defn get-cov
[pkgs lines]
{:line-cov (apply get-percent (take 2 (sum-data pkgs lines)))
:branch-cov (apply get-percent (drop 2 (sum-data pkgs lines)))})
(get-cov ["d" "f"] ["abc" "d>11/23d>34/89d" "e>25/65e>13/25e" "f>36/92f>19/76"])
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.