F#

There’s something strange about learning a programming language you may never use professionally. When I tell people I’m learning F#, the responses are almost predictable:

Are you switching careers?

Is your team moving to .NET?

No. I still work in population and behavioral health research as a data scientist and numerical programmer, where Python dominates and Rust handles the performance-critical parts of our pipeline. F# is unlikely to appear in production systems at my job.

So why invest in it?

Because I’m trying to learn functional programming deeply enough that it changes how I think in the languages I do use. And, quietly, I’m also wondering whether F# might be a hidden gem for my personal ML, NLP, and agentic AI projects — a potential alternative to the Python-first default.

This is the story of how I landed here, why F# specifically, and what I hope it will teach me.


Rediscovering FP: From Haskell’s Abstraction to Rust’s Pragmatism

In my final undergraduate year, I fell in love with Haskell. Coming from imperative languages, the elegance of pure functions, lazy evaluation, and type classes was almost intoxicating. I worked through A Gentle Introduction to Haskell and convinced myself I’d glimpsed some higher programming truth.

Then I abandoned it — or more accurately, Haskell abandoned me.

I was learning in a vacuum. No real problems to solve, no domain grounding. Concepts like monads were intellectually fascinating but practically opaque. And soon after, my career shifted away from programming altogether. Years passed.

When I eventually returned to programming professionally, it was through data science. And unexpectedly, my path back to FP came through Rust.

Rust doesn’t market itself as “functional programming,” yet immutability, pattern matching, algebraic data types, and expression-oriented syntax are everywhere. I learned FP concepts indirectly because they were integral to Rust’s ergonomics and safety guarantees.

Rust taught me something Haskell didn’t: Functional programming is a paradigm, not a purity test. You don’t need a purely functional language to benefit from FP thinking.

With Rust, I finally had context — real problems to solve, real constraints, real benefits when FP tools fit naturally. FP went from abstract theory to practical utility.

That raised a question I’d never seriously asked before: What would an FP-first language feel like if it didn’t carry Rust’s memory-management complexity?


When Python Stops Helping and Starts Fighting

Armed with FP ideas from Rust, I started writing Python differently:

  • favoring pure functions
  • using immutable dataclasses
  • modeling states explicitly
  • composing transformations rather than mutating objects

It worked — to a point.

Python let me express ideas concisely, but it never protected me. Type hints documented intent but didn’t enforce it. I’d annotate a function as returning float, but somewhere deep in the call stack it could return None and blow up 45 minutes into a model run.

To approximate FP error handling, I ended up writing verbose pipelines:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def process_pipeline(data: DataFrame) -> Optional[Result]:
    cleaned = clean_data(data)
    if cleaned is None:
        return None
    
    validated = validate_schema(cleaned)
    if validated is None:
        return None
    
    transformed = transform_features(validated)
    if transformed is None:
        return None
    
    return compute_result(transformed)

Readable? Sure. Composable? Barely. Safe? Not really. Scalable? Not at all.

I was thinking in a language Python wasn’t designed for.


The Trilemma: Readable, Concise, Safe — Choose Two

As a data scientist implementing statistical models, I increasingly need three properties at the same time:

  1. Self-explanatory code — The code should mirror the underlying math.
  2. Conciseness — Research requires rapid iteration; verbosity kills momentum.
  3. Upfront guarantees — Dimensionality, state, invariants, and domain constraints should be validated before runtime.

Most languages force a tradeoff:

  • Python: Readable + concise — but unsafe.
  • Rust: Safe + readable (once learned) — but high cognitive overhead.
  • Haskell: Safe + concise — but impractical for my domain and ecosystem.

I kept asking: Is there a language that gives me all three, without paying Rust’s memory-management cost or Haskell’s conceptual overhead?

That question led me to F#.


F# as the Sweet Spot

F# sits in a unique space:

  • static type safety
  • concise expression-oriented syntax
  • powerful type inference
  • discriminated unions for domain modeling
  • natural function composition
  • lightweight ergonomics
  • automatic memory management (no lifetimes!)

A variance estimation pipeline in F# looks almost like mathematical notation:

1
2
3
4
5
6
let estimateVariance (data: Sample) (weights: Weights) : Variance =
    data
    |> centerByWeightedMean weights
    |> squareDeviations
    |> weightedSum weights
    |> divideBy (effectiveSampleSize weights)

The types tell the story. The pipeline reads as a sequence of transformations. Errors are caught at compile time.

Compare this to Python:

1
2
3
4
5
def estimate_variance(data, weights):
    centered = center_by_weighted_mean(data, weights)  # maybe None?
    squared = square_deviations(centered)
    weighted = weighted_sum(squared, weights)
    return weighted / effective_sample_size(weights)

Or Rust:

1
2
3
4
5
6
7
8
9
fn estimate_variance<'a>(
    data: &'a Sample,
    weights: &'a Weights,
) -> Result<Variance, EstimationError> {
    let centered = center_by_weighted_mean(data, weights)?;
    let squared = square_deviations(&centered)?;
    let weighted = weighted_sum(&squared, weights)?;
    Ok(weighted / effective_sample_size(weights)?)
}

Rust is powerful, but every function signature reminds you that you are managing memory. Python is fast to write, but correctness is an ongoing gamble.

F# gives me executable mathematics: code that reflects the underlying reasoning, backed by a type system that eliminates entire categories of runtime bugs.


The Hidden Gem Argument

Python dominates ML and data science for good reasons:

  • huge ecosystem
  • Jupyter notebooks
  • low barrier to entry
  • strong network effects

