forked from dotnet/fsharp
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathinteractive.fsx
311 lines (230 loc) · 10.3 KB
/
interactive.fsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
(**
---
title: Tutorial: Hosted execution
category: FSharp.Compiler.Service
categoryindex: 300
index: 700
---
*)
(*** hide ***)
#I "../../artifacts/bin/FSharp.Compiler.Service/Debug/netstandard2.0"
(**
Interactive Service: Embedding F# Interactive
=============================================
This tutorial demonstrates how to embed F# interactive in your application. F# interactive
is an interactive scripting environment that compiles F# code into highly efficient IL code
and executes it on the fly. The F# interactive service allows you to embed F# evaluation in
your application.
> **NOTE:** There is a number of options for embedding F# Interactive. The easiest one is to use the
`fsi.exe` process and communicate with it using standard input and standard output. In this
tutorial, we look at calling F# Interactive directly through .NET API. However, if you have
no control over the input, it is a good idea to run F# interactive in a separate process.
One reason is that there is no way to handle `StackOverflowException` and so a poorly written
script can terminate the host process. **Remember that while calling F# Interactive through .NET API,
` --shadowcopyreferences` option will be ignored**. For detailed discussion, please take a look at
[this thread](https://github.com/fsharp/FSharp.Compiler.Service/issues/292).
> **NOTE:** If `FsiEvaluationSession.Create` fails with an error saying that `FSharp.Core.dll` cannot be found,
add the `FSharp.Core.sigdata` and `FSharp.Core.optdata` files. More info [here](https://fsharp.github.io/fsharp-compiler-docs/fcs/corelib.html).
However, the F# interactive service is still useful, because you might want to wrap it in your
own executable that is then executed (and communicates with the rest of your application), or
if you only need to execute a limited subset of F# code (e.g. generated by your own DSL).
Starting the F# interactive
---------------------------
First, we need to reference the libraries that contain the F# interactive service:
*)
#r "FSharp.Compiler.Service.dll"
open FSharp.Compiler.Interactive.Shell
open FSharp.Compiler.Tokenization
(**
To communicate with F# interactive, we need to create streams that represent input and
output. We will use those later to read the output printed as a result of evaluating some
F# code that prints:
*)
open System
open System.IO
open System.Text
// Initialize output and input streams
let sbOut = new StringBuilder()
let sbErr = new StringBuilder()
let inStream = new StringReader("")
let outStream = new StringWriter(sbOut)
let errStream = new StringWriter(sbErr)
// Build command line arguments & start FSI session
let argv = [| "C:\\fsi.exe" |]
let allArgs =
Array.append argv [| "--noninteractive" |]
let fsiConfig =
FsiEvaluationSession.GetDefaultConfiguration()
let fsiSession =
FsiEvaluationSession.Create(fsiConfig, allArgs, inStream, outStream, errStream)
(**
Evaluating and executing code
-----------------------------
The F# interactive service exposes several methods that can be used for evaluation. The first
is `EvalExpression` which evaluates an expression and returns its result. The result contains
the returned value (as `obj`) and the statically inferred type of the value:
*)
/// Evaluate expression & return the result
let evalExpression text =
match fsiSession.EvalExpression(text) with
| Some value -> printfn "%A" value.ReflectionValue
| None -> printfn "Got no result!"
(**
This takes a string as an argument and evaluates (i.e. executes) it as F# code.
*)
evalExpression "42+1" // prints '43'
(**
This can be used in a strongly typed way as follows:
*)
/// Evaluate expression & return the result, strongly typed
let evalExpressionTyped<'T> (text) =
match fsiSession.EvalExpression(text) with
| Some value -> value.ReflectionValue |> unbox<'T>
| None -> failwith "Got no result!"
evalExpressionTyped<int> "42+1" // gives '43'
(**
The `EvalInteraction` method can be used to evaluate side-effectful operations
such as printing, declarations, or other interactions that are not valid F# expressions, but can be entered in
the F# Interactive console. Such commands include `#time "on"` (and other directives), `open System`
all declarations and other top-level statements. The code
does not require `;;` at the end. Just enter the code that you want to execute:
*)
fsiSession.EvalInteraction "printfn \"bye\""
(**
The `EvalScript` method allows to evaluate a complete .fsx script.
*)
File.WriteAllText("sample.fsx", "let twenty = 10 + 10")
fsiSession.EvalScript "sample.fsx"
(**
Catching errors
------------------
``EvalExpression``, ``EvalInteraction`` and ``EvalScript`` are awkward if the
code has type checking warnings or errors, or if evaluation fails with an exception.
In these cases you can use ``EvalExpressionNonThrowing``, ``EvalInteractionNonThrowing``
and ``EvalScriptNonThrowing``. These return a tuple of a result and an array of ``FSharpDiagnostic`` values.
These represent the errors and warnings. The result part is a ``Choice<_,_>`` between an actual
result and an exception.
The result part of ``EvalExpression`` and ``EvalExpressionNonThrowing`` is an optional ``FSharpValue``.
If that value is not present then it just indicates that the expression didn't have a tangible
result that could be represented as a .NET object. This situation shouldn't actually
occur for any normal input expressions, and only for primitives used in libraries.
*)
File.WriteAllText("sample.fsx", "let twenty = 'a' + 10.0")
let result, warnings =
fsiSession.EvalScriptNonThrowing "sample.fsx"
// show the result
match result with
| Choice1Of2 () -> printfn "checked and executed ok"
| Choice2Of2 exn -> printfn "execution exception: %s" exn.Message
(**
Gives:
execution exception: Operation could not be completed due to earlier error
*)
// show the errors and warnings
for w in warnings do
printfn "Warning %s at %d,%d" w.Message w.StartLine w.StartColumn
(**
Gives:
Warning The type 'float' does not match the type 'char' at 1,19
Warning The type 'float' does not match the type 'char' at 1,17
For expressions:
*)
let evalExpressionTyped2<'T> text =
let res, warnings =
fsiSession.EvalExpressionNonThrowing(text)
for w in warnings do
printfn "Warning %s at %d,%d" w.Message w.StartLine w.StartColumn
match res with
| Choice1Of2 (Some value) -> value.ReflectionValue |> unbox<'T>
| Choice1Of2 None -> failwith "null or no result"
| Choice2Of2 (exn: exn) -> failwith (sprintf "exception %s" exn.Message)
evalExpressionTyped2<int> "42+1" // gives '43'
(**
Executing in parallel
------------------
By default the code passed to ``EvalExpression`` is executed immediately. To execute in parallel, submit a computation that starts a task:
*)
open System.Threading.Tasks
let sampleLongRunningExpr =
"""
async {
// The code of what you want to run
do System.Threading.Thread.Sleep 5000
return 10
}
|> Async.StartAsTask"""
let task1 =
evalExpressionTyped<Task<int>> (sampleLongRunningExpr)
let task2 =
evalExpressionTyped<Task<int>> (sampleLongRunningExpr)
(**
Both computations have now started. You can now fetch the results:
*)
task1.Result // gives the result after completion (up to 5 seconds)
task2.Result // gives the result after completion (up to 5 seconds)
(**
Type checking in the evaluation context
------------------
Let's assume you have a situation where you would like to typecheck code
in the context of the F# Interactive scripting session. For example, you first
evaluate a declaration:
*)
fsiSession.EvalInteraction "let xxx = 1 + 1"
(**
Now you want to typecheck the partially complete code `xxx + xx`
*)
let parseResults, checkResults, checkProjectResults =
fsiSession.ParseAndCheckInteraction("xxx + xx")
(**
The `parseResults` and `checkResults` have types `ParseFileResults` and `CheckFileResults`
explained in [Editor](editor.html). You can, for example, look at the type errors in the code:
*)
checkResults.Diagnostics.Length // 1
(**
The code is checked with respect to the logical type context available in the F# interactive session
based on the declarations executed so far.
You can also request declaration list information, tooltip text and symbol resolution:
*)
// get a tooltip
checkResults.GetToolTip(1, 2, "xxx + xx", [ "xxx" ], FSharpTokenTag.IDENT)
checkResults.GetSymbolUseAtLocation(1, 2, "xxx + xx", [ "xxx" ]) // symbol xxx
(**
The 'fsi' object
------------------
If you want your scripting code to be able to access the 'fsi' object, you should pass in an implementation of this object explicitly.
Normally the one from FSharp.Compiler.Interactive.Settings.dll is used.
*)
let fsiConfig2 =
FsiEvaluationSession.GetDefaultConfiguration(fsiSession)
(**
Collectible code generation
------------------
Evaluating code in using FsiEvaluationSession generates a .NET dynamic assembly and uses other resources.
You can make generated code collectible by passing `collectible=true`. However, code will only
be collected if there are no outstanding object references involving types, for example
`FsiValue` objects returned by `EvalExpression`, and you must have disposed the `FsiEvaluationSession`.
See also [Restrictions on Collectible Assemblies](https://learn.microsoft.com/previous-versions/dotnet/netframework-4.0/dd554932(v=vs.100)#restrictions).
The example below shows the creation of 200 evaluation sessions. Note that `collectible=true` and
`use session = ...` are both used.
If collectible code is working correctly,
overall resource usage will not increase linearly as the evaluation progresses.
*)
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 "iteration %d, result = %A" i v.Value.ReflectionValue
// collectionTest() <-- run the test like this