F# Compiler Services


インタラクティブサービス: F# Interactiveの組み込み

このチュートリアルでは、独自のアプリケーションに F# Interactiveを組み込む方法について紹介します。 F# Interactiveは対話式のスクリプティング環境で、 F#コードを高度に最適化されたILコードへとコンパイルしつつ、 それを即座に実行することができます。 F# Interactiveサービスを使用すると、独自のアプリケーションに F#の評価機能を追加できます。

注意: F# Interactiveは様々な方法で組み込むことができます。 最も簡単な方法は fsi.exe プロセスとの間で標準入出力経由でやりとりする方法です。 このチュートリアルではF# Interactiveの機能を.NET APIで 直接呼び出す方法について紹介します。 ただし入力用のコントロールを備えていない場合、別プロセスでF# Interactiveを 起動するのはよい方法だといえます。 理由の1つとしては StackOverflowException を処理する方法がないため、 出来の悪いスクリプトによってはホストプロセスが停止させられてしまう 場合があるからです。 .NET APIを通じてF# Interactiveを呼び出すとしても、 --shadowcopyreferences オプションは無視されることを覚えておきましょう。 詳細な議論については、このスレッド に目を通してみてください。 注意: もしFSharp.Core.dll が見つからないというエラーが出て FsiEvaluationSession.Create に失敗した場合、 FSharp.Core.sigdataFSharp.Core.optdata というファイルを追加してください。 詳しい内容はこちら にあります。

しかしそれでもF# InteractiveサービスにはF# Interactiveを実行ファイルに埋め込んで 実行出来る(そしてアプリケーションの各機能とやりとり出来る)、あるいは 機能限定されたF#コード(たとえば独自のDSLによって生成されたコード)だけを 実行させることが出来るという便利さがあります。

F# Interactiveの開始

まずF# Interactiveサービスを含むライブラリへの参照を追加します:

1: 
2: 
3: 
#r "FSharp.Compiler.Service.dll"
open Microsoft.FSharp.Compiler.SourceCodeServices
open Microsoft.FSharp.Compiler.Interactive.Shell

F# Interactiveとやりとりするには、入出力を表すストリームを作成する必要があります。 これらのストリームを使用することで、 いくつかのF#コードに対する評価結果を後から出力することができます:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
open System
open System.IO
open System.Text

// 入出力のストリームを初期化
let sbOut = new StringBuilder()
let sbErr = new StringBuilder()
let inStream = new StringReader("")
let outStream = new StringWriter(sbOut)
let errStream = new StringWriter(sbErr)

// コマンドライン引数を組み立てて、FSIセッションを開始する
let argv = [| "C:\\fsi.exe" |]
let allArgs = Array.append argv [|"--noninteractive"|]

let fsiConfig = FsiEvaluationSession.GetDefaultConfiguration()
let fsiSession = FsiEvaluationSession.Create(fsiConfig, allArgs, inStream, outStream, errStream)  

コードの評価および実行

F# Interactiveサービスにはコードを評価するためのメソッドがいくつか用意されています。 最初の1つは EvalExpression で、式を評価してその結果を返します。 結果には戻り値が( obj として)含まれる他、値に対して静的に推論された型も含まれます:

1: 
2: 
3: 
4: 
5: 
/// 式を評価して結果を返す
let evalExpression text =
  match fsiSession.EvalExpression(text) with
  | Some value -> printfn "%A" value.ReflectionValue
  | None -> printfn "結果が得られませんでした!"

これは引数に文字列を取り、それをF#コードとして評価(つまり実行)します。

1: 
evalExpression "42+1" // '43' を表示する

これは以下のように強く型付けされた方法で使うことができます:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
/// 式を評価して、強く型付けされた結果を返す
let evalExpressionTyped<'T> (text) =
    match fsiSession.EvalExpression(text) with
    | Some value -> value.ReflectionValue |> unbox<'T>
    | None -> failwith "結果が得られませんでした!"

