Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
{
"cSpell.words": [
"coro",
"destructures",
"elif",
"eprintln",
"fabletext",
"Fantomas",
"fastapi",
"Feliz",
"getcwd",
"Hashnode",
"pathlib",
"pyname",
"stroustrup"
]
Expand Down
79 changes: 54 additions & 25 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

**F# Advent 2025 blog post project** demonstrating Fable.Python capabilities.

**Fabletext** is a literate programming converter (inspired by jupytext) written in F# that transpiles to Python via Fable.Python. It processes `.fs` files with embedded Markdown comments (FSharp.Formatting conventions) and outputs GitHub-flavored Markdown suitable for publishing on platforms like Hashnode.
**Fable.Literate** is a literate programming converter (inspired by jupytext) written in F# that transpiles to Python via Fable.Python. It processes `.fs` files with embedded Markdown comments (FSharp.Formatting conventions) and outputs GitHub-flavored Markdown suitable for publishing on platforms like Hashnode.

**Key concept:** The project is self-documenting - the chapters and converter generate the blog post that documents how they work.

Expand All @@ -19,57 +19,86 @@ just build # Build all chapters and tools to Python
just generate # Generate individual markdown docs from chapters
just blogpost # Generate concatenated blogpost.md for publishing
just format # Format Python with ruff
just lint # Lint Python (ruff) and Markdown (markdownlint)
just lint # Lint Markdown (markdownlint)
just watch # Watch mode for development
just clean # Clean generated files
just all # Full pipeline: restore, build, generate, format, lint
```

## Architecture

### Fabletext Parser State Machine
### Fable.Literate AST Pipeline

The converter uses a line-by-line state machine with three states:
The converter follows a compiler-like architecture with three phases:

- **InMarkdown**: Inside `(** ... *)` blocks - emit content as-is
- **InCode**: F# code outside comment blocks - wrap in fenced code blocks
- **Hidden**: After `(*** hide ***)` - skip until next markdown block
1. **Parse**: Convert source lines into a Block AST
2. **Transform**: Filter hidden blocks, resolve Python includes
3. **Print**: Render the AST as Markdown

### Input/Output Transformation
### Literate Directives

| Input Pattern | Output |
|--------------|--------|
| Directive | Purpose |
|-----------|---------|
| `(** content *)` | Raw markdown content |
| `(*** hide ***)` | Nothing (enters hidden mode) |
| `(*** hide ***)` | Hide following code from output |
| `(*** include-python: symbol ***)` | Include generated Python for symbol |
| Regular F# code | Wrapped in ```fsharp fenced blocks |

### Escaping F# in Headings

Use `` F`#` `` (backticks around `#`) in markdown headings to prevent markdownlint from interpreting it as ATX closed style:

```fsharp
(**
## F`#` Async Workflows
*)
```

### File Structure

```text
chapters/
├── 01-introduction.fs # What is Fable.Python, why use it
├── 02-getting-started.fs # Setup, first project, hello world
├── 03-bindings.fs # Python interop, type bindings
└── 04-compatibility.fs # F# features supported, limitations
tools/
├── fabletext.fs # Fabletext converter source (F#)
└── fabletext.fsproj
├── introduction.fs # What is Fable.Python, why use it
├── python.fs # F# concepts for Python developers
├── getting-started.fs # Setup, first project, hello world
├── interop.fs # Using existing Python libraries
├── bindings.fs # Creating Python bindings
├── compatibility.fs # F# features supported, limitations
├── async-programming.fs # async vs task, Python asyncio mapping
├── fable-v5.fs # Fable v5 features, Rust core, PyPI
├── pydantic.fs # Pydantic models, DTOs, validation
└── units-of-measure.fs # Compile-time dimensional analysis
Fable.Literate/
├── App.fs # Fable.Literate converter source (F#)
└── Fable.Literate.fsproj
output/
├── chapters/ # Generated Python from chapters
└── tools/
└── fabletext.py # Generated converter (Python)
├── chapters/ # Generated Python from chapters
└── Fable.Literate/
└── app.py # Generated converter (Python)
docs/
├── *.md # Individual chapter markdown
└── blogpost.md # Concatenated for Hashnode
├── *.md # Individual chapter markdown
└── blogpost.md # Concatenated for Hashnode
```

## Chapter Writing Guidelines

- Each chapter is a literate F# file with embedded markdown
- Use `(** ... *)` for markdown content
- Use `(*** hide ***)` to hide setup code (module declarations, imports)
- Use `(*** include-python: symbolName ***)` to show generated Python
- Tables are auto-formatted by markdownlint - don't fight it
- Keep code examples self-contained and buildable

## Fable.Python Considerations

- Use `Fable.Core` attributes where needed
- Use `Fable.Core` attributes (`[<Emit>]`, `[<Import>]`, etc.)
- Use `Fable.Python.Pydantic` for Pydantic interop
- Stick to Fable-compatible F# subset
- File I/O via Python interop (`[<Emit>]` with `open`, `read`, etc.)
- `task { }` compiles to native Python `async def` (Fable v5)
- `async { }` for multi-target code (Python, .NET, JS)

## Resources

- [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
60 changes: 37 additions & 23 deletions chapters/async-programming.fs
Original file line number Diff line number Diff line change
Expand Up @@ -126,24 +126,13 @@ The `task` computation expression in .NET creates *hot* tasks that start immedia
However, when compiled to Python via Fable, tasks become Python coroutines - which are
*cold* just like Python's native `async def` functions.

In Fable v5, tasks compile to Python's native `async def` syntax, enabling seamless
integration with frameworks like FastAPI.
A key improvement in Fable v5 is that `task { }` now compiles to Python's native
`async def` syntax. Previously, Fable generated regular functions returning `Awaitable[T]`,
which frameworks like FastAPI couldn't recognize as async endpoints.
*)

open System.Threading.Tasks

let fetchDataTask () =
task {
do! Task.Delay 1000
return "data from task"
}

(**
### Fable v5: Native Python Async

A key improvement in Fable v5 is that `task { }` now compiles to Python's `async def`:
*)

let processItemTask (item: string) =
task {
do! Task.Delay 100
Expand All @@ -152,15 +141,11 @@ let processItemTask (item: string) =

(**
This generates:
*)
(*** include-python: processItemTask ***)

```python
async def process_item_task(item: str) -> str:
await asyncio.sleep(0.1)
return item.upper()
```

Previously, Fable generated regular functions returning `Awaitable[T]`, which frameworks
like FastAPI couldn't recognize as async endpoints. Now the integration is seamless.
(**
Now frameworks like FastAPI can detect and handle these as proper async endpoints.

### Task vs Async: Key Differences

Expand Down Expand Up @@ -188,6 +173,12 @@ like FastAPI couldn't recognize as async endpoints. Now the integration is seaml
### Working with Tasks
*)

let fetchDataTask () =
task {
do! Task.Delay 100 // Do some async work
return "data from task"
}

let taskExample () =
task {
let! result = fetchDataTask ()
Expand Down Expand Up @@ -243,6 +234,30 @@ In Python, this generates:

(*** include-python: simpleTask ***)

(**
### Running Tasks from F`#`

