Stop Handle Leaks with Haskell Linear Types
GHC LinearTypes lets the type checker enforce single-use resource handling at compile time, with no runtime overhead.
26 Dec 2025, 00:52 UTC

The problem: a handle that outlives its welcome
You open a file handle, pass it through helper functions, and forget to close it. Or you close it twice and crash at runtime. In ordinary Haskell, a Handle is just a value: the type checker lets you copy it, discard it, or pass it to ten different functions. Resource discipline lives in documentation and code review, not in the compiler.
The useful takeaway is that GHC's LinearTypes extension lets the type checker enforce single-use semantics at compile time. A value passed through a linear arrow must be consumed exactly once, so handle leaks and double-closes become type errors rather than runtime surprises. There is no runtime overhead because linearity is erased after type checking.
What linearity means in GHC
LinearTypes is available in GHC 9.0 and later. It introduces a Multiplicity kind with two constructors: One and Many. A linear function arrow, written a %1 -> b, means the argument is used exactly once. An unrestricted arrow, written a -> b or a %p -> b, means the argument may be used zero, one, or many times. The %1 annotation is syntactic sugar over GHC's existing kind system.
Worked example: a linear file handle
First verify your toolchain. Run ghc --version in your terminal; you need GHC 9.0 or newer. Older releases such as GHC 8.10 will reject -XLinearTypes. No special permissions are required.
Add these options to your Cabal file in the project directory:
ghc-options: -XLinearTypes -Wlinear-unused -Wlinear-used
-Wlinear-unused warns when a linear value is discarded. -Wlinear-used warns when it is used more than once. Treat both as errors during adoption.
Here is a minimal wrapper around a standard Handle:
{-# LANGUAGE LinearTypes #-}
import System.IO (Handle, IOMode, hClose, openFile)
import Prelude.Linear ((%1))
newtype LinearHandle = LinearHandle Handle
closeLinear :: LinearHandle %1 -> IO ()
closeLinear (LinearHandle h) = hClose h
withLinearFile :: FilePath -> IOMode -> (LinearHandle %1 -> IO a) %1 -> IO a
withLinearFile path mode action = do
h <- openFile path mode
action (LinearHandle h)
The callback action is linear, so it must consume LinearHandle exactly once. If the callback ignores its argument, -Wlinear-unused reports it. If the callback closes the handle and then tries to use it again, the compiler rejects the program. Because openFile returns an unrestricted Handle, wrapping it in LinearHandle shifts the burden of closing from convention to the type system.
You can verify the behavior in GHCi. Start ghci, run :set -XLinearTypes, and ask for the type of (\x %1 -> x). The inferred type should show a linear arrow, not an ordinary function arrow.
Trade-offs and limits
Linearity composes poorly with standard higher-order functions. map, foldr, and friends expect unrestricted functions, so you need linear variants such as lmap or an explicit Ur unrestricted wrapper. That adds boilerplate in polymorphic code.
Linearity is erased after type checking, so it cannot enforce protocols across process boundaries or FFI calls that duplicate pointers. You must trust the FFI boundary, or wrap foreign resources with affine or unrestricted types at the edge.
The linear base modules (System.IO.Linear, Control.Monad.Linear) are still experimental in base 4.18 and later, so APIs may shift between GHC releases. Profiling builds can duplicate linear values for cost-centre stacks, which may trigger false-positive linearity warnings. Some GHC plugins also interact unpredictably with multiplicity changes; test plugin compatibility in CI before rolling the extension out to a large codebase.
Practical adoption path
- Start with one module. Wrap a database connection, temporary file, or socket in a linear newtype.
- Enable
LinearTypeswith-Wlinear-unusedand-Wlinear-used. - Confirm compilation fails when a linear resource is used twice or not at all.
- Expand to linear state threads (
Control.Monad.ST.Linear) for mutable arrays only after the wrapper pattern is stable.
Linear types do not replace careful resource management, but they move the most common handle mistakes from testing and production into compilation. Check your compiler version, enable the warnings, and let GHC enforce single-use where it matters.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.