evalExpressionTyped<int> "42+1"  // '43' になる

EvalInteraction メソッドは画面出力機能や宣言、 F#の式としては不正なものの、F# Interactiveコンソールには入力できるようなものなど、 副作用を伴う命令を評価する場合に使用できます。 たとえば #time "on" (あるいはその他のディレクティブ)や open System 、 その他の宣言やトップレベルステートメントなどが該当します。 指定するコードの終端に ;; を入力する必要はありません。 実行したいコードだけを入力します:

1: 
fsiSession.EvalInteraction "printfn \"bye\""

EvalScript メソッドを使用すると、完全な .fsx スクリプトを評価することができます。

1: 
2: 
File.WriteAllText("sample.fsx", "let twenty = 10 + 10")
fsiSession.EvalScript "sample.fsx"

例外処理

コードに型チェックの警告やエラーがあった場合、または評価して例外で失敗した場合、 EvalExpressionEvalInteraction そして EvalScript ではあまりうまく処理されません。 これらのケースでは、 EvalExpressionNonThrowingEvalInteractionNonThrowing そして EvalScriptNonThrowing を使うことが出来ます。 これらは結果と FSharpErrorInfo 値の配列の組を返します。 これらはエラーと警告を表します。結果の部分は実際の結果と例外のいずれかを表す Choice<_,_> です。

EvalExpression および EvalExpressionNonThrowing の結果部分は オプションの FSharpValue 値です。 その値が存在しない場合、式が .NET オブジェクトとして表現できる具体的な結果を 持っていなかったということを指し示しています。 この状況は実際には入力されたどんな通常の式に対しても発生すべきではなく、 ライブラリ内で使われるプリミティブ値に対してのみ発生すべきです。

1: 
2: 
3: 
4: 
5: 
6: 
7: 
File.WriteAllText("sample.fsx", "let twenty = 'a' + 10.0")
let result, warnings = fsiSession.EvalScriptNonThrowing "sample.fsx"

// 結果を表示する
match result with 
| Choice1Of2 () -> printfn "チェックと実行はOKでした"
| Choice2Of2 exn -> printfn "実行例外: %s" exn.Message

は次のようになります:

1: 
実行例外: Operation could not be completed due to earlier error
1: 
2: 
3: 
// エラーと警告を表示する
for w in warnings do 
   printfn "警告 %s 場所 %d,%d" w.Message w.StartLineAlternate w.StartColumn

は次のようになります:

1: 
2: 
警告 The type 'float' does not match the type 'char' 場所 1,19
警告 The type 'float' does not match the type 'char' 場所 1,17

式に対しては:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
let evalExpressionTyped2<'T> text =
   let res, warnings = fsiSession.EvalExpressionNonThrowing(text)
   for w in warnings do 
       printfn "警告 %s 場所 %d,%d" w.Message w.StartLineAlternate w.StartColumn 
   match res with 
   | Choice1Of2 (Some value) -> value.ReflectionValue |> unbox<'T>
   | Choice1Of2 None -> failwith "null または結果がありません"
   | Choice2Of2 (exn:exn) -> failwith (sprintf "例外 %s" exn.Message)

evalExpressionTyped2<int> "42+1"  // '43' になる

並列実行

デフォルトでは EvalExpression に渡したコードは即時実行されます。 並列に実行するために、タスクを開始する計算を投入します:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
open System.Threading.Tasks

let sampleLongRunningExpr = 
    """
async { 
    // 実行したいコード
    do System.Threading.Thread.Sleep 5000
    return 10 
}
  |> Async.StartAsTask"""

let task1 = evalExpressionTyped<Task<int>>(sampleLongRunningExpr)  
let task2 = evalExpressionTyped<Task<int>>(sampleLongRunningExpr)

両方の計算がいま開始しました。結果を取得することが出来ます:

1: 
2: 
task1.Result // 完了後に結果が出てくる (最大5秒)
task2.Result // 完了後に結果が出てくる (最大5秒)

