Using functions to extract boilerplate code
https://swlaschin.gitbooks.io/fsharpforfunandprofit/content/posts/conciseness-extracting-boilerplate.html
在本系列的第一个示例中,我们看到了一个计算平方和的简单函数,它是用 F# 和 C# 实现的。现在假设我们想要一些类似的新功能,例如:
计算N以内的所有数字的乘积
计算N以内的奇数之和
N 以内数的交替求和【一正一负求和】
显然,所有这些要求都是相似的,但是您将如何提取任何共同的功能呢?
让我们先从 C# 中的一些简单实现开始:
public static int Product(int n)
{
int product = 1;
for (int i = 1; i <= n; i++)
{
product *= i;
}
return product;
}
public static int SumOfOdds(int n)
{
int sum = 0;
for (int i = 1; i <= n; i++)
{
if (i % 2 != 0) { sum += i; }
}
return sum;
}
public static int AlternatingSum(int n)
{
int sum = 0;
bool isNeg = true;
for (int i = 1; i <= n; i++)
{
if (isNeg)
{
sum -= i;
isNeg = false;
}
else
{
sum += i;
isNeg = true;
}
}
return sum;
}所有这些实现有什么共同点?循环逻辑!作为程序员,我们被告知要记住 DRY 原则(“不要重复自己”),但在这里我们每次都重复了几乎完全相同的循环逻辑。让我们看看是否可以仅提取这三种方法之间的差异:
Product
product=1
对i进行连乘
SumOfOdds
sum=0
对基数值进行连加
AlternatingSum
int sum = 0 bool isNeg = true
使用 isNeg 标志来决定是加还是减,并为下一次传递翻转标志。还有把数值加上去
有没有一种方法可以剥离重复的代码,只关注设置和内循环逻辑?是的,有的。下面是F#中同样的三个函数:
let product n =
let initialValue = 1
let action productSoFar x = productSoFar * x
[1..n] |> List.fold action initialValue
//test
product 10
let sumOfOdds n =
let initialValue = 0
let action sumSoFar x = if x%2=0 then sumSoFar else sumSoFar+x
[1..n] |> List.fold action initialValue
//test
sumOfOdds 10
let alternatingSum n =
let initialValue = (true,0)
let action (isNeg,sumSoFar) x = if isNeg then (false,sumSoFar-x)
else (true ,sumSoFar+x)
[1..n] |> List.fold action initialValue |> snd
//test
alternatingSum 100所有这三个函数都具有相同的模式:
设置初始值
设置一个动作函数,在循环内的每个元素上执行。
调用库函数List.fold。这是一个功能强大的通用函数,它从初始值开始,然后依次为列表中的每个元素运行操作函数。
动作函数总是有两个参数:运行总计(或状态)和要作用的列表元素(在上面的示例中称为“x”)。
在最后一个函数alternatingSum中,你会注意到它使用了一个元组(一对值)作为初始值和动作的结果。这是因为运行中的总数和isNeg标志都必须传递给循环的下一次迭代--没有可以使用的“全局”值。折叠的最终结果也是一个元组,所以我们必须使用"snd"(第二)函数来提取我们想要的最终总数。
通过使用 List.fold 并完全避免任何循环逻辑,F# 代码获得了许多好处:
关键的程序逻辑得到了强调和明确。各项功能之间的重要区别变得非常清楚,而共同点则被放到了次要位置
消灭了模板循环代码,因此,代码比C#版本更加紧凑(F#代码4-5行,C#代码至少9行)。
循环逻辑中不可能有错误(如off-by-one),因为这个逻辑没有暴露给我们。
顺便说一下,平方和的例子也可以用 fold 来写:
let sumOfSquaresWithFold n =
let initialValue = 0
let action sumSoFar x = sumSoFar + (x*x)
[1..n] |> List.fold action initialValue
//test
sumOfSquaresWithFold 100"Fold" in C#
你能在C#中使用 "fold"方法吗?可以。LINQ确实有一个与fold相当的东西,叫做Aggregate。这里是为使用它而重写的C#代码:
public static int ProductWithAggregate(int n)
{
var initialValue = 1;
Func<int, int, int> action = (productSoFar, x) =>
productSoFar * x;
return Enumerable.Range(1, n)
.Aggregate(initialValue, action);
}
public static int SumOfOddsWithAggregate(int n)
{
var initialValue = 0;
Func<int, int, int> action = (sumSoFar, x) =>
(x % 2 == 0) ? sumSoFar : sumSoFar + x;
return Enumerable.Range(1, n)
.Aggregate(initialValue, action);
}
public static int AlternatingSumsWithAggregate(int n)
{
var initialValue = Tuple.Create(true, 0);
Func<Tuple<bool, int>, int, Tuple<bool, int>> action =
(t, x) => t.Item1
? Tuple.Create(false, t.Item2 - x)
: Tuple.Create(true, t.Item2 + x);
return Enumerable.Range(1, n)
.Aggregate(initialValue, action)
.Item2;
}好吧,在某种意义上,这些实现比原始的 C# 版本更简单、更安全,但是来自泛型类型的所有额外噪音使得这种方法远不如 F# 中的等效代码优雅。您会明白为什么大多数 C# 程序员更愿意坚持使用显式循环。
一个更相关的例子
在现实世界中经常出现的一个稍微相关的例子是,当元素是类或结构时,如何获得一个列表的 "最大 "元素。LINQ方法 "max "只返回最大值,而不是包含最大值的整个元素。
这是一个使用显式循环的解决方案:
public class NameAndSize
{
public string Name;
public int Size;
}
public static NameAndSize MaxNameAndSize(IList<NameAndSize> list)
{
if (list.Count() == 0)
{
return default(NameAndSize);
}
var maxSoFar = list[0];
foreach (var item in list)
{
if (item.Size > maxSoFar.Size)
{
maxSoFar = item;
}
}
return maxSoFar;
}Doing this in LINQ seems hard to do efficiently (that is, in one pass), and has come up as a Stack Overflow question. Jon Skeet event wrote an article about it.
在LINQ中做到这一点似乎很难有效地做到(也就是一次完成),并且已经作为一个Stack Overflow问题出现了。Jon Skeet事件写了一篇关于它的文章。
再一次的,由fold来拯救程序!
这里是使用Aggregate的C#代码:
public class NameAndSize
{
public string Name;
public int Size;
}
public static NameAndSize MaxNameAndSize(IList<NameAndSize> list)
{
if (!list.Any())
{
return default(NameAndSize);
}
var initialValue = list[0];
Func<NameAndSize, NameAndSize, NameAndSize> action =
(maxSoFar, x) => x.Size > maxSoFar.Size ? x : maxSoFar;
return list.Aggregate(initialValue, action);
}请注意,这个C#版本对空的列表返回null。这似乎很危险 -- 那么应该怎么做呢?抛出一个异常?这似乎也不对。
这是使用 fold 的 F# 代码:
type NameAndSize= {Name:string;Size:int}
let maxNameAndSize list =
let innerMaxNameAndSize initialValue rest =
let action maxSoFar x = if maxSoFar.Size < x.Size then x else maxSoFar
rest |> List.fold action initialValue
// handle empty lists
match list with
| [] ->
None
| first::rest ->
let max = innerMaxNameAndSize first rest
Some maxF#代码有两部分:
innerMaxNameAndSize函数与我们之前看到的类似。the second bit,
match list with, branches on whether the list is empty or not. With an empty list, it returns aNone, and in the non-empty case, it returns aSome. Doing this guarantees that the caller of the function has to handle both cases.第二点,
match list with,判断列表是否为空。对于空列表,它返回一个None,在非空的情况下,它返回一个Some。这样做可以保证函数的调用者必须处理这两种情况。
然后测试:
//test
let list = [
{Name="Alice"; Size=10}
{Name="Bob"; Size=1}
{Name="Carol"; Size=12}
{Name="David"; Size=5}
]
maxNameAndSize list
maxNameAndSize []实际上,我根本不需要写这个,因为F#已经有一个maxBy函数了!
// use the built in function
list |> List.maxBy (fun item -> item.Size)
[] |> List.maxBy (fun item -> item.Size)但如您所见,它不能很好地处理空列表。以下是一个进行了安全包装 maxBy 的版本。
let maxNameAndSize list =
match list with
| [] ->
None
| _ ->
let max = list |> List.maxBy (fun item -> item.Size)
Some maxLast updated