データ ポイント

データ操作の新しい手段、F#

Julie Lerman

コード サンプルのダウンロード

Julie Lerman私は、ここ数年にわたって関数型プログラミングに携わってきました。ときには、暗黙のうちに関数型プログラミングを行いました。たとえば、LINQ でラムダ式を使用してコーディングすることは、関数型プログラミングです。また、明示的に携わったこともあります。たとえば、Entity Framework や LINQ to SQL の、CompiledQuery 型を使用するには、.NET 関数デリゲートを使用する必要があります。これは普段は行わない作業だったため、いつも少し手間取りました。また、Rachel Reese の情熱に感化されて関数型プログラミングに携わったこともあります。Rachel Reese は Microsoft MVP であり、地元のユーザー グループ (VTdotNET) の参加者であるだけでなく、ここバーモントで関数型プログラミングのさまざまな側面と言語に注目する VTFun グループを運営しています。初めて参加した VTFun のミーティングでは、参加者は数学者やロケット科学者ばかりでした。嘘ではありません。常連参加者の 1 人は、バーモント大学の物性理論研究者です。卒倒しそうになりますね。ハイレベルな議論に少し圧倒されましたが、会場で自分だけが間抜けのように感じたことは良い経験になりました。内容はほとんど頭に入りませんでしたが、「関数型言語では、厄介な foreach ループはまったく必要ありません」というひと言だけは意識に残りました。私はこの発言の意味に興味を持ちました。なぜ関数型言語には foreach ループが必要ないのでしょう。

関数型プログラミングが数学演算に適しているという主張は、よく耳にしたことがあります。私は数学の専門家ではないため、この主張を "フィボナッチ数列を使用して非同期動作をテストするデモ" に適していると解釈し、それほど気にしていませんでした。これが、関数型プログラミングのごく簡潔な描写を聞いても理解できなかった理由でした。

しかし、その後ついにより的確な、関数型プログラミングはデータ科学に適しているという表現を耳にするようになりました。これは確かにデータの専門家にとって魅力的です。Microsoft .NET Framework の関数型言語である F# を使用すると、.NET 開発者はさまざまなデータ科学の機能を利用できます。F# には、グラフ作成、時間操作、およびセット操作にそれぞれ特化したライブラリが丸ごと用意されています。2D、3D、および 4D 配列それぞれに対応する、専用ロジックを備えた API もあります。測定単位を認識でき、指定の単位に基づいて制約や検証を実行できます。

F# を使用すると、Visual Studio の興味深いコーディング シナリオも利用可能です。このシナリオでは、コード ファイルでロジックを組み立ててからデバッグするのではなく、インタラクティブ ウィンドウで 1 行ずつコードを記述して実行してから、成功したコードをクラス ファイルに移動します。Tomas Petricek の記事「Understanding the World with F#」(F# で世界を理解する、英語) bit.ly/1cx3cGx をご覧になると、言語としての F# を理解していただきやすいでしょう。.NET ツールセットに F# を追加すれば、Visual Studio が、データ科学ロジック実行用アプリケーションを構築するための強力なツールに生まれ変わります。

この記事では、foreach ループが必要ないという発言を聞いた結果で私が理解することになった、F# と関数型プログラミングの一側面に注目します。関数型言語はデータ セットの操作に非常に適しています。手続き型言語の場合、セットを操作するには明示的にセットを反復処理してロジックを実行する必要があります。一方、関数型言語では手続き型言語とは異なるレベルでセットを解釈するので、セット全体に対してその関数を実行するよう指示するだけで済み、セットをループ処理して各項目に対して関数を実行することはありません。このような関数は、必要に応じて数学などのさまざまなロジックで定義できます。