評価コンテキスト内での型チェック

F# Interactiveの一連のスクリプティングセッション中で コードの型チェックを実行したいような状況を考えてみましょう。 たとえばまず宣言を評価します:

1: 
fsiSession.EvalInteraction "let xxx = 1 + 1"

次に部分的に完全な xxx + xx というコードの型チェックを実行したいとします:

1: 
let parseResults, checkResults, checkProjectResults = fsiSession.ParseAndCheckInteraction("xxx + xx")

parseResultscheckResults はそれぞれ エディタ のページで説明している ParseFileResultsCheckFileResults 型です。 たとえば以下のようなコードでエラーを確認出来ます:

1: 
checkResults.Errors.Length // 1

コードはF# Interactiveセッション内において、その時点までに実行された 有効な宣言からなる論理的な型コンテキストと結びつく形でチェックされます。

また、宣言リスト情報やツールチップテキスト、シンボルの解決といった処理を 要求することもできます:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
open Microsoft.FSharp.Compiler

// ツールチップを取得する
checkResults.GetToolTipTextAlternate(1, 2, "xxx + xx", ["xxx"], FSharpTokenTag.IDENT) 

checkResults.GetSymbolUseAtLocation(1, 2, "xxx + xx", ["xxx"]) // シンボル xxx
  

'fsi'オブジェクト

スクリプトのコードが'fsi'オブジェクトにアクセスできるようにしたい場合、 このオブジェクトの実装を明示的に渡さなければなりません。 通常、FSharp.Compiler.Interactive.Settings.dll由来の1つが使われます。

1: 
let fsiConfig2 = FsiEvaluationSession.GetDefaultConfiguration(fsi)

収集可能なコード生成

FsiEvaluationSessionを使用してコードを評価すると、 .NET の動的アセンブリを生成し、他のリソースを使用します。 collectible=true を渡すことで、生成されたコードを収集可能に出来ます。 しかしながら、例えば EvalExpression から返される FsiValue のような型を必要とする未解放のオブジェクト参照が無く、 かつ FsiEvaluationSession を破棄したに違いない場合に限ってコードが収集されます。 収集可能なアセンブリに対する制限 も参照してください。

以下の例は200個の評価セッションを生成しています。 collectible=trueuse session = ... の両方を使っていることに気をつけてください。

収集可能なコードが正しく動いた場合、全体としてのリソース使用量は 評価が進んでも線形には増加しないでしょう。

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
let collectionTest() = 

    for i in 1 .. 200 do
        let defaultArgs = [|"fsi.exe";"--noninteractive";"--nologo";"--gui-"|]
        use inStream = new StringReader("")
        use outStream = new StringWriter()
        use errStream = new StringWriter()

        let fsiConfig = FsiEvaluationSession.GetDefaultConfiguration()
        use session = FsiEvaluationSession.Create(fsiConfig, defaultArgs, inStream, outStream, errStream, collectible=true)
        
        session.EvalInteraction (sprintf "type D = { v : int }")
        let v = session.EvalExpression (sprintf "{ v = 42 * %d }" i)
        printfn "その %d, 結果 = %A" i v.Value.ReflectionValue

// collectionTest()  <-- このようにテストを実行する
namespace Microsoft
namespace Microsoft.FSharp
namespace Microsoft.FSharp.Compiler
namespace Microsoft.FSharp.Compiler.SourceCodeServices
namespace Microsoft.FSharp.Compiler.Interactive
module Shell

from Microsoft.FSharp.Compiler.Interactive
namespace System
namespace System.IO
namespace System.Text
val sbOut : StringBuilder

