Appendix A.7: Linear Typed Arrays¶
Source: Data/Array/Linear.hs, HsLinearDemo.hs
GHC's -XLinearTypes extension can enforce exclusive ownership
of mutable array regions at compile time. The design is inspired by
konn/linear-extra's Borrowable
SArray, which uses phantom-tagged tokens for zero-copy split/combine. A
self-contained ~200-line module (Data.Array.Linear) extracts the core pattern
and integrates it with the unboxed primops from
Appendix A.6
(readDoubleArray#/writeDoubleArray# instead of Storable).
The core idea: linearity is on tokens (RW s), not the array itself. You
need the right token to read/write, and split/combine tracks disjoint
ownership at the type level.
Design¶
-- Linear token: proves exclusive access to region s
data RW s where MkRW :: RW s
-- Array with phantom region (NOT linear — tokens enforce access)
data DArray s = DArray !Int !(MutableByteArray# RealWorld) !Int -- len, buf, offset
-- Split witness: proves l and r came from splitting s
data SlicesTo s l r where MkSlicesTo :: SlicesTo s l r
-- Operations consume and return tokens
unsafeRead :: RW s %1 -> DArray s -> Int -> (Double, RW s)
unsafeWrite :: RW s %1 -> DArray s -> Int -> Double -> RW s
split :: RW s %1 -> Int -> DArray s -> Slice s
combine :: SlicesTo s l r %1 -> RW l %1 -> RW r %1 -> RW s
The split operation is zero-copy — both halves share the same underlying
MutableByteArray#, just with different offset/length views. No allocation,
no copying, just arithmetic on the offset field.
Type-Safe Row-Partitioned DGEMM¶
case split rwC (half * n) arrC of
MkSlice st rwTop rwBot cTop cBot ->
let rwTop' = linearDgemm rwTop n 0 half arrA arrB cTop
rwBot' = linearDgemm rwBot n half half arrA arrB cBot
rwC' = combine st rwTop' rwBot'
in unsafeRead rwC' arrC 0 -- can only read after recombining
This is the Haskell-side type encoding of what OpenMP does at runtime with
#pragma omp parallel for schedule(static) — partitioning the output matrix
into disjoint row blocks. The type system statically guarantees:
- No two computations can write to the same rows
- The original array cannot be accessed while split
- All slices must be recombined before reading results
C FFI Integration¶
The unsafeWithPtr function passes pinned ByteArray data directly to C:
This threads through IO properly (unlike runRW#-based approaches that
create independent state threads), ensuring C side effects are visible to
subsequent Haskell reads.
Implementation Notes¶
unsafeDupablePerformIOoverrunRW#: Element access usesunsafeDupablePerformIOwith{-# NOINLINE #-}to ensure writes from one operation are visible to subsequent reads.runRW#creates independent state threads that GHC can reorder under-O2.- Bang patterns for sequencing: In lazy contexts, token-producing
operations must be forced with
!to ensure their side effects execute. parCombinefor GHC sparks: A parallel variant ofcombinethat sparks the left token for parallel evaluation usingspark#/seq#. UsesunsafePerformIO(which includesnoDuplicate#) to prevent thunk duplication — essential when tokens are produced by destructive array operations.unsafeCoerce#bridges linear types with non-linear GHC primitives (same approach askonn/linear-extra'sUnsafe.toLinear).- Self-contained: ~280 lines, no dependencies beyond
baseandghc-prim.