LINQ では、こうした処理の手間を省く手法を利用でき、関数を渡せる ForEach メソッドさえも省略できます。しかし背後の処理では、使用する言語 (C#、Visual Basic など) によってこの機能はループに変換されます。

F# などの関数型言語では、セット関数を比較的低レベルで実行できるうえ、容易な並列処理を利用できるので関数の実行速度も高速です。他にも、豊富な計算処理機能、測定単位まで解釈できる非常に詳しい型システム、大規模なデータ セットを計算するための強力なツールなど、大きなメリットがあります。

F# や他の関数型言語には、まだ私がよく把握できていないメリットがたくさんありますが、このコラムでは、大規模に投資することなく、関数型言語の特定の側面から手軽にメリットを享受する方法に重点を置きます。その側面とは、データベースのロジックをアプリケーションに移植することです。これは私のお気に入りの学習方法です。理解できることをいくつか見つけて使いながら、新しいプラットフォーム、言語、フレームワーク、または他の種類のツールを少しずつ理解します。

F# を使用しても開発言語を切り替えることにはならないと、Reese が開発者に明確に伝えようと努めていると聞いたことがあります。LINQ クエリやストアド プロシージャを使用して特定の問題を解決する場合と同様に、F# メソッドのライブラリを作成して、関数型言語が得意とするたぐいのアプリケーションの問題を解決できます。

この記事では、データベースに組み込まれているビジネス ロジック (データベースの長所である、大規模なデータ セットの処理ロジック) を抽出して、関数型メソッドに置き換えることについて取り上げます。

F# はセットの処理を目的としていて数学的関数との相性が良いため、SQL や手続き型言語 (C#、Visual Basic など) よりもコードが効率的になることがあります。セットの各項目に対してロジックを並列実行するよう F# に指示することは、非常に簡単です。並列処理の効果は、同じ動作を手続き型言語でエミュレートする際に必要なコードよりもコード量が少ないことだけではありません。並列化とは、コードの実行速度が高まることを意味します。C# コードを並列実行するよう設計することも可能ですが、私はその作業に労力をかけようとは思いません。

現実世界での問題

何年も前に、私は膨大な科学データの収集、保守、および報告と大量の計算が必要な Visual Basic 5 アプリケーションを作成しました。一部の計算はあまりに複雑だったため、Excel API に送信していました。

そのような計算の 1 つは、物質の塊の崩壊を引き起こす重量に基づくポンド/平方インチ (PSI) の特定についてでした。この塊は、さまざまな円柱形とサイズになる可能性がありました。アプリケーションでは、円柱の測定値と (形状やサイズに応じた) 特定の式を使用して、円柱の底面積を計算しました。続いて適切な許容係数を適用し、最後に、円柱が崩壊する重量を適用しました。この処理全体をまとめ合わせることで、テスト対象にしている特定の物質の PSI を算出しました。

1997 年の当時、Excel API を利用して Visual Basic 5 や Visual Basic 6 の内部から式を評価することは、きわめて優れた解決策のように思えました。

評価が変化する

数年後、私は .NET でこのアプリケーションを改良しました。このときは、ユーザーのコンピューターですべての計算に時間を費やさずに済むよう、ユーザーが多数の円柱のセットを更新した後で SQL Server の機能を利用して円柱の PSI を計算することにしました。この手法は非常にうまく機能しました。

さらに何年かたち、データベースのビジネス ロジックに関する私の考えが変わりました。私はこの計算をクライアント側に戻そうと思いました。もちろん、そのころにはクライアント コンピューターの処理速度も向上していました。C# でロジックを書き直すのはそれほど難しくありませんでした。円柱の崩壊を引き起こす重量 (ポンド単位の荷重) でユーザーが円柱のセットを更新したら、更新した円柱をアプリケーションで反復処理して PSI を計算します。その後、新しい荷重値と PSI 値でデータベースの円柱を更新します。

おなじみの C# を (この後説明する) F# での最終結果と比較するために、CylinderMeasurements という円柱の種類のリスト (図 1 参照) と C# の独自の計算機クラス (図 2 参照) を作成して、円柱セットの PSI を算出する方法をご覧いただけるようにしました。円柱のセットに対する PSI の計算を始めるには、CylinderCalculator.UpdateCylinders メソッドを呼び出します。このメソッドではセット内の各円柱を反復処理し、適切な計算を実行します。メソッドの 1 つ、GetAreaForCylinder が円柱の種類に依存していることに注目してください。これは、適切な式を使用して円柱の底面積を計算するためです。

図 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 を計算するための計算機クラス

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# を使用すれば、データ操作に適したその生来の性質により、一度に 1 つの式を評価するよりも優れた解決策を利用できることに気付きました。

Reese が開催した F# 入門セッションで、私は長年悩んできたこの問題について説明し、関数型言語を使用するとこの問題をより満足できる方法で解決できるかどうかたずねました。Reese は、計算ロジックをまとめてセット全体に適用でき、F# で多数の円柱の PSI を並列計算できると請け合ってくれました。このため、クライアント側機能とパフォーマンス向上を同時に達成できました。

私にとって重要だったのは、ストアド プロシージャを使用する場合とほとんど同じ方法で、F# を使用して特定の問題を解決できると気付いたことです。つまり、F# は手持ちの手段と共存可能な新しい手段です。C# に投入した労力をあきらめる必要はありません。私とは逆に、アプリケーションの大部分を F# で記述して、特定の問題に対処するために C# を使用する方もいらっしゃるでしょう。主義はどうあれ、C# の CylinderCalculator を参考に Reese がこの処理を実行する簡単な F# プロジェクトを作成してくれました。このため、今回のテストでは、自作の計算機クラスの呼び出しを Reese が作成した計算機クラスの呼び出しに置き換えることができました (図 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# を上回るパフォーマンスの向上を確認できませんでした。F# を使用するとテスト環境のコストが増えるという Reese の説明を受けて、私は経過時間を報告する Stopwatch クラスを使用したコンソール アプリケーションで、パフォーマンスをテストしました。このアプリケーションでは 50,000 個の円柱で構成されたリストを作成し、Stopwatch を開始してから、円柱を C# または F# の計算機に渡して各円柱の PSI 値を更新し、計算が完了したら Stopwatch を停止しました。

ほとんどの場合、C# の処理には F# の処理の 3 倍の時間がかかりましたが、約 20% の例で C# は F# よりもわずかに高速でした。例外については説明が付きませんが、より正確なプロファイリングのためにはさらに理解しなければならないことがありそうです。

関数型言語が必要なロジックに気を配る

私の F# スキルにはまだ改善が必要ですが、既に運用中のアプリケーションや今後のアプリケーションにこの新しい知識をうまく適用できそうです。運用中のアプリケーションについては、データベース任せだったビジネス ロジックを見直して、F# に置き換えるとアプリケーションにメリットがあるかどうか検討できます。新しいアプリケーションについては、さらに鋭い目で、F# で効率良くコードを記述できる機能を見つけ出し、データ操作を実行し、厳密に型指定された測定単位を利用して、パフォーマンスを向上できるようになっています。しかも、新しい言語を学んでその言語の存在意義になる適切なシナリオを見つけるという楽しみを、いつも感じることができます。

Julie Lerman は、バーモント ヒルズ在住の Microsoft MVP、.NET の指導者、およびコンサルタントです。世界中のユーザー グループやカンファレンスで、データ アクセスなどの Microsoft .NET トピックについてプレゼンテーションを行っています。彼女のブログは thedatafarm.com/blog (英語) で、『Programming Entity Framework』(O'Reilly Media、2010 年)、および同書の Code First 版 (O'Reilly Media、2011 年) と DbContext 版 (O'Reilly Media、2012 年) の著者でもあります。Twitter (twitter.com/julielerman、英語) で彼女をフォローし、juliel.me/PS-Videos (英語) で Pluralsight のコースをご覧ください。

この記事のレビューに協力してくれた技術スタッフの Rachel Reese (Firefly Logic) に心より感謝いたします。
Rachel Reese は、テネシー州ナッシュビルでソフトウェア エンジニア兼数学専門家として長年の経験があります。最近まで、バーリントンで VT 関数型プログラミング ユーザー グループ @VTFun (英語) を運営していました。VTFun は絶えずインスピレーションの源となり、彼女はしばしば F# について講演を行っていました。Reese は、ASPInsider、F# MVP、熱心なコミュニティ参加者、@lambdaladies (英語) 創始者の 1 人、および Rachii メンバーでもあります。彼女の連絡先は、Twitter (@rachelreese、英語) およびブログ (rachelree.se、英語) です。