Full name: Interactive.sbOut
Multiple items
type StringBuilder =
  new : unit -> StringBuilder + 5 overloads
  member Append : value:string -> StringBuilder + 18 overloads
  member AppendFormat : format:string * arg0:obj -> StringBuilder + 4 overloads
  member AppendLine : unit -> StringBuilder + 1 overload
  member Capacity : int with get, set
  member Chars : int -> char with get, set
  member Clear : unit -> StringBuilder
  member CopyTo : sourceIndex:int * destination:char[] * destinationIndex:int * count:int -> unit
  member EnsureCapacity : capacity:int -> int
  member Equals : sb:StringBuilder -> bool
  ...

Full name: System.Text.StringBuilder

--------------------
StringBuilder() : unit
StringBuilder(capacity: int) : unit
StringBuilder(value: string) : unit
StringBuilder(value: string, capacity: int) : unit
StringBuilder(capacity: int, maxCapacity: int) : unit
StringBuilder(value: string, startIndex: int, length: int, capacity: int) : unit
val sbErr : StringBuilder

Full name: Interactive.sbErr
val inStream : StringReader

Full name: Interactive.inStream
Multiple items
type StringReader =
  inherit TextReader
  new : s:string -> StringReader
  member Close : unit -> unit
  member Peek : unit -> int
  member Read : unit -> int + 1 overload
  member ReadLine : unit -> string
  member ReadToEnd : unit -> string

Full name: System.IO.StringReader

--------------------
StringReader(s: string) : unit
val outStream : StringWriter

Full name: Interactive.outStream
Multiple items
type StringWriter =
  inherit TextWriter
  new : unit -> StringWriter + 3 overloads
  member Close : unit -> unit
  member Encoding : Encoding
  member GetStringBuilder : unit -> StringBuilder
  member ToString : unit -> string
  member Write : value:char -> unit + 2 overloads

Full name: System.IO.StringWriter

--------------------
StringWriter() : unit
StringWriter(formatProvider: IFormatProvider) : unit
StringWriter(sb: StringBuilder) : unit
StringWriter(sb: StringBuilder, formatProvider: IFormatProvider) : unit
val errStream : StringWriter

Full name: Interactive.errStream
val argv : string []

Full name: Interactive.argv
val allArgs : string []

Full name: Interactive.allArgs
type Array =
  member Clone : unit -> obj
  member CopyTo : array:Array * index:int -> unit + 1 overload
  member GetEnumerator : unit -> IEnumerator
  member GetLength : dimension:int -> int
  member GetLongLength : dimension:int -> int64
  member GetLowerBound : dimension:int -> int
  member GetUpperBound : dimension:int -> int
  member GetValue : [<ParamArray>] indices:int[] -> obj + 7 overloads
  member Initialize : unit -> unit
  member IsFixedSize : bool
  ...

Full name: System.Array
val append : array1:'T [] -> array2:'T [] -> 'T []

Full name: Microsoft.FSharp.Collections.Array.append
val fsiConfig : FsiEvaluationSessionHostConfig

Full name: Interactive.fsiConfig
type FsiEvaluationSession =
  interface IDisposable
  member EvalExpression : code:string -> FsiValue option
  member EvalExpressionNonThrowing : code:string -> Choice<FsiValue option,exn> * FSharpErrorInfo []
  member EvalInteraction : code:string -> unit
  member EvalInteractionNonThrowing : code:string -> Choice<unit,exn> * FSharpErrorInfo []
  member EvalScript : filePath:string -> unit
  member EvalScriptNonThrowing : filePath:string -> Choice<unit,exn> * FSharpErrorInfo []
  member GetCompletions : longIdent:string -> seq<string>
  member Interrupt : unit -> unit
  member ParseAndCheckInteraction : code:string -> FSharpParseFileResults * FSharpCheckFileResults * FSharpCheckProjectResults
  ...

Full name: Microsoft.FSharp.Compiler.Interactive.Shell.FsiEvaluationSession
static member FsiEvaluationSession.GetDefaultConfiguration : unit -> FsiEvaluationSessionHostConfig
static member FsiEvaluationSession.GetDefaultConfiguration : fsiObj:obj -> FsiEvaluationSessionHostConfig
static member FsiEvaluationSession.GetDefaultConfiguration : fsiObj:obj * useFsiAuxLib:bool -> FsiEvaluationSessionHostConfig
val fsiSession : FsiEvaluationSession

