数据点

数据,见见我的新朋友 F#

Julie Lerman

下载代码示例

Julie Lerman过去几年来,我接触到了一些函数式编程。其中一些是隐式的。在 LINQ 中使用 lambda 进行编码就是函数式编程。一些已成为显式的:使用 Entity Framework 和 LINQ to SQL CompiledQuery 类型迫使我使用了 .NET 函数逻辑。这始终有点儿复杂,因为我甚少这样做。Microsoft MVP Rachel Reese 极富感染力的热情也使我了解了函数式编程,Rachel Reese 不仅参加了我的当地用户组 (VTdotNET),而且还在佛蒙特运作 VTFun 组,这个组关注函数式编程的许多方面和语言。我第一次参加 VTFun 会议时,与会者都是些数学家和火箭科学家。我不是在开玩笑。其中一位常客是佛蒙特大学的一位凝聚态物理学家。太令人惊讶了!那里进行的都是高级讨论,有趣的是,实际上我感觉自己像个傻瓜。他们谈论的内容大部分我都无法理解 — 除了一句话引起了我的注意:“函数式语言不需要任何讨厌的 foreach 循环。”哇塞!我当时很想知道这是什么意思。它们为什么不需要 foreach 循环?

我经常听说函数式编程非常适用于数学运算。我不是一个数学高手,我将其理解为“用于使用 Fibonacci 序列测试异步行为的演示”,因此没有太在意。这就是仅仅听有关函数式编程的简短电梯游说的问题所在。

但我最终开始听到了更加准确的描述 — 函数式编程非常适用于数据科学。这当然会吸引数据高手。Microsoft .NET Framework 的函数式语言 F# 赋予 .NET 开发人员各种数据科学能力。它具有专用于图表、时间操作和集合操作的整个库。它具有包含 2D、3D 和 4D 数组专用逻辑的 API。它理解测量单位,并且能够基于指定单位进行约束和验证。

F# 还支持 Visual Studio 中有趣的编码方案。您不用在代码文件中构建您的逻辑然后进行调试,而是可以在一个交互式窗口中编写并逐行执行代码,然后将成功的代码移到类文件中。阅读 Tomas Petricek 的文章“通过 F# 了解世界”(bit.ly/1cx3cGx),您会对作为语言的 F# 有很好的认识。通过将 F# 添加到 .NET 工具集中,Visual Studio 成为用于构建执行数据科学逻辑的应用程序的强大工具。

在本文中,我将着重介绍 F# 和函数式编程的一个方面,这是自从我听到有关不需要 foreach 循环的评论后开始明白的一个方面。函数式语言真的很适合与数据集配合使用。在过程性语言中,当您使用集时,必须显式遍历它们才能执行逻辑。相比之下,函数式语言在不同级别上理解集,因此您只需要求它在集上执行函数,而不是在整个集中循环,并在每个项目上执行函数。必要时,可使用许多逻辑(包括数学逻辑)定义该函数。