But for solo or small-team projects, where you control the stack end-to-end, the calculus changes.

Why F# is compelling for prototyping and algorithm design

  • Types catch conceptual errors before runtime
  • Pipelines express mathematical transformations cleanly
  • Railway-oriented error handling eliminates boilerplate
  • Refactoring becomes low-risk

When a simulation takes 30 minutes to run, it’s no longer charming that Python lets you “just try it.”

Why F# is compelling for agentic AI systems

Agent systems have subtle failure modes. Bugs are expensive — both financially (API costs) and in correctness (hard-to-debug nondeterministic behavior).

A strongly typed domain model helps enormously:

1
2
3
4
5
6
type AgentState =
    | Idle
    | Planning of Task
    | ExecutingTool of ToolCall
    | AwaitingLLMResponse of Context
    | Failed of Error

Python, by contrast, leaves state transitions implicit and error-prone.

Why F# bridges prototyping and production

F# runs on .NET. That matters.

You can prototype with the speed of a high-level FP language, yet deploy performant, maintainable code without rewriting your core logic in a “real” language.

It’s not that F# beats Python in ecosystem — it doesn’t. It’s that F# beats Python in correctness and beats Rust in ergonomics, for domains where memory safety isn’t the bottleneck.


Tooling That Accelerates Learning

One of the biggest surprises has been tooling.

I use:

  • uv, ruff, and pyright in Python
  • cargo and rust-analyzer in Rust

Both ecosystems are considered excellent.

F# with Ionide is at least on par — and in some ways better:

  • more responsive type inference
  • more accurate static analysis
  • fewer false positives
  • faster feedback loops
  • extremely reliable refactoring
  • REPL + notebooks (a luxury Rust does not have)

Rust’s analysis tools (e.g., rust-analyzer) must reason about lifetimes, trait bounds, and macro expansion; F# tools do not. Ionide benefits from the simplicity of F#’s type system, and you feel it immediately.

This makes FP concepts click faster. The editor becomes a teacher rather than an obstacle.


Why F# Over Haskell or OCaml?

I seriously considered both.

Haskell

Beautiful, pure, academically rich — but:

  • tooling still inconsistent at the edges
  • purity demands full commitment
  • ecosystem for ML/data is thinner
  • I already tried and failed to adopt it without domain context

OCaml

Elegant and fast — but:

  • smaller community
  • tooling not as polished as the F# + Ionide stack
  • no .NET ecosystem
  • more friction for practical ML/NLP work

F#

The right blend of:

  • real FP concepts
  • approachable syntax
  • powerful types
  • strong inference
  • practical runtime
  • excellent tooling
  • compatibility with my prior C# knowledge

F# isn’t the purest FP language. But purity isn’t my goal. Transferable understanding is.


What F# Is Already Teaching Me

Early on, a few patterns validated the decision.

1. Domain modeling becomes natural

Discriminated unions make the domain visible in your type signatures. You end up modeling the problem space rather than encoding it ad hoc.

2. Type inference enhances readability

Rust’s signatures are correct but heavy. F#’s type inference preserves conciseness without sacrificing clarity.

3. Ergonomics encourage flow

The pipeline operator, indentation-sensitive syntax, and lightweight declarations make code feel like a sketch of your reasoning.

4. “If Ionide says it compiles, it probably works”

F# eliminates many runtime error classes outright. This is enormous for research code.

5. Computation expressions make monads intuitive

F#’s computation expressions finally made monads click for me. They feel like sequencing operations with context — not abstract algebra.

These early signals matter: F# is teaching FP in a way that Rust’s pragmatism and Haskell’s purity never did.


Putting It Into Practice: Advent of Code 2025

To go beyond theory, I’m committing to doing Advent of Code 2025 entirely in F#. I completed the last two years in Rust, so this will be a direct comparison of how the languages feel for algorithm-heavy daily problem-solving.

AoC is a perfect crucible:

  • daily exposure to core features
  • varied problem domains
  • immediate correctness feedback
  • organic progression from basic to advanced patterns

I’ll be documenting what emerges — strengths, weaknesses, ergonomic patterns, and how F# affects my thinking.


Where This Leads

There are several possible futures, all positive:

  • Best case: F# becomes my primary language for personal ML/NLP/AI projects.
  • Good case: F# prepares me to revisit Haskell with clarity that was missing years ago.
  • Acceptable case: F# simply makes me a better Python and Rust programmer.

All scenarios justify the investment. Because ultimately, this isn’t about switching languages — it’s about deepening a paradigm.


Why Now?

Because I finally have:

  • context — real problems where FP helps
  • experience — enough Python/Rust to see conceptual gaps
  • maturity — learning for understanding, not résumé lines
  • tooling — F#’s ecosystem is ready
  • clarity — I’m not trying to replace anything; I’m trying to think better

F# wins not by being the best FP language in isolation, but by being the language that best fits my niche: a researcher-engineer who values clarity, safety, correctness, and flow.


The Bet

F# is not the most popular language. It’s not the purest. It doesn’t have the biggest ML ecosystem. But it may be the best language for learning FP deeply while still building practical, maintainable projects.

It offers:

  • code that reads like math
  • types that prevent conceptual errors
  • ergonomics that help me stay in flow
  • tooling that supports exploration
  • paradigms I can transfer back to Python and Rust

Maybe it won’t replace Python for everything I do. Maybe it won’t become my primary language. Maybe the ecosystem gaps will matter more over time.

But even if none of that pans out, I will think differently — and write better Python and Rust — because I learned F#.

That’s a bet worth making.