Full name: Interactive.fsiSession
static member FsiEvaluationSession.Create : fsiConfig:FsiEvaluationSessionHostConfig * argv:string [] * inReader:TextReader * outWriter:TextWriter * errorWriter:TextWriter * ?collectible:bool * ?msbuildEnabled:bool -> FsiEvaluationSession
val evalExpression : text:string -> unit

Full name: Interactive.evalExpression


 式を評価して結果を返す
val text : string
member FsiEvaluationSession.EvalExpression : code:string -> FsiValue option
union case Option.Some: Value: 'T -> Option<'T>
val value : FsiValue
val printfn : format:Printf.TextWriterFormat<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
property FsiValue.ReflectionValue: obj
union case Option.None: Option<'T>
val evalExpressionTyped : text:string -> 'T

Full name: Interactive.evalExpressionTyped


 式を評価して、強く型付けされた結果を返す
val unbox : value:obj -> 'T

Full name: Microsoft.FSharp.Core.Operators.unbox
val failwith : message:string -> 'T

Full name: Microsoft.FSharp.Core.Operators.failwith
Multiple items
val int : value:'T -> int (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.int

--------------------
type int = int32

Full name: Microsoft.FSharp.Core.int

--------------------
type int<'Measure> = int

Full name: Microsoft.FSharp.Core.int<_>
member FsiEvaluationSession.EvalInteraction : code:string -> unit
type File =
  static member AppendAllLines : path:string * contents:IEnumerable<string> -> unit + 1 overload
  static member AppendAllText : path:string * contents:string -> unit + 1 overload
  static member AppendText : path:string -> StreamWriter
  static member Copy : sourceFileName:string * destFileName:string -> unit + 1 overload
  static member Create : path:string -> FileStream + 3 overloads
  static member CreateText : path:string -> StreamWriter
  static member Decrypt : path:string -> unit
  static member Delete : path:string -> unit
  static member Encrypt : path:string -> unit
  static member Exists : path:string -> bool
  ...

Full name: System.IO.File
File.WriteAllText(path: string, contents: string) : unit
File.WriteAllText(path: string, contents: string, encoding: Encoding) : unit
member FsiEvaluationSession.EvalScript : filePath:string -> unit
val result : Choice<unit,exn>

Full name: Interactive.result
val warnings : Compiler.FSharpErrorInfo []

Full name: Interactive.warnings
member FsiEvaluationSession.EvalScriptNonThrowing : filePath:string -> Choice<unit,exn> * Compiler.FSharpErrorInfo []
union case Choice.Choice1Of2: 'T1 -> Choice<'T1,'T2>
union case Choice.Choice2Of2: 'T2 -> Choice<'T1,'T2>
Multiple items
val exn : exn

--------------------
type exn = Exception

Full name: Microsoft.FSharp.Core.exn
property Exception.Message: string
val not : value:bool -> bool

Full name: Microsoft.FSharp.Core.Operators.not
val w : Compiler.FSharpErrorInfo
property Compiler.FSharpErrorInfo.Message: string
property Compiler.FSharpErrorInfo.StartLineAlternate: int
property Compiler.FSharpErrorInfo.StartColumn: int
val evalExpressionTyped2 : text:string -> 'T

Full name: Interactive.evalExpressionTyped2
val res : Choice<FsiValue option,exn>
val warnings : Compiler.FSharpErrorInfo []
member FsiEvaluationSession.EvalExpressionNonThrowing : code:string -> Choice<FsiValue option,exn> * Compiler.FSharpErrorInfo []
val sprintf : format:Printf.StringFormat<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.sprintf
namespace System.Threading
namespace System.Threading.Tasks
val sampleLongRunningExpr : string