LINQ 提供了实现此操作的捷径,甚至是您可以将函数传递到的 ForEach 方法。但在后台,您的语言(或许为 C# 或 Visual Basic)只是将其转变成循环。

像 F# 等函数式语言能够以更低的级别执行集函数,并且凭借其轻松的并行处理,速度要快得多。再加上其他关键优势,例如丰富的数学处理能力,以及甚至理解测量单位的难以置信的详细键入系统,您获得了用于在大型数据集上执行计算的强大工具。

F# 和其他函数式语言中仍有许多我难以理解的方面。但在本专栏中,我想着重探讨在不进行大量投资的情况下从函数式语言的一个特定方面中快速获益的方式:将逻辑从数据库移到我的应用程序中。我喜欢的学习方式是这样的:寻找我能理解的一些事情,然后利用它们慢慢了解新平台、语言、框架或其他类型的工具。

我听说 Reese 尽量让开发人员明白,使用 F# 并不意味着转换开发语言。通过您可能使用 LINQ 查询或存储过程解决特定问题的相同方式,可以创建一个 F# 方法库来解决应用程序中函数式语言真正擅于解决的问题类型。

在这里,我将关注的是提取内置到我数据库中的业务逻辑,用于处理大型数据集的逻辑 — 这是该数据库非常擅长的方面 — 然后将其替换为函数式方法。

由于 F# 可与集一同使用,并且真正擅于使用数学函数,因此代码的效率能够比它在使用 SQL 或者 C# 或 Visual Basic 等过程性语言时所具有的效率更高。让 F# 在集中的项目上并行执行逻辑非常轻松。这不仅可减少在过程性语言中为模拟此行为可能需要的代码量,而且并行化意味着代码的运行速度将更快。您可以将 C# 代码设置成并行运行,但我也不愿这样做。

真实问题

许多年前,我编写了一个 Visual Basic 5 应用程序,该应用程序必须收集、维护和报告大量科学数据并执行大量计算。其中一些计算过于复杂,以致于我将它们发送到 Excel API 中。

其中一个计算涉及了根据导致一大块材料断裂的重量确定每平方英寸的磅数 (PSI)。这个材料块可能是任何数目的圆柱形和大小。该应用程序将使用对圆柱体的测量,并且根据其形状和大小,使用特定公式来计算其面积。然后其将应用相关的公差因子,最后应用使该圆柱体断裂所需的重量。所有这些结合在一起得出了针对特定受测材料的 PSI。

1997 年,充分利用 Excel API 从 Visual Basic 5 和 Visual Basic 6 中对该公式求值感觉像是非常聪明的解决方法。

此求值的发展

几年后,我在 .NET 中改进了此应用程序。那时,在用户更新了大型圆柱体集后,我决定利用 SQL Server 在这些集上执行 PSI 计算,而不是让用户的计算机费时地运行所有这些计算。这个办法非常好。

许多年过去了,我对数据库中的业务逻辑有了不同的想法。我想让计算重新在客户端进行,当然,那时的客户机速度更快。使用 C# 重写该逻辑非常困难。在用户利用使一系列圆柱体断裂所需的重量(负荷,以磅表示)更新了这些圆柱体后,该应用程序将遍历这些更新的圆柱体并计算 PSI。然后我能够在数据库中利用圆柱体的新负荷和 PSI 值更新这些圆柱体。

为了将熟悉的 C# 与 F# 中的最终结果(我将立刻看到)进行比较,我列出了圆柱体类型 CylinderMeasurements(图 1)和我的 C# Calculator 类(图 2),这样您便能够看到我是如何得出圆柱体集的 PSI 的。这是为开始进行圆柱体集的 PSI 计算而调用的 CylinderCalculator.UpdateCylinders 方法。该方法遍历该集中的每个圆柱体,然后执行相应的计算。注意,其中一种方法 GetAreaForCalculation 取决于圆柱体类型,因为我使用相应公式计算圆柱体的面积。

图 1 CylinderMeasurement 类

 

public class CylinderMeasurement
{
  public CylinderMeasurement(double widthA, double widthB,
    double height)
  {
    WidthA = widthA;
    WidthB = widthB;
    Height = height;
  }
  public int Id { get; private set; }
  public double Height { get; private set; }
  public double WidthB { get; private set; }
  public double WidthA { get; private set; }
  public int LoadPounds { get; private set; }
  public double Psi { get; set; }
  public CylinderType CylinderType { get; set; }
  public void UpdateLoadPoundsAndTypeEnum(int load, 
    CylinderType cylType) {
    LoadPounds = load; CylinderType = cylType;
  }
   private double? Ratio {
    get {
      if (Height > 0 && WidthA + WidthB > 0) {
        return Math.Round(Height / ((WidthA + WidthB) / 2), 2);
      }
      return null;
    }
  }
  public double ToleranceFactor {
    get {
      if (Ratio > 1.94 || Ratio < 1) {
        return 1;
      }
      return .979;
    }
  }
}

图 2 用以计算 PSI 的 Calculator 类

public static class CylinderCalculator
  {
    private static CylinderMeasurement _currentCyl;
    public static void UpdateCylinders(IEnumerable<CylinderMeasurement> cyls) {
      foreach (var cyl in cyls)
      {
        _currentCyl = cyl;
        cyl.Psi = GetPsi();
      }
    }
    private static double GetPsi() {
      var area = GetAreaForCylinder();
      return PsiCalculator(area);
    }
    private static double GetAreaForCylinder() {
      switch (_currentCyl.CylinderType)
      {
        case CylinderType.FourFourEightCylinder:
          return 3.14159*((_currentCyl.WidthA + _currentCyl.WidthB)/2)/2*
            ((_currentCyl.WidthA + _currentCyl.WidthB)/2/2);
        case CylinderType.SixSixTwelveCylinder:
          return 3.14159*((_currentCyl.WidthA + _currentCyl.WidthB)/2)/2*
            ((_currentCyl.WidthA + _currentCyl.WidthB)/2/2);
        case CylinderType.ThreeThreeSixCylinder:
          return _currentCyl.WidthA*_currentCyl.WidthB;
        case CylinderType.TwoTwoTwoCylinder:
          return ((_currentCyl.WidthA + _currentCyl.WidthB)/2)*
            ((_currentCyl.WidthA + _currentCyl.WidthB)/2);
        default:
          throw new ArgumentOutOfRangeException();
      }
    }
    private static int PsiCalculator(double area) {
      if (_currentCyl.LoadPounds > 0 && area > 0)
      {
        return (int) (Math.Round(_currentCyl.LoadPounds/area/1000*
          _currentCyl.ToleranceFactor, 2)*1000);
      }
      return 0;
    }
  }

以数据为主,以及利用 F# 提高处理速度

最后我发现,凭借 F# 处理数据的自然倾向,F# 提供了比每次对一个公式求值好得多的解决方案。

在 Reese 召开的有关 F# 的介绍会议上,我阐述了这个多年来一直困扰我的问题,并且询问是否能够使用函数式语言以更令人满意的方式解决这一问题。她确认说,我能够在整个集上应用我的完整计算逻辑,并且让 F# 并行得出许多圆柱体的 PSI。我能够同时获得客户端功能和性能提升。

对我来说,关键是我意识到我在使用 F# 解决特定问题时,能够使用我在存储过程中使用的几乎相同的方式 — 这只是我工具腰带中的另一个工具。这不需要放弃我在 C# 上的投资。或许有些人恰恰相反 — 使用 F# 编写大部分的应用程序,然后使用 C# 攻克特定问题。在任何情况下,以 C# CylinderCalculator 作为指导,Reese 创建的小 F# 项目都执行了此任务,并且在我的测试中我能够将对我计算器的调用替换成对她的计算器的调用,如图 3 所示。

图 3 F# PSI 计算器

module calcPsi =
  let fourFourEightFormula WA WB = 3.14159*((WA+WB)/2.)/2.*((WA+WB)/2./2.)
  let sixSixTwelveFormula WA WB = 3.14159*((WA+WB)/2.)/2.*((WA+WB)/2./2.)
  let threeThreeSixFormula (WA:float) (WB:float) = WA*WB
  let twoTwoTwoFormula WA WB = ((WA+WB)/2.)*((WA+WB)/2.)
  // Ratio function
  let ratioFormula height widthA widthB =
    if (height > 0. && (widthA + widthB > 0.)) then
      Some(Math.Round(height / ((widthA + widthB)/2.), 2))
    else
      None
  // Tolerance function
  let tolerance (ratioValue:float option) = match ratioValue with
    | _ when (ratioValue.IsSome && ratioValue.Value > 1.94) -> 1.
    | _ when (ratioValue.IsSome && ratioValue.Value < 1.) -> 1.
    | _ -> 0.979
  // Update the PSI, and return the original cylinder information.
  let calculatePsi (cyl:CylinderMeasurement) =
    let formula = match cyl.CylinderType with
      | CylinderType.FourFourEightCylinder -> fourFourEightFormula
      | CylinderType.SixSixTwelveCylinder -> sixSixTwelveFormula
      | CylinderType.ThreeThreeSixCylinder -> threeThreeSixFormula
      | CylinderType.TwoTwoTwoCylinder -> twoTwoTwoFormula
      | _ -> failwith "Unknown cylinder"
    let basearea = formula cyl.WidthA cyl.WidthB
    let ratio = ratioFormula cyl.Height cylinder.WidthA cyl.WidthB
    let tFactor = tolerance ratio
    let PSI = Math.Round((float)cyl.LoadPounds/basearea/1000. * tFactor, 2)*1000.
    cyl.Psi <- PSI
    cyl
  // Map evaluate to handle all given cylinders.
  let getPsi (cylinders:CylinderMeasurement[])
              = Array.Parallel.map calculatePsi cylinders

如果您也像我一样是个 F# 新手,仅看看代码数量您便会知道,在 C# 上选择这条路是没有意义的。但进一步调查后,您可能领会到该语言非常简洁,能够更精确地定义这些公式,并且可使我轻松将 Reese 定义的 calculatePsi 函数应用到我传递到该方法的圆柱体数组。

之所以如此简洁,是因为 F# 能够比 C# 更好地执行数学函数,从而可更高效地定义这些函数。但除被该语言深深吸引外,我对性能也非常感兴趣。当我在测试中增加了每个集的圆柱体数目时,最初我没有看到性能比 C# 有所提高。Reese 解释说,在使用 F# 时,测试环境的成本更高。于是我在使用 Stopwatch 报告用时的控制台应用程序中测试了性能。该应用程序构建了一个包含 50,000 个圆柱体的列表,启动 Stopwatch 之后将这些圆柱体传递入 C# 或 F# 计算器,以更新每个圆柱体的 PSI 值,然后在这些计算完成时停止 Stopwatch。

在大多数情况下,C# 过程的用时比 F# 过程长大约三倍,尽管约 20% 的时间 C# 小胜 F#。我无法解释这种奇怪现象,但要执行更真实的剖析,我可能需要了解更多知识。

密切关注乞求函数式语言的逻辑

因此,当我必须利用我的 F# 技能工作时,我的新理解将完美地应用到我已经投入生产的应用程序以及未来应用程序中。在我的生产应用程序中,我能够着眼于已经转移到数据库中的业务逻辑,并且考虑应用程序是否会从 F# 替代中获益。凭借新的应用程序,我现在能够以更敏锐的眼光发现能够使用 F# 更有效编码的功能,执行数据处理,充分利用强类型化的测量单位以及提高性能。学习新的语言以及寻找该语言的恰当应用情景始终充满乐趣!

Julie Lerman是 Microsoft MVP、.NET 导师和顾问,住在佛蒙特州的山区。您可以在全球的用户组和会议中看到她对数据访问和其他 Microsoft .NET 主题的演示。她是《Programming Entity Framework》(2010) 以及“代码优先”版 (2011) 和 DbContext 版 (2012)(均出自 O’Reilly Media)的作者,博客网址为 thedatafarm.com/blog。通过她的 Twitter(网址为 twitter.com/julielerman)关注她,并在 juliel.me/PS-Videos 上观看其 Pluralsight 课程。

衷心感谢以下技术专家对本文的审阅:Rachel Reese (Firefly Logic)
Rachel Reese 长久以来一直是田纳西州纳什维尔的一位软件工程师和数学高手。最近,她运作了位于佛蒙特州伯灵顿的函数式编程用户组 @VTFun,这个组源源不断地为她提供灵感,并且是她经常谈论 F# 的地方。她还是 ASP 专家、F# MVP、社区爱好者、@lambdaladies 创始人之一,以及 Rachii。您可以通过 twitter @rachelreese 或她的博客与她联系,她的博客地址为:rachelree.se