diff --git a/.editorconfig b/.editorconfig index 6f74c81..b228e14 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,3 +4,4 @@ root = true fsharp_multiline_bracket_style = stroustrup fsharp_record_multiline_formatter = number_of_items fsharp_max_record_number_of_items = 1 +fsharp_max_infix_operator_expression = 60 diff --git a/.vscode/settings.json b/.vscode/settings.json index 46de6ed..53894eb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "cSpell.words": [ "aiohttp", "alfonsogarciacaro", + "boto", "Buildalyzer", "coro", "destructures", @@ -19,9 +20,13 @@ "nativeint", "ncave", "pathlib", + "pdfplumber", + "Plotly", "pyfile", "pyname", + "Pythonistas", "Pyxpecto", + "scikit", "Sergey", "stroustrup", "Tihon", diff --git a/CLAUDE.md b/CLAUDE.md index 8e06c25..9ece319 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -95,4 +95,4 @@ docs/ - [Fable.Python docs](https://fable.io/docs/getting-started/python.html) - [Fable.Python GitHub](https://github.com/fable-compiler/Fable.Python/) -- [Content Plan](CONTENT-PLAN.md) - Chapter structure and TODO items +- [Content Plan](CONTENT-PLAN.md) - Chapter structure and TODO items (important!) diff --git a/Fable.Literate/App.fs b/Fable.Literate/App.fs index de70896..f9a5bb7 100644 --- a/Fable.Literate/App.fs +++ b/Fable.Literate/App.fs @@ -100,7 +100,9 @@ module Utils = if m.Value.Length = 1 then m.Value.ToLowerInvariant() else - m.Value.Substring(0, 1) + "_" + m.Value.Substring(1, 1).ToLowerInvariant() + m.Value.Substring(0, 1) + + "_" + + m.Value.Substring(1, 1).ToLowerInvariant() ) else name @@ -328,7 +330,10 @@ module MarkdownPrinter = | IncludePython symbols -> // Unresolved - should have been transformed let symbolList = String.concat ", " symbols - "\n\n" + + "\n\n" | Hidden _ -> "" // Should have been filtered /// Render a document to markdown string. @@ -348,7 +353,11 @@ module MarkdownPrinter = | s when s.StartsWith "#" -> inCodeBlock, ("#" + line) :: acc | _ -> inCodeBlock, line :: acc - lines |> Array.fold folder (false, []) |> snd |> List.rev |> String.concat "\n" + lines + |> Array.fold folder (false, []) + |> snd + |> List.rev + |> String.concat "\n" (** ## Pipeline Module diff --git a/Fable.Literate/Fable.Literate.fsproj b/Fable.Literate/Fable.Literate.fsproj index 5022a0a..bb33b37 100644 --- a/Fable.Literate/Fable.Literate.fsproj +++ b/Fable.Literate/Fable.Literate.fsproj @@ -8,7 +8,7 @@ - + diff --git a/README.md b/README.md index 78db4c6..5c66420 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > Write F#, run Python - a practical guide to Fable.Python -This is a comprehensive guide to [Fable.Python](https://github.com/fable-compiler/Fable.Python/), written as literate F# that transpiles to Python and generates its own documentation. +This blog-post is a guide to [Fable.Python](https://github.com/fable-compiler/Fable.Python/), written as literate F# that transpiles to Python and generates its own documentation. ## Chapters @@ -23,9 +23,10 @@ This is a comprehensive guide to [Fable.Python](https://github.com/fable-compile ## The Strange Loop -This guide is self-documenting: each chapter is an `.fs` file with embedded Markdown comments. **Fabletext** (the final chapter) processes these files to generate the documentation you're reading - including itself. +This guide is self-documenting: each chapter is an `.fs` file with embedded Markdown comments. **Fable.Literate** (the final chapter) processes these files to generate the documentation you're reading - including itself. The chain: + 1. Write F# with embedded Markdown (`chapters/*.fs`) 2. Compile to Python with Fable 3. Run Fabletext (F# compiled to Python) to extract documentation diff --git a/chapters/AsyncProgramming.fs b/chapters/AsyncProgramming.fs index a1a01ec..cb2edee 100644 --- a/chapters/AsyncProgramming.fs +++ b/chapters/AsyncProgramming.fs @@ -76,7 +76,9 @@ F# async shines when composing multiple operations: let fetchMultipleAsync () = async { - let! results = [ fetchDataAsync (); fetchDataAsync (); fetchDataAsync () ] |> Async.Parallel + let! results = + [ fetchDataAsync (); fetchDataAsync (); fetchDataAsync () ] + |> Async.Parallel return results |> Array.toList } @@ -398,7 +400,9 @@ When you need rich composition primitives: let complexWorkflow () = async { // Run three operations in parallel - let! results = [ fetchDataAsync (); fetchDataAsync (); fetchDataAsync () ] |> Async.Parallel + let! results = + [ fetchDataAsync (); fetchDataAsync (); fetchDataAsync () ] + |> Async.Parallel // Then do something sequential do! Async.Sleep 100 diff --git a/chapters/Bindings.fs b/chapters/Bindings.fs index 12abb02..251abdf 100644 --- a/chapters/Bindings.fs +++ b/chapters/Bindings.fs @@ -3,9 +3,13 @@ module Bindings (** # Creating Python Bindings -When a Python library doesn't have F# bindings, you can create your own. -This chapter covers the patterns and best practices for writing type-safe -bindings that feel natural in F#. +When a Python library doesn't have F# bindings, you can create your own. This chapter +covers the patterns and best practices for writing type-safe bindings that feel natural +in F#. + +> Writing bindings have long been a major pain point, spending countless hours wrestling +> with interop details. With AI-assisted coding tools, generating initial binding code +> for your favorite Python libraries has become much easier. ## Core Principles @@ -273,6 +277,5 @@ let createClient url = myLibrary.createClient url ## What's Next? -Now you know how to create bindings. The **Compatibility** chapter covers -which F# features work with Fable.Python and any limitations to be aware of. +With bindings covered, the **Compatibility** chapter shows which F# features work with Fable.Python and any limitations to be aware of. *) diff --git a/chapters/FastAPI.fs b/chapters/FastAPI.fs index 3ea8962..ea62714 100644 --- a/chapters/FastAPI.fs +++ b/chapters/FastAPI.fs @@ -97,27 +97,35 @@ let items = ResizeArray() type API() = /// GET /items - List all items [] - static member get_items() : ResizeArray = - items + static member get_items() : ResizeArray = items /// GET /items/{item_id} - Get item by ID [] - static member get_item(item_id: int) : Task = task { - match items |> Seq.tryFind (fun i -> i.Id = item_id) with - | Some item -> return item :> obj - | None -> return {| error = "Item not found" |} - } + static member get_item(item_id: int) : Task = + task { + match items |> Seq.tryFind (fun i -> i.Id = item_id) with + | Some item -> return item :> obj + | None -> return {| error = "Item not found" |} + } /// POST /items - Create a new item [] - static member create_item(request: CreateItemRequest) : Task = task { - let newId = - if items.Count = 0 then 1 - else (items |> Seq.map (fun i -> i.Id) |> Seq.max) + 1 - let newItem = Item(newId, request.Name, request.Price, request.InStock) - items.Add(newItem) - return {| status = "created"; item = newItem |} - } + static member create_item(request: CreateItemRequest) : Task = + task { + let newId = + if items.Count = 0 then + 1 + else + (items |> Seq.map (fun i -> i.Id) |> Seq.max) + 1 + + let newItem = Item(newId, request.Name, request.Price, request.InStock) + items.Add(newItem) + + return {| + status = "created" + item = newItem + |} + } (** This generates Python with proper FastAPI decorators: @@ -143,8 +151,10 @@ F# anonymous records compile to Python dictionaries, perfect for JSON responses: [] type HealthAPI() = [] - static member health() = - {| status = "healthy"; version = "1.0.0" |} + static member health() = {| + status = "healthy" + version = "1.0.0" + |} (** ## Async Endpoints @@ -155,11 +165,12 @@ For I/O-bound operations, use `task { }` to create async endpoints: [] type AsyncAPI() = [] - static member slow_operation() = task { - // Simulate async work (e.g., database query) - do! Task.Delay(100) - return {| message = "Done!" |} - } + static member slow_operation() = + task { + // Simulate async work (e.g., database query) + do! Task.Delay(100) + return {| message = "Done!" |} + } (** The `task { }` computation expression compiles to Python's `async def`, @@ -175,12 +186,16 @@ Path parameters are extracted from the URL: [] type UsersAPI() = [] - static member get_user(user_id: int) = - {| id = user_id; name = "User " + string user_id |} + static member get_user(user_id: int) = {| + id = user_id + name = "User " + string user_id + |} [] - static member get_user_post(user_id: int, post_id: int) = - {| user_id = user_id; post_id = post_id |} + static member get_user_post(user_id: int, post_id: int) = {| + user_id = user_id + post_id = post_id + |} (** ### Query Parameters @@ -191,8 +206,10 @@ Query parameters are function arguments not in the path: [] type SearchAPI() = [] - static member search(q: string, limit: int) = - {| query = q; limit = limit |} + static member search(q: string, limit: int) = {| + query = q + limit = limit + |} (** A request to `/search?q=hello&limit=10` maps to `search("hello", 10)`. @@ -213,7 +230,11 @@ type UserCrudAPI() = [] static member create_user(request: CreateUserRequest) = // FastAPI automatically validates the request body - {| status = "created"; name = request.name; email = request.email |} + {| + status = "created" + name = request.name + email = request.email + |} (** FastAPI validates the incoming JSON against the Pydantic model and returns @@ -230,8 +251,10 @@ type ErrorAPI() = static member protected_route() = // Check authentication (simplified example) let isAuthenticated = false + if not isAuthenticated then raise (System.Exception("Not authenticated")) + {| message = "Secret data" |} (** @@ -336,4 +359,51 @@ This combination gives you: 4. **Familiar ecosystem** - Deploy with standard Python tools You write type-safe F# code, but deploy and run it like any Python web service. + +## Hybrid Architecture: F# Backend with Python Endpoints + +Another compelling use case is when you have an existing web service written in F# (using +ASP.NET Core, Giraffe, or Oxpecker) but need access to the Python ecosystem for specific +functionality. You can use FastAPI to expose endpoints that leverage Python libraries, +while your main service remains in F#. + +This hybrid approach works well when you need: + +### AI/ML Libraries + +- **LangChain** / **LlamaIndex** - LLM orchestration and RAG pipelines +- **Hugging Face Transformers** - Pre-trained models for NLP, vision, audio +- **OpenAI SDK** / **Anthropic SDK** - LLM API integration with structured outputs +- **scikit-learn** - Classical machine learning models +- **PyTorch** / **TensorFlow** - Deep learning inference + +### Data Science & Analytics + +- **Pandas** / **Polars** - Data manipulation and analysis +- **NumPy** - Numerical computing +- **Matplotlib** / **Plotly** - Chart and visualization generation +- **Apache Arrow** - Efficient cross-language data interchange + +### Document Processing + +- **PyMuPDF** / **pdfplumber** - PDF text and table extraction +- **python-docx** - Word document generation +- **Pillow** - Image processing and manipulation +- **OpenCV** - Computer vision operations + +### Specialized APIs + +- **boto3** - AWS services (S3, Lambda, SQS, etc.) +- **google-cloud-*** - GCP services (BigQuery, Cloud Storage, Vertex AI) + +### Scientific Computing + +- **SciPy** - Scientific algorithms and optimization +- **SymPy** - Symbolic mathematics +- **NetworkX** - Graph algorithms and analysis + +The pattern is straightforward: your F# service handles core domain logic and type-safe +business rules, while specific endpoints delegate to a FastAPI service for capabilities +where Python dominates. This is especially powerful for AI/ML workloads where the Python +ecosystem is unmatched. *) diff --git a/chapters/GettingStarted.fs b/chapters/GettingStarted.fs index 4800abd..f3d4068 100644 --- a/chapters/GettingStarted.fs +++ b/chapters/GettingStarted.fs @@ -3,7 +3,7 @@ module GettingStarted (** # Getting Started with Fable.Python -Let's set up a Fable.Python project from scratch and get our first F# code running as Python. +In this section we will set up a Fable.Python project from scratch and get our first F# code running as Python. ## Prerequisites @@ -12,7 +12,8 @@ You'll need: - [.NET SDK](https://dotnet.microsoft.com/download) (6.0 or later. We recommend installing the latest LTS version, currently .NET 10) - [Python 3.12+](https://www.python.org/downloads/) (Fable targets Python 3.12 or higher) -- [uv](https://docs.astral.sh/uv/) (recommended) - A fast Python package manager written in Rust +- [uv](https://docs.astral.sh/uv/) (recommended) - A fast Python package manager written in Rust that + simplifies dependency management, virtual environments, and the installation of Python itself. If you don't have `uv` installed: @@ -60,8 +61,9 @@ pip install "fable-library==5.0.0a21" --- -**Note:** Version pinning matters. The fable-library version must match -your Fable compiler version. PyPI uses `5.0.0a21` format instead of `5.0.0-alpha.21`. +**Note:** Version pinning matters. The fable-library version must match your Fable +compiler version. Note that PyPI uses `5.0.0a21` format instead of `5.0.0-alpha.21` for +prerelease alpha releases. --- @@ -136,6 +138,6 @@ my-fable-python/ ## Next Steps -Now that you have a working setup, let's explore how to interact with Python -libraries in the next chapter on **Bindings**. +Now that you have a working setup, let's see how we can interact with Python +libraries by using **Bindings**. *) diff --git a/chapters/Interop.fs b/chapters/Interop.fs index ca19dfc..a99aaa2 100644 --- a/chapters/Interop.fs +++ b/chapters/Interop.fs @@ -3,8 +3,7 @@ module Interop (** # Python Interop -Now that you have a Fable.Python project set up, let's explore how to work -with Python libraries and the existing bindings in the Fable.Python ecosystem. +With a Fable.Python project set up, we can start to work with Python libraries and the existing bindings in the Fable.Python ecosystem. ## The Fable.Python Library @@ -427,7 +426,7 @@ let loadConfig (path: string) = (** ## What's Next? -Now you know how to use existing Python bindings and core interop features. -In the next chapter, we'll learn how to create your own bindings for -Python libraries that don't have F# bindings yet. +Now you know how to use existing Python bindings and core interop features. In the next +chapter we will see how you can create your own bindings for Python libraries that don't +have F# bindings yet. *) diff --git a/chapters/Introduction.fs b/chapters/Introduction.fs index dfff1e1..a51897f 100644 --- a/chapters/Introduction.fs +++ b/chapters/Introduction.fs @@ -7,7 +7,7 @@ module Introduction 2025](https://sergeytihon.com/2025/11/03/f-advent-calendar-in-english-2025/). Thank you, Sergey Tihon, for organizing this wonderful tradition that brings the F# community together every year! -Welcome to this guide on [Fable](https://fable.io/) and [Fable.Python](https://github.com/fable-compiler/Fable.Python/) - +This guide covers [Fable](https://fable.io/) and [Fable.Python](https://github.com/fable-compiler/Fable.Python/) - a compiler that transforms F# code into Python. ## Table of Contents @@ -21,7 +21,7 @@ a compiler that transforms F# code into Python. 7. [Testing](#heading-testing-fablepython-projects) - Using pytest with F# code 8. [Fable v5](#heading-fable-v5-whats-new) - New features and the Rust core 9. [Pydantic Integration](#heading-pydantic-interop) - Type-safe data validation -10. [FastAPI](#heading-fastapi) - Building type-safe web APIs +10. [FastAPI](#heading-fastapi) - Building type-safe web APIs in the Python ecosystem 11. [Units of Measure](#heading-units-of-measure) - Compile-time dimensional analysis 12. [Fable.Literate](#heading-fableliterate-the-strange-loop) - The tool that wrote this post @@ -42,9 +42,10 @@ F# is a functional-first language with powerful features like: - **Type inference** - Write less, express more - **Pattern matching** - Elegant handling of complex data - **Immutability by default** - Safer, more predictable code -- **Algebraic data types** - Model your domain precisely +- **Algebraic data types** - Model your domain precisely with discriminated unions and records -With Fable.Python, you get all these benefits while targeting the Python ecosystem. +These features make F# excellent for [Domain Modeling](https://www.pragprog.com/titles/swdddf/domain-modeling-made-functional/) - +expressing business rules as types that the compiler enforces. Python is currently [the most popular programming language in the world](https://www.tiobe.com/tiobe-index/). And no matter what you think of Python, it will always be the second best language for everything. That ubiquity is exactly why @@ -74,7 +75,7 @@ Fable.Python is a great choice when: ## A First Example -Let's start with F# code that compiles to Python: +Let's begin with a simple F# example: *) let greet (name: string) = $"Hello, {name}!" @@ -117,6 +118,6 @@ shape variant, the compiler will warn you about unhandled cases in the `area` fu ## What's Next? -In the following chapters, we'll explore setting up your environment, working with Python libraries, and understanding +In the following chapters, we will get started by setting up your environment, working with Python libraries, and understanding F# compatibility with Fable. Let's begin. *) diff --git a/chapters/Python.fs b/chapters/Python.fs index 7b70d90..e42af88 100644 --- a/chapters/Python.fs +++ b/chapters/Python.fs @@ -3,8 +3,7 @@ module ForPythonDevelopers (** # Are You a Python Developer? -If you're coming from Python, welcome. This chapter will help you understand the F# code you'll see throughout this -guide. F# is more approachable than it might appear, and many concepts are familiar. +If you're coming from Python, this chapter covers the F# code you'll see throughout this guide. F# is more approachable than it might appear, and many concepts are familiar. ## What is F#? @@ -16,7 +15,7 @@ is the language you know. ## Key Concepts You'll See -Let's map F# concepts to Python equivalents you already understand. +Here's how F# concepts map to Python equivalents you already know. ### Type Inference @@ -47,7 +46,7 @@ No need to write it unless you want to. ### Pattern Matching -Python 3.10+ has `match`/`case`. F# pattern matching is similar but more powerful: +Python 3.10+ has `match`/`case`: ```python # Python match/case @@ -59,6 +58,8 @@ match command: case _: return unknown_command() ``` + +F# pattern matching is similar but more powerful: *) let handleCommand command = @@ -159,7 +160,10 @@ let numbers = [ -1; 2; -3; 4; 5 ] // F# pipeline - reads left to right, top to bottom let result = - numbers |> List.filter (fun x -> x > 0) |> List.map (fun x -> x * 2) |> List.sum + numbers + |> List.filter (fun x -> x > 0) + |> List.map (fun x -> x * 2) + |> List.sum (** The `|>` operator takes the value on the left and passes it as the last diff --git a/chapters/Summary.fs b/chapters/Summary.fs index 3d9c834..48e82de 100644 --- a/chapters/Summary.fs +++ b/chapters/Summary.fs @@ -49,5 +49,5 @@ from the community make it better for everyone. - [Fable.Python on GitHub](https://github.com/fable-compiler/Fable.Python/) - [F# Software Foundation](https://fsharp.org/) -Welcome to Fable.Python. Now go build something. +... now go build something. *) diff --git a/chapters/Testing.fs b/chapters/Testing.fs index a004212..5f07fc1 100644 --- a/chapters/Testing.fs +++ b/chapters/Testing.fs @@ -235,6 +235,9 @@ project setup: ### Justfile Commands +We recommend [just](https://github.com/casey/just) as a command runner - it's like +`make` but simpler and cross-platform. Here's how to set up test commands: + ```just # Run tests (.NET) test: diff --git a/justfile b/justfile index ffc82af..abd8f86 100644 --- a/justfile +++ b/justfile @@ -59,6 +59,9 @@ generate: build format-python blogpost: build format-python #!/usr/bin/env bash mkdir -p docs + # Get Fable version (strip ANSI codes) and timestamp + fable_version=$(dotnet fable --version 2>/dev/null | sed 's/\x1b\[[0-9;]*m//g' || echo "unknown") + timestamp=$(date -u +"%Y-%m-%d %H:%M UTC") first=true for name in {{chapters}}; do # Convert PascalCase to snake_case for Python file naming @@ -75,6 +78,10 @@ blogpost: build format-python uv run python output/Fable.Literate/app.py \ --python-file "$pyfile" \ "chapters/${name}.fs" > docs/blogpost.md + # Insert version banner after the first heading using awk + awk -v ts="$timestamp" -v fv="$fable_version" \ + 'NR==1 {print; print ""; print "*Generated on " ts " using Fable v" fv "*"; next} {print}' \ + docs/blogpost.md > docs/blogpost.tmp && mv docs/blogpost.tmp docs/blogpost.md first=false else # Remaining chapters get headers increased by one level