navigation

インタラクティブサービス: 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サービスを含むライブラリへの参照を追加します:

#r "FSharp.Compiler.Service.dll"
open FSharp.Compiler.SourceCodeServices
open FSharp.Compiler.Interactive.Shell

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

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 として)含まれる他、値に対して静的に推論された型も含まれます:

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

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

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

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

/// 式を評価して、強く型付けされた結果を返す
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 、 その他の宣言やトップレベルステートメントなどが該当します。 指定するコードの終端に ;; を入力する必要はありません。 実行したいコードだけを入力します:

fsiSession.EvalInteraction "printfn \"bye\""

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

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

例外処理

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

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

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

は次のようになります:

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

は次のようになります:

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

式に対しては:

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 に渡したコードは即時実行されます。 並列に実行するために、タスクを開始する計算を投入します:

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)

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

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

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

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

fsiSession.EvalInteraction "let xxx = 1 + 1"

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

let parseResults, checkResults, checkProjectResults =
    fsiSession.ParseAndCheckInteraction("xxx + xx") |> Async.RunSynchronously

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

checkResults.Errors.Length // 1

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

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

open FSharp.Compiler

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

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

'fsi'オブジェクト

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

let fsiConfig2 = FsiEvaluationSession.GetDefaultConfiguration(fsi)

収集可能なコード生成

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

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

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

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()  <-- このようにテストを実行する
Multiple items
namespace FSharp

--------------------
namespace Microsoft.FSharp
namespace FSharp.Compiler
namespace FSharp.Compiler.SourceCodeServices
Multiple items
namespace FSharp

--------------------
namespace Microsoft.FSharp

--------------------
type FSharpAttribute =
  member Format : context:FSharpDisplayContext -> string
  member AttributeType : FSharpEntity
  member ConstructorArguments : IList<FSharpType * obj>
  member IsUnresolved : bool
  member NamedArguments : IList<FSharpType * string * bool * obj>
namespace FSharp.Compiler.Interactive
module Shell

from FSharp.Compiler.Interactive
namespace System
namespace System.IO
namespace System.Text
val sbOut : StringBuilder
Multiple items
type StringBuilder =
  new : unit -> StringBuilder + 5 overloads
  member Append : value:string -> StringBuilder + 23 overloads
  member AppendFormat : format:string * arg0:obj -> StringBuilder + 7 overloads
  member AppendJoin : separator:string * [<ParamArray>] values:obj[] -> StringBuilder + 5 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:Span<char> * count:int -> unit + 1 overload
  member EnsureCapacity : capacity:int -> int
  ...
  nested type ChunkEnumerator

--------------------
StringBuilder() : StringBuilder
StringBuilder(capacity: int) : StringBuilder
StringBuilder(value: string) : StringBuilder
StringBuilder(value: string, capacity: int) : StringBuilder
StringBuilder(capacity: int, maxCapacity: int) : StringBuilder
StringBuilder(value: string, startIndex: int, length: int, capacity: int) : StringBuilder
val sbErr : StringBuilder
val inStream : StringReader
Multiple items
type StringReader =
  inherit TextReader
  new : s:string -> StringReader
  member Close : unit -> unit
  member Peek : unit -> int
  member Read : unit -> int + 2 overloads
  member ReadAsync : buffer:Memory<char> * ?cancellationToken:CancellationToken -> ValueTask<int> + 1 overload
  member ReadBlock : buffer:Span<char> -> int
  member ReadBlockAsync : buffer:Memory<char> * ?cancellationToken:CancellationToken -> ValueTask<int> + 1 overload
  member ReadLine : unit -> string
  member ReadLineAsync : unit -> Task<string>
  member ReadToEnd : unit -> string
  ...

--------------------
StringReader(s: string) : StringReader
val outStream : StringWriter
Multiple items
type StringWriter =
  inherit TextWriter
  new : unit -> StringWriter + 3 overloads
  member Close : unit -> unit
  member Encoding : Encoding
  member FlushAsync : unit -> Task
  member GetStringBuilder : unit -> StringBuilder
  member ToString : unit -> string
  member Write : value:char -> unit + 4 overloads
  member WriteAsync : value:char -> Task + 4 overloads
  member WriteLine : buffer:ReadOnlySpan<char> -> unit + 1 overload
  member WriteLineAsync : value:char -> Task + 4 overloads

--------------------
StringWriter() : StringWriter
StringWriter(formatProvider: IFormatProvider) : StringWriter
StringWriter(sb: StringBuilder) : StringWriter
StringWriter(sb: StringBuilder, formatProvider: IFormatProvider) : StringWriter
val errStream : StringWriter
val argv : string []
val allArgs : string []
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
  ...
val append : array1:'T [] -> array2:'T [] -> 'T []
val fsiConfig : FsiEvaluationSessionHostConfig
type FsiEvaluationSession =
  interface IDisposable
  member AddBoundValue : name:string * value:obj -> unit
  member EvalExpression : code:string -> FsiValue option
  member EvalExpressionNonThrowing : code:string -> Choice<FsiValue option,exn> * FSharpErrorInfo []
  member EvalInteraction : code:string * ?cancellationToken:CancellationToken -> unit
  member EvalInteractionNonThrowing : code:string * ?cancellationToken:CancellationToken -> Choice<FsiValue option,exn> * FSharpErrorInfo []
  member EvalScript : filePath:string -> unit
  member EvalScriptNonThrowing : filePath:string -> Choice<unit,exn> * FSharpErrorInfo []
  member FormatValue : reflectionValue:obj * reflectionType:Type -> string
  member GetBoundValues : unit -> FsiBoundValue list
  ...
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
static member FsiEvaluationSession.Create : fsiConfig:FsiEvaluationSessionHostConfig * argv:string [] * inReader:TextReader * outWriter:TextWriter * errorWriter:TextWriter * ?collectible:bool * ?legacyReferenceResolver:FSharp.Compiler.ReferenceResolver.Resolver -> FsiEvaluationSession
val evalExpression : text:string -> unit


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


 式を評価して、強く型付けされた結果を返す
