I think the convention is perfectly fine. Itâs confusing when youâre not quite used to the idea record accessors are âjust functionsâ, and thinking of them as such, but this idea is essential to writing idiomatic Haskell. Just look at GADT syntax, which highlights this even more:
data Writer w a where
runWriter :: Writer w a -> (a, w)
Using this notation, we are conceptually no longer saying "Writer
is a data type, parametrized over two types w
and a
, that has one field called runWriter
, whose type is (a, w)
"; we are saying "there exists a type Writer
, parametrized over two types w
and a
, for which a function runWriter
exists, whose type is Writer w a -> (a, w)
". Itâs literally the same thing, the difference is purely in the surface syntax, but the viewpoint is a different one.
This idea of data types and functions being the same thing, or at least isomorphic, permeates Haskell, and many common idioms can only be fully understood if you have developed an intuition for this. Is a list a data structure or a control construct? Yes! Is Functor an abstraction of data structures or control flow? Also yes! Whatâs up with the Data
vs. Control
namespaces? Nothing but historical cruft, really! Or look at DLists, which literally represent lists as functions. Or defunctionalization as a programming technique.
So while writing runWriter
as a âfieldâ may be confusing to a beginner, that confusion cannot be avoided entirely, and IMO that makes it a weak argument against using it here.