[ANN] ttc-1.0.0.0 - Textual Type Classes

I am happy to announce the 1.0 release of TTC (Textual Type Classes). The library provides the following functionality: * The Textual type class is used to convert between common textual data types. It can be used to write functions that accept or return values of any of these textual data types. * The Render type class is used to render a value as text. Avoid bugs by only using Show for debugging/development purposes. * The Parse type class is used to parse a value from text. Unlike Read, it has support for error messages. * Validate constants at compile-time using Parse instances. Links: * GitHub: https://github.com/ExtremaIS/ttc-haskell * Hackage: https://hackage.haskell.org/package/ttc * Guided Tour: https://www.extrema.is/articles/ttc-textual-type-classes Best regards, Travis

On Thu, 3 Jun 2021, Travis Cardwell via Haskell-Cafe wrote:
I am happy to announce the 1.0 release of TTC (Textual Type Classes).
The library provides the following functionality:
* The Textual type class is used to convert between common textual data types. It can be used to write functions that accept or return values of any of these textual data types.
* The Render type class is used to render a value as text. Avoid bugs by only using Show for debugging/development purposes.
That is, your class is intended for text representation for program users (instead of programmers)? I think a notable difference between Show (which is for programmers) and a human readable text formatter should be a control of formatting details. Currently 'printf' is the way to format human readable text and you can control formatting of numbers e.g. by "%d", "%3d", "%03d" etc. It's not type safe but better than Show.

On Thu, Jun 3, 2021 at 2:57 PM Henning Thielemann wrote:
That is, your class is intended for text representation for program users (instead of programmers)?
Yes, that is correct. While Show instances should produce valid Haskell code and Read instances should parse that code to create the original value, forming an isomorphism, the Render and Parse type classes can be used by the developer to define instances as required for each particular application. The Render and Parse type classes in Data.TTC have no instances, allowing developers to write their own instances for Int for example, but some default instances can optionally be imported from Data.TTC.Instances when they are appropriate.
I think a notable difference between Show (which is for programmers) and a human readable text formatter should be a control of formatting details.
Currently 'printf' is the way to format human readable text and you can control formatting of numbers e.g. by "%d", "%3d", "%03d" etc. It's not type safe but better than Show.
Indeed. Due to the nature of type classes, each concrete type can have a single instance. I usually define Render and Parse instances that use the canonical representation, and I provide separate functions for customized formatting based on options. Functions that only need the canonical representation can rely on the Render instance, while functions that need to be more flexible can accept a render/formatting function, to which TTC.render can be passed when the canonical representation is sufficient. I worry that my explanation is not clear, so here is a minimal example, using a type Quantity that is assumed to have a Render instance. -- This function relies on the Render instance of Quantity. formatQuantityList :: [Quantity] -> Text -- This function allows you to customize how a Quantity is rendered. -- TTC.render can be passed as the first argument when appropriate. formatQuantityList' :: (Quantity -> Text) -> [Quantity] -> Text The common way to get around the one-instance-per-type limitation is to use newtypes. I generally do not use this strategy with TTC to provide different ways to format text, however. Best regards, Travis

On Thu, 3 Jun 2021, Travis Cardwell wrote:
On Thu, Jun 3, 2021 at 2:57 PM Henning Thielemann wrote:
That is, your class is intended for text representation for program users (instead of programmers)?
Yes, that is correct. While Show instances should produce valid Haskell code and Read instances should parse that code to create the original value, forming an isomorphism, the Render and Parse type classes can be used by the developer to define instances as required for each particular application. The Render and Parse type classes in Data.TTC have no instances, allowing developers to write their own instances for Int for example, but some default instances can optionally be imported from Data.TTC.Instances when they are appropriate.
Are these instances orphan?
The common way to get around the one-instance-per-type limitation is to use newtypes. I generally do not use this strategy with TTC to provide different ways to format text, however.
Is it feasible to have a newtype for any of "%3d", "%03d", "%05d" ...?

