BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル F# の基本を越えて - ワークフロー

F# の基本を越えて - ワークフロー

ブックマーク

本稿では私が前回 InfoQ に寄稿した記事 "Beyond Foundation of F# - Active Patterns"(F# の基本を越えて - アクティブパターン)を踏まえ、また別の新たな言語機能「ワークフロー」を紹介します。F# の入門書 "Foundations of F#"(Apress刊、2007年5月)の第11章では、ドメイン固有言語(DSL)を用いて記述されるプログラムについてのテクニックである言語指向プログラミングについて取り上げています。これらの DSL は具体的な言語表現としてテキストあるいは XML に分離して実装される場合があります。また時には DSL はデータ構造やその他の言語機能を用いて汎用プログラミング言語内に完全に埋め込まれる場合もあります。ワークフローはただでさえ強力な F# の言語指向プログラミング機能に基づいて設計された機能です。C# 3.0 の式木と同様のテクニックで、ワークフローによってコードの小さな一部をキャプチャしてその内容を分析してライブラリ実装者に DSL の構築ブロックとしてそれを利用する機会を与えることができます。本稿では F# におけるワークフローがどのように機能するのかその中身を調べて掘り下げてみたいと思います。

F# は .NET Framework をターゲットとした静的型付けの関数型プログラミング言語です。また別の良く知られた関数型プログラミング言語である OCaml と共通のコア言語を有しており、Haskell、Erlang、C# といった他の多数のプログラミング言語からアイディアが採り入れられています。これは一言で言うと、F# がインタラクティブなコード実行が可能ためにちょっとスクリプトを記述したいと思うような素晴らしい簡潔な構文を持ちながらも、すべての型安全性とコンパイラ方式言語のパフォーマンスを有しているということを意味します。本稿は F# の入門を意図したものではありませんが、既にウェブ上には F# を簡単に習得するための情報源が多数存在しています。私の F# に関する前回記事中の補足 "F# Resources" を参照してください。

ワークフローの作成

ワークフローは2つの部分で構成されます。ユーザが定義するワークフローのインスタンスを作成するコードと、そのワークフローが何を行うかを定義するライブラリコンポーネントです。非常に単純なワークフローの例を見てみましょう。:

ここでは "script" ワークフローが識別子 "num" に結び付けられているのがわかります。ワークフローは実際にワークフローが何を行うのかを表す F# の式を中括弧で囲み、先頭に識別子 "script" を付加して作成されます。ではこのスクリプトを動作させるために必要なライブラリ基盤を調べてみましょう。:

たくさんのコードですね。一つ一つ分解してそれぞれの断片が元のスクリプトにどのように作用するのか見てみましょう。最初の型定義でスクリプトの型を定義します。この場合はスクリプトは値を返す関数です。先ほど定義したスクリプト "num" は実行時に整数値(この場合は 42)を返す関数なので Script 型となります。次に、スクリプトを実行し、また実行される直前までスクリプトの生成を遅らせる(これでスクリプトが生成されて実行される際にスクリプトに含まれる副作用がすべて実行される)ために利用する "delay" を定義するために使用される関数 "runScript" を定義します。

次のライブラリコンポーネントの重要な部分は ScriptBuilder クラスです。これはスクリプトを構成する各種の式を処理するために使用されるメソッドを定義します。式の各部分が値としてメソッドに渡されます。ここで "Return" および "Let" メソッドはスクリプト中の let および return バインディングを処理します。これらのうちでより興味深いのは let バインディングです。let バインディングではパラメータの値を出力するために "printfn" 関数が使用されますが、即ちこれはスクリプト実行時に let バインディング内の値がコンソールに出力されるということを意味します。これはデバッグの際にとても便利な助けとなります。最後の部分でようやく ScriptBuilder クラスのインスタンスを生成しています。これで "num" ワークフローでこのインスタンス "script" が使用されます。そして "num" スクリプトが実行されると、:

runScript num

コンソールには以下のように出力されます。

2
21
val it : int = 42

最初の2つの値は let バインディングで出力された値で、最後の値は F# のインタラクティブな実行によって関数の結果がその型に応じて自動的に出力されたものです。

ワークフローは、先の例のような let などの通常の F# の式だけではなく、"let!"(let bang と発音する)や "yield" あるいはその他の新しい式も処理することが可能です。これこそがワークフローの真の能力であり、ライブラリ実装者はこれらのキーワードを持ち込んで新しい意味を持たせることが可能になります。

では先ほどの例を "let!" バインディングの式も含んだものに拡張しましょう。すべての let バインディングでコンソール出力するのではなく重要なバインディングだけで出力したいということにして、プログラマがどのバインディングで出力するかを選べるようにするためのうってつけな方法として "let!" を実装してみます。これは let! バインディングを処理するために以下のメソッドを "ScriptBuilder" クラスに追加することによって実装されます。:

またコンソール出力を行わないように "Let" も少し書き換える必要があります。

これでスクリプトの定義および実行時に特定の let バインディングでコンソール出力するために let! バインディングを使用することができます。

今度は 21 だけがコンソールに出力されます。

21
val it : int = 42

F# コンパイラはワークフローと最終的に実行される式との間でどのように翻訳を行っているのでしょうか?このプロセスは "脱糖" と呼ばれ、次節ではそれがどのように動作するのか掘り下げてみましょう。

脱糖を理解する

ワークフロー内の式は "継続渡し形式" を用いたデータ構造へ変換されますが、このプロセスは脱糖として知られており、その意味でワークフローはまさに "糖衣構文" なのです。

ここで最初の簡単な例に戻るのがわかりやすいでしょう。

これは以下のように翻訳されます。:

この構造を手でタイプしなくてもよいことの優位性は明らかです。"糖衣" 版の方がより簡潔で理解しやすく、糖衣無し版の記述は困難かつ間違いやすいでしょう。普通の式ではなく後者の形式で式を記述する利点はちょっとわかりません。ここで重要なポイントは Bind および Let メソッドが呼び出される事です。これらのメソッドは1つ目のパラメータとして let バインディングが、2つ目には値が与えられるのを待っている関数である継続が渡されて呼び出されます。これにより、以下の "Bind" メソッドの定義に見られるような、パラメータ "p" を出力した後に計算を続けるためにそれを "rest" 関数に受け渡すというトリックが可能になります。

printfn "%A" p
rest p

これが F# におけるワークフローの核心部分です。開発者は継続形式の受け渡しによって値のバインド時に追加的なアクションを差し込むことができます。先ほどの例ではこれらのアクションはいささか些細なものでしたが、次回記事の非同期ワークフローに関する考察でわかる通り、これらのアクションは差し込みが可能であると同時に非同期計算の完了を待ってアクションを再開することができます。

逐次ワークフロー

これまでのところ小さなデバッグスクリプトにおけるワークフローの使い方は全く印象的ではないように思えるかも知れません。それでは逐次ワークフローを用いたもっと現実的な例を掘り下げてみましょう。逐次ワークフローは F# のベースクラスライブラリの一部として利用可能なワークフローの一種です。逐次式はコレクションを作成するのに便利です。例えば、以下の逐次式は最初の3つの平方数とその平方根のリストを作成します。

逐次式の最も便利な機能の一つは、コレクションの一部として値を返却して計算を継続する yield キーワードの使用です。例えば、以下のコードはテキストファイルを読み込んで各行を返却して、テキストファイルの行からなる配列を作成するというものです。

また逐次式では yield!(yield bang と発音する)キーワードも使用できます。これは要素の配列をコレクションに追加することができ、再帰的な逐次式でよく利用されます。以下は WPF コントロールツリーをフラット配列化する再帰逐次式の使用方法を示しています。これはコントロールツリーの全ての要素に対して何らかの処理を適用したい場合にとても便利です。

逐次式の最初の行でコレクションに追加した最初の要素を返却しているのがわかります。そして、逐次式の後半では for ループを用いてコントロールの子要素を列挙してそれらに対して再帰的に "treeToList" を呼び出しています。そしてこの式が返却する配列が yield! キーワードを用いてコレクションに格納され、ツリーがリストへとフラット化されます。

実際にこの逐次式の動作を確認するには、以下のような WPF ウィンドウを作成するコードを使用する必要があります。

より詳しく知りたい人のために

Don Syme氏、Adam Granicz氏 および Antonio Cisternino氏による著書 "Expert F#" の第9章でワークフローについて詳しく解説されています。

まとめ

ワークフローはライブラリ設計者が非常に高い柔軟性を備えたライブラリを作成可能とする強力なテクニックです。必ずしも全ての F# プログラマが自身の DSL を作成するためにワークフローを利用するわけではないかもしれませんが、何らかの場面で F# プログラマがワークフローを用いて記述されたライブラリを使用する可能性は非常に高く、逐次ワークフローは見逃すにはあまりにも便利すぎるものです。次回記事では、.NET の非同期プログラミングモデルの利用が簡単になるワークフローのもう一つの革新的な利用法である非同期ワークフローについて調べてみたいと思います。

 

原文はこちらです:http://www.infoq.com/articles/pickering-fsharp-workflow
(このArticleは2008年1月3日に原文が掲載されました)

この記事に星をつける

おすすめ度
スタイル

BT