From ad8f9f55ea79a4d587aecc69698808da82de3d62 Mon Sep 17 00:00:00 2001 From: somhairle Date: Tue, 21 Apr 2026 20:40:27 +0800 Subject: 二〇二六年四月二十一日 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../10-Tips-for-Productive-FSharp-Scripting.md | 265 +++++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 source/_posts/10-Tips-for-Productive-FSharp-Scripting.md (limited to 'source/_posts/10-Tips-for-Productive-FSharp-Scripting.md') diff --git a/source/_posts/10-Tips-for-Productive-FSharp-Scripting.md b/source/_posts/10-Tips-for-Productive-FSharp-Scripting.md new file mode 100644 index 00000000..feaed28a --- /dev/null +++ b/source/_posts/10-Tips-for-Productive-FSharp-Scripting.md @@ -0,0 +1,265 @@ +--- +title: 10 Tips for Productive FSharp Scripting +date: 2024-10-18 23:40:21 +tags: [Technique, Archive] +--- + +Scott Hanselman recently had a [nice post on C# and F# REPLs](http://www.hanselman.com/blog/InteractiveCodingWithCAndFREPLsScriptCSOrTheVisualStudioInteractiveWindow.aspx), which reminded me of the time I started using F# scripts. Over time, I found out a couple of small tricks, which helped make the experience productive. I found about them mainly by accident, so I figured, let’s see if I can list them in one place! Some of these are super simple, some probably a bit obscure, but hopefully, one of them at least will make your path towards scripting nirvana an easier one… + +> Note: these tips are not necessarily ordered by usefulness. For that matter, there might or might not be exactly 10 of them :) + +## Tip 1: Use `.fsx` Files for Interactive Coding + +You can use the F# Interactive 2 ways: you can directly type code into FSI, the F# Interactive window, or you can write code in an `.fsx` file, and select pieces of the code you want to execute. I recommend the second approach, for at least two reasons. First, FSI is a very primitive environment, `.fsx` files provide a much richer experience (IntelliSense). Then this encourages writing clean scripts you can reuse later. + +> This is not specific to scripts, but… if you are on Visual Studio, do yourself a service and install the [Visual F# Power Tools](http://fsprojects.github.io/VisualFSharpPowerTools/) - you’ll get nice things such as better code highlighting, refactoring, and more. + +To execute code interactively, simply type code in an `.fsx` file, select a block of code, and hit Alt + Enter. The selected code will be evaluated, and the result will show up in the FSI window. In Visual Studio, you can also select code and right-click “Execute in Interactive”, but shortcuts are way faster. + +> You can also execute a single-line with Alt + '. I rarely use this option, but this can save you time because you don’t need to select the entire line of code. + +> In case the keyboard shortcuts to send code to FSI do not work anymore (ReSharper used to over-write them in the past), you can reset them in Visual Studio, by going to Tools / Options / Environment / Keyboard. The 2 commands you need to map are EditorContextMenus.CodeWindow.ExecuteInInteractive and EditorContextMenus.CodeWindow.ExecuteLineInInteractive. + +You can also use these shortcuts from a regular `.fs` file, which can be handy if you want to validate that a piece of code is behaving the way you want. + +> Interactive coding is by far my main usage for scripts - I use it extensively to prototype designs, run dumb tasks, or explore data or libraries. I realized recently that a few of my C# friends use LinqPad for the same purpose. + +## Tip 2: What is `it`? + +While I encourage working primarily from `.fsx` files, the FSI window is also very helpful. I use it primarily for small verifications. For instance, I might have in my script file code like this: + +``` +let add x y = + x + y +``` + +Once I send it for evaluation into FSI, I will see the following show up in FSI: + +``` +val add : x:int -> y:int -> int +> +``` + +My function `add` is now in memory, in my FSI session; I can start typing in the FSI window and use it: + +``` +> add 1 2;; +val it : int = 3 +> +``` + +Enter does not trigger execution in FSI. The `;;` indicates to FSI “Please execute everything I just typed, up to that point”. This is useful if you want to type multiple lines of code in FSI, and execute them as a block. + +> `it`: in our `add 1 2` example, the result showed up as `it`. We simply ran add, but didn’t assign the result to anything. `it` now contains the result, until we run another expression. If you want to re-use that value, you can assign it in FSI, by doing for instance `let x = it;;`.′ + +> Once a value is loaded in your FSI session, it will remain there, available to you until you shadow it (in the example above, `x` will remain available, until I run for instance `let x = 42;;`). This is extremely convenient: for instance, you can load a data file once `let data = File.ReadAllLines path`, and keep using `data` for as long as you want, without having to reload it between code changes. + +> FSI often shows an abbreviated version of values for large items. For instance, `[1..999]` will show up as `val it : int list = [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; ...]` - note the … at the end, which indicate that there is more. + +What if you inadvertently started a very long computation, or an infinite loop? In Visual Studio, you can either kill the session entirely, by right-clicking over the FSI window and selecting “Reset Interactive Session” or Ctrl + Alt + R, or cancel the latest evaluation you requested (“Cancel Interactive Evaluation”, or Ctrl + Break.). + +## Tip 3: Run Scripts from the Command Line + +Besides interactive scripting, you can also run a script from the command line, by using `FSI.exe`: + +`>fsi.exe "C:\myscript.fsx"` + +> `FSI.exe` is typically located at `C:\Program Files (x86)\Microsoft SDKs\F#\4.0\Framework\v4.0`. You can also install it separately, see [fsharp.org/use](http://fsharp.org/) section for instructions for various platforms. + +You can define different behaviors in your script, depending on whether it is run interactively or from the command line, like this: + +``` +#if INTERACTIVE +let msg = "Interactive" +#else +let msg = "Not Interactive" +#endif + +printfn "%s" msg +``` + +_Updated, Sep 19: thanks Matt Klein for [pointing the issue](http://stackoverflow.com/q/39581342/114519)._ + +For more information on FSI from the command line, [check the reference page here](https://msdn.microsoft.com/en-us/library/dd233175.aspx). + +_Updated, Feb 20: [Ramon Soto Mathiesen](https://twitter.com/genTauro42) points out that [Tip 9 also applies to the command line](https://twitter.com/genTauro42/status/696407757835132928)._ + +## Tip 4: Use Relative Paths + +Sometimes, your script will reference another resource; for instance, you need to read the contents of a `.txt` file somewhere. You can use absolute path, as in: + +``` +File.ReadAllLines @"C:/data/myfile.txt" +``` + +> Pre-pending a string with `@` makes it a verbatim string, and ignore escape sequences, such as `\`. + +> Use `/` rather than `\`, so that path work both on Windows and Mono. + +However, if that resource lives in a location relative to your script, consider using relative path, so that you can move your script folder around without breaking it. + +Relative paths can be a bit tricky; for instance, running the following code interactively… + +``` +System.Environment.CurrentDirectory +``` + +… produces a potentially unexpected result in FSI: + +``` +val it : string = "C:\Users\Mathias Brandewinder\AppData\Local\Temp" +> +``` + +You can avoid these issues by using built-in constants, which refer respectively to the directory where the script lives, the script file name, and the current line of the script: + +``` +__SOURCE_DIRECTORY__ +__SOURCE_FILE__ +__LINE__ +``` + +So if your folder structure was along these lines… + +``` +root + /src/script.fsx + /data/data.txt +``` + +… you could refer to the data file `data.txt` from your script like this: + +``` +let path = System.IO.Path.Combine(__SOURCE_DIRECTORY__,"..","data/data.txt") +System.IO.File.ReadAllText path +``` + +## Tip 5: Including Assemblies + +By default, FSI loads `FSharp.Core` and nothing else. If you want to use `System.DateTime`, you will need to first `open System` in your script. If you want to use an assembly that is not part of the standard .NET distribution, you will need to reference it first using `#r`. Imagine for instance that you installed the Nuget package `fsharp.data`; to use it in your script, you would do something like: + +``` +#r @"../packages/FSharp.Data.2.2.5/lib/net40/FSharp.Data.dll" +open FSharp.Data +``` + +> When you execute `open System` in interactive, don’t worry if nothing seems to happen: the only result is a new `>` showing up in FSI. + +For assemblies that are part of .NET but not referenced by default, you can use a shorter version: + +``` +#r @"System.Xaml" +open System.Xaml +``` + +> In Visual Studio, you can right-click a reference from Solution Explorer, and send to F# interactive. You can then directly open it, and start using it in FSI. + +_Updated, Feb 20: [Sergey Tihon](https://twitter.com/sergey_tihon) shared an interesting comment, explaining where Tip 5 can sometimes go wrong. I’d say, try Tip 5 first, but be aware that this might at times not quite work:_ + +> [@brandewinder](https://twitter.com/brandewinder) don't load assemblies like in Tip 5 ) [https://t.co/Owft1NmPoo](https://t.co/Owft1NmPoo) +> +> — Sergey Tihon (@sergey_tihon) [February 7, 2016](https://twitter.com/sergey_tihon/status/696395229285523456) + +_Updated, Feb 20: [F# open source contributor Don Syme](https://twitter.com/dsyme) share a related nice trick:_ + +> [@jeroldhaas](https://twitter.com/jeroldhaas) [@sergey_tihon](https://twitter.com/sergey_tihon) [@brandewinder](https://twitter.com/brandewinder) Use [#I](https://twitter.com/hashtag/I?src=hash) __SOURCE_DIRECTORY__, it is wondrous, very satisfying. All relative paths then work +> +> — Don Syme (@dsyme) [February 7, 2016](https://twitter.com/dsyme/status/696429115184955393) + +## Tip 6: Use `Paket` + +The Nuget package manager is useful to consume existing packages. However, by default, Nuget stores assemblies in a folder that includes the package version number. This is very impractical for a script. In our example above, if `fsharp.data` gets an update, our script reference will be broken once we update the Nuget package: + +`#r @"../packages/FSharp.Data.2.2.5/lib/net40/FSharp.Data.dll"` + +Fixing the script requires manually editing the version number in the path, which quickly becomes a pain. [Paket](https://fsprojects.github.io/Paket/) provides a better experience, because it stores packages without the version number, in this case, under: + +`#r @"../packages/FSharp.Data/lib/net40/FSharp.Data.dll"` + +Your scripts will now gracefully handle version number changes. + +If you end up consuming numerous packages, you can make your life even easier, by referencing paths where assemblies might be searched for, using `#I`: + +``` +#I @"../packages/ +#r @"FSharp.Data/lib/net40/FSharp.Data.dll" +``` + +> If your primary goal is to “just script”, consider using [Atom](https://atom.io/) or [VSCode](https://code.visualstudio.com/), with the [Ionide plugin](http://ionide.io/). You can create and run free-standing F# scripts, with beautiful [Paket integration](http://ionide.io/#paket-integration). + +## Tip 7: Include Files + +You might want to use the code from an existing file in your script. Suppose that we have a code file `Code.fs` somewhere, looking like this: + +``` +namespace Mathias + +module Common = + let hello name = sprintf "Hello, %s" name +``` + +You can use that code from your script, by using the `#load` directive: + +``` +#load "Code.fs" +open Mathias.Common +hello "World" +``` + +> You might have to close and re-open the script file if you end up changing the contents of the file. + +> If the file you are attempting to load contains references to other assemblies or files, you might get an error on the `#load` statement: “One or more errors in loaded files. The namespace or module … is not defined”. Simply reference the missing assemblies above the `#load` statement, so that your script uses the same dependencies as the file it refers to. + +## Tip 8: Profile your Code with #time + +Another handy directive, `#time`, turns on basic profiling. Once it is executed, for every block of code you send for execution you will see timing and garbage collection information. For instance, running this code… + +``` +#time +[| 1 .. 10000000 |] |> Array.map (fun x -> x * x) +``` + +… will produce the following in FSI: + +``` +--> Timing now on + +Real: 00:00:00.887, CPU: 00:00:00.828, GC gen0: 2, gen1: 2, gen2: 2 +val it : int [] = + [|1; 4; 9; 16; 25; 36; 49; // snipped for brevity +``` + +We get the wall time and CPU time it took, as well as some information about garbage collection in generations 0, 1 and 2. This would not replace a full-blown profiler, but this is an awfully convenient tool to figure out quickly if there are obvious ways to improve a piece of code. + +Note that every time you execute `#time`, the timer will be switched from on to off, or vice-versa. This is not always convenient; you can also explicitly set it to the desired state, like this: + +``` +#time "on" +// everything now is timed +#time "off" +``` + +> If you are interested in profiling, you should take a look at [PrivateEye](http://www.privateeye.io/); check out [Greg Young](https://twitter.com/gregyoung)’s [talk at NDC Oslo 2015](https://vimeo.com/131637366) to get a feel for what it does. + +## Tip 9: Turn 64-bits on + +Hat tip to [Rick Minerich](https://twitter.com/rickasaurus) for that one. I’ll refer you to his blog post to see how to [set FSI to 64 bits to handle large datasets](http://richardminerich.com/2013/03/setting-up-fsharp-interactive-for-machine-learning-with-large-datasets/). + +## Tip 10: Bonus Material + +Did you know that you could… + +- [debug an F# script? (around 0:12:35 in)](https://channel9.msdn.com/Events/Visual-Studio/Visual-Studio-2015-Final-Release-Event/Six-Quick-Picks-from-Visual-F-40) +- [inspect the objects in your FSI session with FsEye?](http://www.swensensoftware.com/fseye) +- change the FSI font size in Tools/Options/Environment/Fonts and Colors/Show Settings for/F# Interactive? +- add your own pretty-printer to FSI, [like this](https://github.com/mathnet/mathnet-numerics/blob/master/src/FSharp/MathNet.Numerics.fsx)? +- mess with your coworkers’ mental sanity, by executing `(*` (opening a multiline comment) in FSI? (credit: [Tomas](https://twitter.com/tomaspetricek)) +- simplify loading references with Visual Studio and Power Tools? (credit: [Kit Eason](https://twitter.com/kitlovesfsharp), see details in comments section). + +And again… if you are not using the [Visual F# Power Tools](http://fsprojects.github.io/VisualFSharpPowerTools/), you are missing out: + +> "Don't let your friends try [#fsharp](https://twitter.com/hashtag/fsharp?src=hash) without installing [@FSPowerTools](https://twitter.com/FSPowerTools)." [@dsyme](https://twitter.com/dsyme) at [#ndclondon](https://twitter.com/hashtag/ndclondon?src=hash) +> +> — Tomas Petricek (@tomaspetricek) [January 15, 2016](https://twitter.com/tomaspetricek/status/687934127627186176) + +That’s what I got! I am sure I forgot some - do you have a useful or favorite trick to share? \ No newline at end of file -- cgit v1.2.3