{"id":432,"date":"2025-09-12T13:30:30","date_gmt":"2025-09-12T12:30:30","guid":{"rendered":"https:\/\/informedica.nl\/?p=432"},"modified":"2025-09-12T13:32:00","modified_gmt":"2025-09-12T12:32:00","slug":"working-with-f-interactive-fsi-a-development-workflow","status":"publish","type":"post","link":"https:\/\/informedica.nl\/?p=432","title":{"rendered":"Working with F# Interactive (FSI): A Development Workflow"},"content":{"rendered":"\n<p>F# Interactive (FSI) has fundamentally changed how I approach software development. Rather than the traditional edit-compile-run cycle, FSI enables a direct, conversational\/interactive approach to coding that eliminates friction and accelerates development. This post explores the practical advantages I&#8217;ve discovered through years of FSI-first development using F# script files.<\/p>\n\n\n<p><!--more--><\/p>\n\n\n<h2 class=\"wp-block-heading\">Script-Based Development with Immediate Feedback<\/h2>\n\n\n\n<p>My preferred approach is working with <code>.fsx<\/code> files that can be executed directly in FSI. There&#8217;s no project setup, no build configuration, no waiting for compilation. You write code in a script file and execute it instantly:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"fsharp\" class=\"language-fsharp\">\/\/ factorial.fsx\nlet factorial n = \n    let rec loop acc i = \n        if i &lt;= 1 then acc \n        else loop (acc * i) (i - 1)\n    loop 1 n\n\n\/\/ Test the function\nlet result = factorial 5\nprintfn \"5! = %d\" result<\/code><\/pre>\n\n\n\n<p>Execute with: <code>dotnet fsi factorial.fsx<\/code>. Or, directly send it to the FSI using for example VSCode or Rider.<\/p>\n\n\n\n<p>Output:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">5! = 120<\/code><\/pre>\n\n\n\n<p>This immediacy fundamentally changes how you think about code. Instead of writing large blocks and hoping they work, you build incrementally in script files, verifying each step. The feedback loop is measured in seconds, not minutes.<\/p>\n\n\n\n<p><strong>Note<\/strong>: you can even select specific parts of the code and evaluate only that selection directly in the FSI.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Exploring Existing Codebases<\/h2>\n\n\n\n<p>One of FSI&#8217;s most powerful features is its ability to load and interact with existing assemblies and source files. When working with an unfamiliar codebase, I create exploration scripts that load modules and experiment with functions:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"fsharp\" class=\"language-fsharp\">\/\/ explore.fsx\n#load \"DataProcessor.fs\"\nopen DataProcessor\n\nlet sampleData = [1; 2; 3; 4; 5]\nlet result = processData sampleData\nprintfn \"Sample data: %A\" sampleData\nprintfn \"Processed result: %A\" result\n\n\/\/ Experiment with edge cases\nlet emptyResult = processData []\nprintfn \"Empty input result: %A\" emptyResult\n\n\/\/ Test with different data types\nlet stringData = [\"a\"; \"b\"; \"c\"]\n\/\/ This will show compile error if processData doesn't handle strings<\/code><\/pre>\n\n\n\n<p>Execute with: <code>dotnet fsi explore.fsx<\/code><\/p>\n\n\n\n<p>This exploratory approach is invaluable for understanding legacy code or third-party libraries. You can probe functions with various inputs, examine their behavior, and build understanding through experimentation rather than static code reading.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Module Shadowing for Safe Refactoring<\/h2>\n\n\n\n<p>FSI&#8217;s module system allows you to shadow existing modules with new implementations. This enables safe refactoring directly in script files:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"fsharp\" class=\"language-fsharp\">\/\/ refactor-test.fsx\n\/\/ Load original module\n#load \"Calculator.fs\"\n\n\/\/ Test original behavior\nlet originalResult = Calculator.add 5 3\nprintfn \"Original add result: %d\" originalResult\n\n\/\/ Shadow with improved implementation\nmodule Calculator =\n    let add x y = \n        printfn \"Adding %d and %d\" x y\n        x + y\n\n    let multiply x y = \n        printfn \"Multiplying %d and %d\" x y\n        x * y\n\n\/\/ Test new implementation\nprintfn \"Testing new implementation:\"\nlet newResult = Calculator.add 5 3\nprintfn \"New add result: %d\" newResult\n\nlet multiplyResult = Calculator.multiply 4 7\nprintfn \"Multiply result: %d\" multiplyResult<\/code><\/pre>\n\n\n\n<p>Execute with: <code>dotnet fsi refactor-test.fsx<\/code><\/p>\n\n\n\n<p>The shadowed module completely replaces the original in the script execution. You can test the new implementation thoroughly before committing changes to source files. This approach eliminates the fear of breaking existing code during refactoring.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">My Development Workflow<\/h2>\n\n\n\n<p>My typical development process follows this pattern:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Create an <code>.fsx<\/code> script file for experimentation<\/li>\n\n\n\n<li>Load relevant modules and experiment with data structures<\/li>\n\n\n\n<li>Build functions incrementally, testing each piece<\/li>\n\n\n\n<li>Refactor and optimize in the script environment<\/li>\n\n\n\n<li>Copy the final, tested code to source files<\/li>\n<\/ol>\n\n\n\n<p>Here&#8217;s a concrete example of building a simple text parser:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"fsharp\" class=\"language-fsharp\">\/\/ text-parser.fsx\nlet input = \"name:John,age:30,city:Amsterdam\"\n\n\/\/ Build parsing functions incrementally\nlet splitPairs (s: string) = s.Split(',')\n\n\/\/ Test it immediately\nlet pairs = splitPairs input\nprintfn \"Split pairs: %A\" pairs\n\nlet parseKeyValue (pair: string) =\n    match pair.Split(':') with\n    | [|key; value|] -&gt; Some (key.Trim(), value.Trim())\n    | _ -&gt; None\n\n\/\/ Test parsing\nlet parsedPairs = pairs |&gt; Array.map parseKeyValue \nprintfn \"Parsed pairs: %A\" parsedPairs\n\n\/\/ Combine into final function\nlet parseConfig input =\n    input\n    |&gt; splitPairs\n    |&gt; Array.choose parseKeyValue\n    |&gt; Map.ofArray\n\n\/\/ Final test\nlet config = parseConfig input\nprintfn \"Final config: %A\" config\nprintfn \"Name: %s\" config.[\"name\"]\nprintfn \"Age: %s\" config.[\"age\"]\nprintfn \"City: %s\" config.[\"city\"]<\/code><\/pre>\n\n\n\n<p>Execute with: <code>dotnet fsi text-parser.fsx<\/code><\/p>\n\n\n\n<p>Each step is validated immediately. By the time I copy this code to a source file, I know it works correctly.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Advanced FSI Features<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Loading External Dependencies<\/h3>\n\n\n\n<p>FSI scripts can load NuGet packages and external assemblies directly:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"fsharp\" class=\"language-fsharp\">\/\/ json-example.fsx\n#r \"nuget: Newtonsoft.Json\"\nopen Newtonsoft.Json\n\nlet data = {| name = \"John\"; age = 30; city = \"Amsterdam\" |}\nlet json = JsonConvert.SerializeObject(data)\nprintfn \"Serialized: %s\" json\n\nlet deserialized = JsonConvert.DeserializeObject(json)\nprintfn \"Deserialized: %A\" deserialized<\/code><\/pre>\n\n\n\n<p>Execute with: <code>dotnet fsi json-example.fsx<\/code><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Performance Profiling<\/h3>\n\n\n\n<p>You can measure execution time using <code>#time<\/code> directive in scripts:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"fsharp\" class=\"language-fsharp\">\/\/ performance-test.fsx\n#time \"on\"\n\nlet slowFunction () = \n    [1..1000000] |&gt; List.sum\n\nprintfn \"Testing performance...\"\nlet result = slowFunction()\nprintfn \"Sum result: %d\" result<\/code><\/pre>\n\n\n\n<p>Execute with: <code>dotnet fsi performance-test.fsx<\/code><\/p>\n\n\n\n<p>Running this script shows timing information for each operation.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Script-like Development<\/h3>\n\n\n\n<p>FSI bridges the gap between scripting and compiled languages. You can write substantial programs that feel like scripts but have the performance and type safety of compiled F#:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"fsharp\" class=\"language-fsharp\">\/\/ web-download.fsx\nopen System.Net.Http\nopen System.Threading.Tasks\n\nlet downloadAndProcess (url: string) =\n    task {\n        use client = new HttpClient()\n        let! content = client.GetStringAsync(url)\n        return content.Length\n    }\n\nlet urls = [\n    \"https:\/\/api.github.com\/users\/octocat\"\n    \"https:\/\/httpbin.org\/json\"\n]\n\nprintfn \"Downloading content lengths...\"\nfor url in urls do\n    try\n        let length = downloadAndProcess url |&gt; Async.AwaitTask |&gt; Async.RunSynchronously\n        printfn \"URL: %s, Content Length: %d\" url length\n    with\n    | ex -&gt; printfn \"Error downloading %s: %s\" url ex.Message<\/code><\/pre>\n\n\n\n<p>Execute with: <code>dotnet fsi web-download.fsx<\/code><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Type-Driven Development<\/h2>\n\n\n\n<p>FSI excels at type-driven development in script files. You can define types first and let the compiler guide implementation:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"fsharp\" class=\"language-fsharp\">\/\/ type-driven.fsx\ntype Customer = { Id: int; Name: string; Email: string }\ntype Order = { Id: int; CustomerId: int; Items: string list; Total: decimal }\n\nlet findCustomerOrders (customers: Customer list) (orders: Order list) customerId =\n    orders |&gt; List.filter (fun o -&gt; o.CustomerId = customerId)\n\n\/\/ Test data\nlet customers = [\n    { Id = 1; Name = \"John Doe\"; Email = \"john@example.com\" }\n    { Id = 2; Name = \"Jane Smith\"; Email = \"jane@example.com\" }\n]\n\nlet orders = [\n    { Id = 101; CustomerId = 1; Items = [\"laptop\"; \"mouse\"]; Total = 1200M }\n    { Id = 102; CustomerId = 2; Items = [\"keyboard\"]; Total = 100M }\n    { Id = 103; CustomerId = 1; Items = [\"monitor\"]; Total = 300M }\n]\n\n\/\/ Test the function\nlet johnOrders = findCustomerOrders customers orders 1\nprintfn \"John's orders: %A\" johnOrders\nlet totalValue = johnOrders |&gt; List.sumBy (fun o -&gt; o.Total)\nprintfn \"Total value: %M\" totalValue<\/code><\/pre>\n\n\n\n<p>Execute with: <code>dotnet fsi type-driven.fsx<\/code><\/p>\n\n\n\n<p>The type signatures provide immediate feedback about function behavior and help catch errors early when you run the script.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Working with Existing Projects<\/h2>\n\n\n\n<p>When working with existing F# projects, I create script files to explore and test without affecting the main codebase:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"fsharp\" class=\"language-fsharp\">\/\/ project-exploration.fsx\n\/\/ Reference the compiled project\n#r \"bin\/Debug\/net8.0\/MyProject.dll\"\nopen MyProject.Core\nopen MyProject.Models\n\n\/\/ Test existing functions with new data\nlet testUser = { Name = \"Test User\"; Email = \"test@example.com\" }\nlet validationResult = UserValidation.validate testUser\nprintfn \"Validation result: %A\" validationResult\n\n\/\/ Experiment with modifications\nlet improvedValidation user =\n    \/\/ Test enhanced validation logic here\n    UserValidation.validate user\n    \/\/ Additional checks...\n\nlet enhancedResult = improvedValidation testUser\nprintfn \"Enhanced validation: %A\" enhancedResult<\/code><\/pre>\n\n\n\n<p>Execute with: <code>dotnet fsi project-exploration.fsx<\/code><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Limitations and Considerations<\/h2>\n\n\n\n<p>FSI isn&#8217;t perfect for all scenarios. Large applications with complex build processes, extensive configuration, or performance-critical code may require traditional compilation. <\/p>\n\n\n\n<p>However, for exploration, prototyping, data analysis, and incremental development, FSI provides an unmatched development experience. Also, even with large code bases, you can opt to use parts of the code base in the FSI.<\/p>\n\n\n\n<p>Script files also have some limitations:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>No IntelliSense in basic text editors (though many IDEs support <code>.fsx<\/code> files)<\/li>\n\n\n\n<li>Debugging is more limited compared to compiled projects, however, who needs debugging when writing F#<\/li>\n\n\n\n<li>Large scripts can become unwieldy without proper organization. You can move everything in modules, however, effectively creating an organized script file!<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>F# Interactive has transformed my development workflow from a traditional batch process to an interactive conversation with code through script files. The ability to experiment freely, shadow modules safely, and build incrementally with immediate feedback creates a development experience that&#8217;s both more productive and more enjoyable. While I eventually move tested code to proper source files, the path to that final code is far more direct and confident through FSI&#8217;s script-based interactive environment.<\/p>\n\n\n\n<p>The key is treating script files as throwaway exploration tools &#8211; write them quickly, test ideas immediately, and don&#8217;t worry about perfect code structure. Once you&#8217;ve proven your approach works, then refactor and move to proper source files. This workflow has dramatically improved my productivity and code quality.<\/p>\n\n\n\n<p><strong>Bottom Line: USE THE FSI<\/strong><\/p>\n","protected":false},"excerpt":{"rendered":"<p>F# Interactive (FSI) has fundamentally changed how I approach software development. Rather than the traditional edit-compile-run cycle, FSI enables a direct, conversational\/interactive approach to coding that eliminates friction and accelerates development. This post explores the practical advantages I&#8217;ve discovered through years of FSI-first development using F# script files.<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[9],"tags":[],"class_list":["post-432","post","type-post","status-publish","format-standard","hentry","category-programming"],"_links":{"self":[{"href":"https:\/\/informedica.nl\/index.php?rest_route=\/wp\/v2\/posts\/432","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/informedica.nl\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/informedica.nl\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/informedica.nl\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/informedica.nl\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=432"}],"version-history":[{"count":3,"href":"https:\/\/informedica.nl\/index.php?rest_route=\/wp\/v2\/posts\/432\/revisions"}],"predecessor-version":[{"id":435,"href":"https:\/\/informedica.nl\/index.php?rest_route=\/wp\/v2\/posts\/432\/revisions\/435"}],"wp:attachment":[{"href":"https:\/\/informedica.nl\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=432"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/informedica.nl\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=432"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/informedica.nl\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=432"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}