Is it bad form to make new types/datas for clarity?
I wouldn't consider that bad form, but clearly, I don't speak for the Haskell community at large. The language feature exists, as far as I can tell, for that particular purpose: to make the code easier to read.
One can find examples of the use of type aliases in various 'core' libraries. For example, the Read
class defines this method:
readList :: ReadS [a]
The ReadS
type is just a type alias
type ReadS a = String -> [(a, String)]
Another example is the Forest
type in Data.Tree
:
type Forest a = [Tree a]
As Shersh points out, you can also wrap new types in newtype
declarations. That's often useful if you need to somehow constrain the original type in some way (e.g. with smart constructors) or if you want to add functionality to a type without creating orphan instances (a typical example is to define QuickCheck Arbitrary
instances to types that don't otherwise come with such an instance).
You're using type aliases, they only slightly help with code readability. However, it's better to use newtype
instead of type
for better type-safety. Like this:
data Alignment = LeftAl | CenterAl | RightAl
newtype Delimiter = Delimiter { unDelimiter :: Char }
newtype Width = Width { unWidth :: Int }
setW :: Width -> Alignment -> Delimiter -> String -> String
You will deal with extra wrapping and unwrapping of newtype
. But the code will be more robust against further refactorings. This style guide suggests to use type
only for specializing polymorphic types.
Using newtype
—which creates a new type with the same representation as the underlying type but not substitutable with it— is considered good form. It's a cheap way to avoid primitive obsession, and it's especially useful for Haskell because in Haskell the names of function arguments are not visible in the signature.
Newtypes can also be a place on which to hang useful typeclass instances.
Given that newtypes are ubiquitous in Haskell, over time the language has gained some tools and idioms to manipulate them:
Coercible A "magical" typeclass that simplifies conversions between newtypes and their underlying types, when the newtype constructor is in scope. Often useful to avoid boilerplate in function implementations.
ghci> coerce (Sum (5::Int)) :: Int
ghci> coerce [Sum (5::Int)] :: [Int]
ghci> coerce ((+) :: Int -> Int -> Int) :: Identity Int -> Identity Int -> Identity Int
ala
. An idiom (implemented in various packages) that simplifies the selection of a newtype that we might want to use with functions likefoldMap
.ala Sum foldMap [1,2,3,4 :: Int] :: Int
GeneralizedNewtypeDeriving
. An extension for auto-deriving instances for your newtype based on instances available in the underlying type.DerivingVia
A more general extension, for auto-deriving instances for your newtype based on instances available in some other newtype with the same underlying type.