Best Practices
The following are some best practices that I have arrived at through using TTC in projects of various sizes over the past few years. Note that these are best practices for released projects as well as production systems, not for throwaway prototypes where many corners may be cut to quickly explore a design space.
Render
and
Parse
Usage
Render
and Parse
instances are most
appropriate when there is a canonical textual representation of a type.
For example, a database identifier string is only expressed in one way.
A Render
instance makes it easy to render the identifier in
the many Textual
types, and a Parse
instance
makes it easy to parse a value from a Textual
type, with
validation.
Do not feel that you need to declare Render
and
Parse
instances for everything. These type classes are
usually not a good match for types that can be formatted in many
different ways. In this case, implementing a formatting function is
almost always a better choice. These type classes are also not a good
match for rendering complex data. I do not recommend implementing
serialization, CSV, JSON, or XML functionality using Render
and Parse
instances, for example.
Shared Libraries
When implementing a shared library, defining Render
and
Parse
instances for defined types can be convenient to
users of the library. I recommend implementing them as regular functions
and exporting those functions so that users can choose to use the TTC
interface or not.
Defining orphan instances of core data types (such as
Int
) should be strictly avoided. Doing so would prevent
users of the library from defining their own. It can also lead to
clashes in the case that two libraries define instances for the same
type.
Instead of (or in addition to) defining functions that require a
Render
or Parse
instance, define a
higher-order function that takes a “render” or “parse” function. Such a
function is more general, as it can be used with or without TTC.
Show
, Read
,
IsString
Usage
The use of Show
and Read
instances should
be strictly limited.
When using third-party software that makes use of Show
and/or Read
instances, I often create a type that wraps the
external type in a newtype
and exposes a limited API from
the type module. The Show
and Read
instances
of the wrapped type is only used within that type module.
In other cases, Show
instances should only be used for
debugging purposes. It is important to create Show
instances so that values can be displayed in the REPL. Never use show
instances to render strings for database insertion, API usage, output
for the user, etc.
Read
instances can usually be avoided.
IsString
instances can be declared in order to easily
create a value of a type in the REPL, but such instances should not be
used within the code (including unit tests). Validated
constants provide a safe way to declare constants within the
code.
Avoid Textual Type Ambiguity
In the preface to the first edition of Structure and Interpretation of Computer Programs, Abelson and Sussman wrote:
Programs must be written for people to read, and only incidentally for machines to execute.
The Haskell type system is quite powerful, and type inference allows developers to communicate with the compiler with very little “boilerplate.” One must remember, however, that communicating with the compiler is the easy part. Writing software is as much about communicating with humans, and minimizing keystrokes is not a suitable objective. If your code requires an IDE (such as HLS) to understand, it is likely too complicated and has much room for improvement. Some languages require the use of an IDE because they are too verbose; writing code that requires an IDE because it is too concise is just as bad. Using an IDE to write code is a matter of taste, but strive to write code that can be understood on a printed page (or GitHub code listing), without the assistance of an IDE.
Since TTC provides an abstraction over the most common textual data
types, it can make understanding the actual types difficult. Even in
cases where the type system can determine the correct types for
convert
, render
, or parse
,
consider using an equivalent function that communicates the type to the
human reading the code if the type is not already obvious. A few extra
characters serve as documentation that may save people from having to
investigate using an IDE or check reference documentation.
Always Use
OverloadedStrings
Use of the OverloadedStrings
extension changes the types
of string constants in a program. In some cases, code must be written
differently depending on if the extension is enabled or not. Code that
does not use OverloadedStrings
can therefore fail to load
in a REPL that has OverloadedStrings
enabled. I recommend
enabling OverloadedStrings
in the project
.cabal
file to ensure that all of the modules support it
and can be loaded in a REPL that has OverloadedStrings
enabled.