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.
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
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
Parse instances, for example.
When implementing a shared library, defining
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
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.
The use of
Read instances should be strictly limited.
When using third-party software that makes use of
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
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
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.
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