Jul 22, 2025
·
reflections-on-haskell-and-rust
Reflections on Haskell and Rust

Sibi Prabakaran
Introduction
For most of my professional experience, I have been writing production code in both Haskell and Rust, primarily focusing on web services, APIs, and HTTP stack development. My journey started with Haskell, followed by working with Rust, and most recently returning to the Haskell ecosystem.
This experience has given me perspective on both languages' strengths and limitations in real-world applications. Each language has aspects that I appreciate and miss when working with the other. This post examines the features and characteristics that stand out to me in each language.
Variable shadowing
Rust's ability to shadow variables seamlessly is something I came to appreciate. In Rust, you can write:
This pattern is common and encouraged in Rust, making code more readable by avoiding the need for intermediate variable names. In Haskell, you would typically need different names:
Haskell's approach is slightly harder to read, while Rust's shadowing makes transformation pipelines more natural.
Sum types of records
Rust's enum system, particularly when combined with pattern matching, feels more robust than Haskell's sum types of records. When defining sum types of records in Haskell, there is a possibility of introducing partial record accessors which can cause runtime crashes, though recent versions of GHC now produce compile-time warnings for this pattern:
Rust eliminates this class of errors by design:
Enum variant namespacing
Rust allows multiple enum types to have the same variant names within the same module, while Haskell's constructor names must be unique within their scope. This leads to different patterns in the two languages.
In Rust, you can define:
The same approach in Haskell would cause a compile error:
In Haskell, you need unique constructor names:
Alternatively, Haskell developers often use qualified imports syntax to achieve similar namespacing:
The Typename::Variant
syntax in Rust makes the intent clearer at the usage site. You immediately know which enum type you're working with, while Haskell's approach can sometimes require additional context or prefixing to achieve the same clarity.
Struct field visibility
Rust provides granular visibility control for struct fields, allowing you to expose only specific fields while keeping others private. This fine-grained control is built into the language:
Rust offers even more granular visibility control beyond simple pub
and private fields. You can specify exactly where a field should be accessible:
These granular visibility modifiers (pub(crate)
, pub(super)
, pub(in path)
) allow you to create sophisticated access patterns that match your module hierarchy and architectural boundaries. Some of these patterns are simply not possible to replicate in Haskell's module system.
In Haskell, record field visibility is controlled at the type level, not the field level. This means you typically either export all of a record's fields at once (using ..
) or none of them. To achieve similar granular control, you need to use more awkward patterns:
The Haskell approach requires writing boilerplate accessor functions and losing the convenient record syntax for the fields you want to keep private. Rust's per-field visibility eliminates this awkwardness while maintaining the benefits of direct field access for public fields.
Purity and Referential Transparency
One of Haskell's most significant strengths is its commitment to purity. Pure functions, which have no side effects, are easier to reason about, test, and debug. Referential transparency—the principle that a function call can be replaced by its resulting value without changing the program's behavior—is a direct benefit of this purity.
In Haskell, the type system explicitly tracks effects through monads like IO
, making it clear which parts of the code interact with the outside world. This separation of pure and impure code is a powerful tool for building reliable software.
While Rust encourages a similar separation of concerns, it does not enforce it at the language level in the same way. A function in Rust can perform I/O or mutate state without any explicit indication in its type signature (beyond &mut
for mutable borrows). This means that while you can write pure functions in Rust, the language doesn't provide the same strong guarantees as Haskell.
This lack of enforced purity in Rust means you lose some of the strong reasoning and refactoring guarantees that are a hallmark of Haskell development.
Error handling
Rust's explicit error handling through Result<T, E>
removes the cognitive overhead of exceptions. Compare these approaches:
Haskell (with potential exceptions):
Rust (explicit throughout):
In Rust, the ?
operator makes error propagation clean while keeping the flow clear.
Unit tests as part of source code
Rust's built-in support for unit tests within the same file as the code being tested is convenient:
In Haskell, tests are typically in separate files:
Rust's co-location makes tests harder to forget and easier to maintain. Additionally, in Haskell you often need to export internal types and functions from your modules just to make them accessible for testing, which can pollute your public API.
Rust's #[cfg(test)]
attribute means test code has access to private functions and types without exposing them publicly.
Standard formatting
Rust's rustfmt
provides a standard formatting tool that the entire community has adopted:
In Haskell, while we have excellent tools like fourmolu
and ormolu
, the lack of a single standard has led to configuration debates:
I have witnessed significant time spent on style discussions when team members are reluctant to adopt formatting tools.
Language server support
While Haskell Language Server (HLS) has improved significantly, it still struggles with larger projects. Basic functionality like variable renaming can fail in certain scenarios, particularly in Template Haskell heavy codebases.
rust-analyzer provides a more reliable experience across different project sizes, with features like "go to definition" working consistently even in large monorepos. One feature I'm particularly fond of is rust-analyzer's ability to jump into the definitions of standard library functions and external dependencies. This seamless navigation into library code is something I miss when using HLS, even on smaller projects, where such functionality is not currently possible.
Another feature I extensively use in rust-analyzer is the ability to run tests inline directly from the editor. This functionality is currently missing in HLS, though there is an open issue tracking this feature request.
Compilation time
Despite Rust's reputation for slow compilation, I've found it consistently faster than Haskell for equivalent services. The Rust team has made significant efforts to optimize the compiler over the years, and these improvements are noticeable in practice. In contrast, Haskell compilation times have remained slow, and newer GHC versions unfortunately don't seem to provide meaningful improvements in this area.
Interactive development experience
I appreciate Haskell's REPL (Read-eval-print loop) for rapid prototyping and experimentation. Not having a native REPL in Rust noticeably slows down development when you need to try things out quickly or explore library APIs interactively. In GHCi, you can load your existing codebase and experiment around it, making it easy to test functions, try different inputs, and explore how your code behaves.
As an alternative, I have been using org babel in rustic mode for interactive Rust development. While this provides some level of interactivity within Emacs, it feels more like a band-aid than an actual solution for quickly experimenting with code. The workflow is more cumbersome compared to the direct approach of typing expressions directly into GHCi and seeing results instantly.