On Thu, Jun 3, 2021 at 4:49 PM Henning Thielemann wrote:
On Thu, 3 Jun 2021, Travis Cardwell wrote:
The Render and Parse type classes in Data.TTC have no instances, allowing developers to write their own instances for Int for example, but some default instances can optionally be imported from Data.TTC.Instances when they are appropriate.
Are these instances orphan?
Indeed they are. I use the following directive to hide the warnings for that module: {-# OPTIONS_GHC -fno-warn-orphans #-}
The common way to get around the one-instance-per-type limitation is to use newtypes. I generally do not use this strategy with TTC to provide different ways to format text, however.
Is it feasible to have a newtype for any of "%3d", "%03d", "%05d" ...?
In cases where the formats are fixed and finite/few, one could create a newtype corresponding to each format. Personally, I think that using a formatting function would result in a better design in most cases, however. For example, one might want to format numbers according to locale, using appropriate decimal separators. If newtypes are defined for each supported format, the constructor could be passed to functions to determine how values are formatted. formatQuantityList :: TTC.Render a => (Quantity -> a) -> [Quantity] -> TLB.Builder In my opinion, passing a formatting function instead results in a better design because it is more flexible. formatQuantityList :: (Quantity -> TLB.Builder) -> [Quantity] -> TLB.Builder This function might be called as follows, where locale is a sum type that enumerates the supported locales (instead of using newtypes) and the formatFor function formats the Quantity value appropriately for the specified locale: formatQuantityList (Quantity.formatFor locale) quantities I hope that I understood your question correctly and that my response is helpful. Best regards, Travis

On Thu, 3 Jun 2021, Travis Cardwell wrote:
On Thu, Jun 3, 2021 at 4:49 PM Henning Thielemann wrote:
On Thu, 3 Jun 2021, Travis Cardwell wrote:
The Render and Parse type classes in Data.TTC have no instances, allowing developers to write their own instances for Int for example, but some default instances can optionally be imported from Data.TTC.Instances when they are appropriate.
Are these instances orphan?
Indeed they are. I use the following directive to hide the warnings for that module:
{-# OPTIONS_GHC -fno-warn-orphans #-}
I would not do that. Even orphan instances must be unique. If I would decide to define my own instances but import a library that transitively imports Data.TTC.Instances somewhere, I get a clash. Orphan instances are really only helpful for the case where you need an instance but you neither maintain the class nor the type definition but otherwise are sure that your instance is the one and only.

Agreed. I like to use DefaultSignatures like this: class Parse a where parse :: ... default parse :: ParseInternal a => ... -- same ... as above parse = parseInternal class ParseInternal a where parseInternal :: ... -- again, same ... instance ParseInternal Double where parseInternal = ... -- specific implementation instance ParseInternal Float where parseInternal = ... -- specific implementation Then all the user needs to do is to say instance Parse Double and it would automagically use the provided implementation.
On 3 Jun 2021, at 11:18, Henning Thielemann
wrote: On Thu, 3 Jun 2021, Travis Cardwell wrote:
On Thu, Jun 3, 2021 at 4:49 PM Henning Thielemann wrote:
On Thu, 3 Jun 2021, Travis Cardwell wrote:
The Render and Parse type classes in Data.TTC have no instances, allowing developers to write their own instances for Int for example, but some default instances can optionally be imported from Data.TTC.Instances when they are appropriate.
Are these instances orphan?
Indeed they are. I use the following directive to hide the warnings for that module:
{-# OPTIONS_GHC -fno-warn-orphans #-}
I would not do that. Even orphan instances must be unique. If I would decide to define my own instances but import a library that transitively imports Data.TTC.Instances somewhere, I get a clash. Orphan instances are really only helpful for the case where you need an instance but you neither maintain the class nor the type definition but otherwise are sure that your instance is the one and only. _______________________________________________ Haskell-Cafe mailing list To (un)subscribe, modify options or view archives go to: http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe Only members subscribed via the mailman list are allowed to post.

I would not do that. Even orphan instances must be unique. If I would decide to define my own instances but import a library that
On 3 Jun 2021, at 11:18, Henning Thielemann
Agreed. I like to use DefaultSignatures like this:
class Parse a where parse :: ... default parse :: ParseInternal a => ... -- same ... as above parse = parseInternal
class ParseInternal a where parseInternal :: ... -- again, same ...
instance ParseInternal Double where parseInternal = ... -- specific implementation instance ParseInternal Float where parseInternal = ... -- specific implementation
Then all the user needs to do is to say
instance Parse Double
and it would automagically use the provided implementation.
On 3 Jun 2021, at 11:18, Henning Thielemann
wrote: On Thu, 3 Jun 2021, Travis Cardwell wrote:
On Thu, Jun 3, 2021 at 4:49 PM Henning Thielemann wrote:
On Thu, 3 Jun 2021, Travis Cardwell wrote:
The Render and Parse type classes in Data.TTC have no instances, allowing developers to write their own instances for Int for example, but some default instances can optionally be imported from Data.TTC.Instances when they are appropriate.
Are these instances orphan?
Indeed they are. I use the following directive to hide the warnings for that module:
{-# OPTIONS_GHC -fno-warn-orphans #-}
I would not do that. Even orphan instances must be unique. If I would decide to define my own instances but import a library that transitively imports Data.TTC.Instances somewhere, I get a clash. Orphan instances are really only helpful for the case where you need an instance but you neither maintain the class nor the type definition but otherwise are sure that your instance is the one and only. _______________________________________________ Haskell-Cafe mailing list To (un)subscribe, modify options or view archives go to: http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe Only members subscribed via the mailman list are allowed to post.
_______________________________________________ Haskell-Cafe mailing list To (un)subscribe, modify options or view archives go to: http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe Only members subscribed via the mailman list are allowed to post.

On Thu, Jun 3, 2021 at 6:44 PM MigMit wrote:
Agreed. I like to use DefaultSignatures like this: <SNIP> Then all the user needs to do is to say
instance Parse Double
and it would automagically use the provided implementation.
This looks like a great compromise, allowing developers to utilize the default implementation for a specific type with very little code. A prominent warning against doing so in shared libraries would still be needed, of course. I look forward to experimenting with this design soon! Thank you very much for the suggestion! Also, thank you for the example code, as this pattern is new to me. Best regards, Travis

I have created an issue on GitHub concerning the orphan instances. https://github.com/ExtremaIS/ttc-haskell/issues/1 Thank you very much for the feedback and advice so far. Anybody who would like to weigh in is welcome to add a comment to the GitHub issue or reply on this thread. I avoided editorializing in the issue description, but please feel free to express opinions and preferences. Best regards, Travis

On Thu, Jun 3, 2021 at 6:18 PM Henning Thielemann wrote:
On Thu, 3 Jun 2021, Travis Cardwell wrote:
On Thu, Jun 3, 2021 at 4:49 PM Henning Thielemann wrote:
On Thu, 3 Jun 2021, Travis Cardwell wrote:
The Render and Parse type classes in Data.TTC have no instances, allowing developers to write their own instances for Int for example, but some default instances can optionally be imported from Data.TTC.Instances when they are appropriate.
Are these instances orphan?
Indeed they are. I use the following directive to hide the warnings for that module:
{-# OPTIONS_GHC -fno-warn-orphans #-}
I would not do that. Even orphan instances must be unique. If I would decide to define my own instances but import a library that transitively imports Data.TTC.Instances somewhere, I get a clash. Orphan instances are really only helpful for the case where you need an instance but you neither maintain the class nor the type definition but otherwise are sure that your instance is the one and only.
Indeed. It seems that I have forgotten to put a prominent warning about usage of those instances. In the past, I have used TTC in applications, avoiding using it in (shared) libraries. In shared libraries, I simply define "render" and "parse" functions using the most appropriate textual types. Applications can then define (orphan) instances of Render and Parse as simple wrappers around the functions exposed by the library. By only defining instances in the leaves of the dependency hierarchy, one does not run into conflicts. Very recently, I have started to use TTC in libraries as well, however, in cases where I would otherwise have to duplicate functionality that TTC provides. I had planned on advising not using TTC in libraries, but I ended up deciding to not do so. Frankly, I have never used Data.TTC.Instances myself. I included it in an attempt to provide convenience for others. It is indeed extremely inconvenient if a shared library imports the instances, and that is possible->probable even with a prominent warning against doing so. Perhaps it would be best to remove the Data.TTC.Instances module as well as add a prominent warning against declaring orphan instances, especially in a shared library. Thank you very much for the feedback! I really appreciate it! Best regards, Travis
participants (4)
-
Henning Thielemann
-
MigMit
-
Tom Smeding
-
Travis Cardwell