val unbox : value:obj -> 'T
val failwith : message:string -> 'T
Multiple items
val int : value:'T -> int (requires member op_Explicit)

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

--------------------
type int<'Measure> = int
type File =
  static member AppendAllLines : path:string * contents:IEnumerable<string> -> unit + 1 overload
  static member AppendAllLinesAsync : path:string * contents:IEnumerable<string> * ?cancellationToken:CancellationToken -> Task + 1 overload
  static member AppendAllText : path:string * contents:string -> unit + 1 overload
  static member AppendAllTextAsync : path:string * contents:string * ?cancellationToken:CancellationToken -> Task + 1 overload
  static member AppendText : path:string -> StreamWriter
  static member Copy : sourceFileName:string * destFileName:string -> unit + 1 overload
  static member Create : path:string -> FileStream + 2 overloads
  static member CreateText : path:string -> StreamWriter
  static member Decrypt : path:string -> unit
  static member Delete : path:string -> unit
  ...
File.WriteAllText(path: string, contents: string) : unit
File.WriteAllText(path: string, contents: string, encoding: Encoding) : unit
val result : Choice<unit,exn>
val warnings : 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
val not : value:bool -> bool
val w : FSharpErrorInfo
val evalExpressionTyped2 : text:string -> 'T
val res : Choice<FsiValue option,exn>
val sprintf : format:Printf.StringFormat<'T> -> 'T
namespace System.Threading
namespace System.Threading.Tasks
val sampleLongRunningExpr : string
val task1 : Task<int>
Multiple items
type Task =
  new : action:Action -> Task + 7 overloads
  member AsyncState : obj
  member ConfigureAwait : continueOnCapturedContext:bool -> ConfiguredTaskAwaitable
  member ContinueWith : continuationAction:Action<Task> -> Task + 19 overloads
  member CreationOptions : TaskCreationOptions
  member Dispose : unit -> unit
  member Exception : AggregateException
  member GetAwaiter : unit -> TaskAwaiter
  member Id : int
  member IsCanceled : bool
  ...

--------------------
type Task<'TResult> =
  inherit Task
  new : function:Func<'TResult> -> Task<'TResult> + 7 overloads
  member ConfigureAwait : continueOnCapturedContext:bool -> ConfiguredTaskAwaitable<'TResult>
  member ContinueWith : continuationAction:Action<Task<'TResult>> -> Task + 19 overloads
  member GetAwaiter : unit -> TaskAwaiter<'TResult>
  member Result : 'TResult
  static member Factory : TaskFactory<'TResult>

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

--------------------
Task(function: Func<'TResult>) : Task<'TResult>
Task(function: Func<'TResult>, cancellationToken: Threading.CancellationToken) : Task<'TResult>
Task(function: Func<'TResult>, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(function: Func<obj,'TResult>, state: obj) : Task<'TResult>
Task(function: Func<'TResult>, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(function: Func<obj,'TResult>, state: obj, cancellationToken: Threading.CancellationToken) : Task<'TResult>
Task(function: Func<obj,'TResult>, state: obj, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(function: Func<obj,'TResult>, state: obj, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task<'TResult>
val task2 : Task<int>
val parseResults : FSharpParseFileResults
val checkResults : FSharpCheckFileResults
val checkProjectResults : FSharpCheckProjectResults
Multiple items
type Async =
  static member AsBeginEnd : computation:('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit)
  static member AwaitEvent : event:IEvent<'Del,'T> * ?cancelAction:(unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate)
  static member AwaitIAsyncResult : iar:IAsyncResult * ?millisecondsTimeout:int -> Async<bool>
  static member AwaitTask : task:Task -> Async<unit>
  static member AwaitTask : task:Task<'T> -> Async<'T>
  static member AwaitWaitHandle : waitHandle:WaitHandle * ?millisecondsTimeout:int -> Async<bool>
  static member CancelDefaultToken : unit -> unit
  static member Catch : computation:Async<'T> -> Async<Choice<'T,exn>>
  static member Choice : computations:seq<Async<'T option>> -> Async<'T option>
  static member FromBeginEnd : beginAction:(AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
  ...

--------------------
type Async<'T> =
static member Async.RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:Threading.CancellationToken -> 'T
module FSharpTokenTag

from FSharp.Compiler.SourceCodeServices
val IDENT : int
val fsiConfig2 : FsiEvaluationSessionHostConfig
val collectionTest : unit -> unit
val i : int32
val defaultArgs : string []
val session : FsiEvaluationSession
static member FsiEvaluationSession.Create : fsiConfig:FsiEvaluationSessionHostConfig * argv:string [] * inReader:TextReader * outWriter:TextWriter * errorWriter:TextWriter * ?collectible:bool * ?legacyReferenceResolver:ReferenceResolver.Resolver -> FsiEvaluationSession
val v : FsiValue option