I would like to know if it is bad form to do something like this:
data Alignment = LeftAl | CenterAl | RightAl
type Delimiter = Char
type Width = Int
setW :: Width -> Alignment -> Delimiter -> String -> String
Rather than something like this:
setW :: Int -> Char -> Char -> String -> String
I do know that remaking those types effectively does nothing but take up a few lines in exchange for clearer code. However, if I use the type Delimiter
for multiple functions, this would be much clearer to someone importing this module, or reading the code later.
I am relatively new to Haskell so I do not know what is good practice for this type of stuff. If this is not a good idea, or there is something that would improve clarity that is preferred, what would that be?
Line charts are the most effective chart for displaying time-series data. They can handle a ton of data points and multiple data series, and everyone knows how to read them.
Scatter plots are best for showing distribution in large data sets.
If you have long category names, it is best to use bar charts because they give more space for long text. You should also use bar charts, instead of column charts, when the number of categories is greater than seven (but not more than fifteen) or for displaying a set with negative numbers.
The best position for validationInstant validation is best positioned to the right hand side of the input, or failing that immediately below. This works best in principle for creating the conversation between user and for that manages to gamify the often tedious process of form inputs.
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.
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).
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 like foldMap
.
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.
One important thing to note is that Alignment
versus Char
is not just a matter of clarity, but one of correctness. Your Alignment
type expresses the fact that there are only three valid alignments, as opposed to however many inhabitants Char
has. By using it, you avoid trouble with invalid values and operations, and also enable GHC to informatively tell you about incomplete pattern matches if warnings are turned on.
As for the synonyms, opinions vary. Personally, I feel type
synonyms for small types like Int
can increase cognitive load, by making you track different names for what is rigorously the same thing. That said, leftaroundabout makes a great point in that this kind of synonym can be useful in the early stages of prototyping a solution, when you don't necessarily want to worry about the details of the concrete representation you are going to adopt for your domain objects.
(It is worth mentioning that the remarks here about type
largely don't apply to newtype
. The use cases are different, though: while type
merely introduces a different name for the same thing, newtype
introduces a different thing by fiat. That can be a surprisingly powerful move -- see danidiaz's answer for further discussion.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With