Full name: Interactive.sampleLongRunningExpr
val task1 : Task<int>

Full name: Interactive.task1
Multiple items
type Task =
  new : action:Action -> Task + 7 overloads
  member AsyncState : obj
  member ContinueWith : continuationAction:Action<Task> -> Task + 9 overloads
  member CreationOptions : TaskCreationOptions
  member Dispose : unit -> unit
  member Exception : AggregateException
  member Id : int
  member IsCanceled : bool
  member IsCompleted : bool
  member IsFaulted : bool
  ...

Full name: System.Threading.Tasks.Task

--------------------
type Task<'TResult> =
  inherit Task
  new : function:Func<'TResult> -> Task<'TResult> + 7 overloads
  member ContinueWith : continuationAction:Action<Task<'TResult>> -> Task + 9 overloads
  member Result : 'TResult with get, set
  static member Factory : TaskFactory<'TResult>

Full name: System.Threading.Tasks.Task<_>

--------------------
Task(action: Action) : unit
Task(action: Action, cancellationToken: Threading.CancellationToken) : unit
Task(action: Action, creationOptions: TaskCreationOptions) : unit
Task(action: Action<obj>, state: obj) : unit
Task(action: Action, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : unit
Task(action: Action<obj>, state: obj, cancellationToken: Threading.CancellationToken) : unit
Task(action: Action<obj>, state: obj, creationOptions: TaskCreationOptions) : unit
Task(action: Action<obj>, state: obj, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : unit

--------------------
Task(function: Func<'TResult>) : unit
Task(function: Func<'TResult>, cancellationToken: Threading.CancellationToken) : unit
Task(function: Func<'TResult>, creationOptions: TaskCreationOptions) : unit
Task(function: Func<obj,'TResult>, state: obj) : unit
Task(function: Func<'TResult>, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : unit
Task(function: Func<obj,'TResult>, state: obj, cancellationToken: Threading.CancellationToken) : unit
Task(function: Func<obj,'TResult>, state: obj, creationOptions: TaskCreationOptions) : unit
Task(function: Func<obj,'TResult>, state: obj, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : unit
val task2 : Task<int>

Full name: Interactive.task2
property Task.Result: int
val parseResults : FSharpParseFileResults

Full name: Interactive.parseResults
val checkResults : FSharpCheckFileResults

Full name: Interactive.checkResults
val checkProjectResults : FSharpCheckProjectResults

Full name: Interactive.checkProjectResults
member FsiEvaluationSession.ParseAndCheckInteraction : code:string -> FSharpParseFileResults * FSharpCheckFileResults * FSharpCheckProjectResults
property FSharpCheckFileResults.Errors: Compiler.FSharpErrorInfo []
property Array.Length: int
member FSharpCheckFileResults.GetToolTipTextAlternate : line:int * colAtEndOfNames:int * lineText:string * names:string list * tokenTag:int -> Async<FSharpToolTipText>
module FSharpTokenTag

from Microsoft.FSharp.Compiler.SourceCodeServices
val IDENT : int

Full name: Microsoft.FSharp.Compiler.SourceCodeServices.FSharpTokenTag.IDENT
member FSharpCheckFileResults.GetSymbolUseAtLocation : line:int * colAtEndOfNames:int * lineText:string * names:string list -> Async<FSharpSymbolUse option>
val fsiConfig2 : FsiEvaluationSessionHostConfig

Full name: Interactive.fsiConfig2
val fsi : Interactive.InteractiveSession

Full name: Microsoft.FSharp.Compiler.Interactive.Settings.fsi
val collectionTest : unit -> unit

Full name: Interactive.collectionTest
val i : int32
val defaultArgs : string []
val inStream : StringReader
val outStream : StringWriter
val errStream : StringWriter
val fsiConfig : FsiEvaluationSessionHostConfig
val session : FsiEvaluationSession
val v : FsiValue option
property Option.Value: FsiValue
Fork me on GitHub