To run a task and get its result in F#:
*)

let runTaskExample () =
let tsk = simpleTask ()

// Block and wait for result
let result = tsk.GetAwaiter().GetResult()
printfn $"Got: {result}"

(**
You can also await tasks inside other tasks:
*)

let chainedTasks () =
task {
let! first = simpleTask ()
let! second = simpleTask ()
return first + second
}

(**
### Running in Python's Event Loop

Expand All @@ -251,7 +266,6 @@ When your compiled Python code runs, you'll need an event loop. For scripts:
```python
import asyncio

# If using task-based code
async def main():
result = await simple_task()
print(result)
Expand Down
22 changes: 20 additions & 2 deletions chapters/fable-v5.fs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ module FableV5
- **Fixed-size arrays** - No more Python list quirks for byte streams
- **Reliable numerics** - Fable 4's pure Python numerics were a constant source of bugs

While Rust is fast, don't expect dramatic speedups for typical F# code. Many F#
functions are higher-order and callback to Python - `List.map`, `List.filter`,
`Seq.fold`, etc. all invoke your Python lambdas. The Rust core handles the
data structures correctly; your code still runs at Python speed.

## fable-library via PyPI

Before Fable v5, the runtime was bundled in the NuGet package and copied
Expand All @@ -58,6 +63,19 @@ pip install fable-library
uv add fable-library
```

For projects, pin your dependencies in `pyproject.toml`. For stable releases use
a minimum version constraint:

```toml
dependencies = ["fable-library>=5.0.0"]
```

For alpha/beta releases, pin the exact version to avoid surprises:

```toml
dependencies = ["fable-library==5.0.0a21"]
```

This makes dependency management much simpler and follows Python conventions.

## Test Coverage
Expand All @@ -78,10 +96,10 @@ To use Fable v5, install the alpha CLI:

```bash
# Install Fable 5 CLI
dotnet tool install fable --version 5.0.0-alpha.17
dotnet tool install fable --version 5.0.0-alpha.21

# Add Fable.Core to your project
dotnet add package Fable.Core --version 5.0.0-beta.2
dotnet add package Fable.Core --version 5.0.0-beta.4

# Install the Python runtime
uv add fable-library==5.0.0a17
Expand Down
3 changes: 2 additions & 1 deletion chapters/getting-started.fs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ running as Python.

You'll need:

- [.NET SDK](https://dotnet.microsoft.com/download) (6.0 or later)
- [.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)

## Project Setup
Expand Down
4 changes: 4 additions & 0 deletions chapters/introduction.fs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ F# is a functional-first language with powerful features like:

With Fable.Python, you get all these benefits while targeting the Python ecosystem.

Python is the [most popular programming language](https://www.tiobe.com/tiobe-index/)
in the world. And no matter what you think of Python, it will always be the second
best language for everything. That ubiquity is exactly why Fable.Python exists.

## When to Use Fable.Python

Fable.Python is a great choice when:
Expand Down
Loading