text-display-0.0.3.0: src/Data/Text/Display.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
-- |
-- Module : Data.Text.Display
-- Copyright : © Hécate Moonlight, 2021
-- License : MIT
-- Maintainer : hecate@glitchbra.in
-- Stability : stable
--
-- Use 'display' to produce user-facing text
module Data.Text.Display
( -- * Documentation
display
, Display (..)
-- * Deriving your instance automatically
, ShowInstance (..)
, OpaqueInstance (..)
-- * Writing your instance by hand
, displayParen
-- * Design choices
-- $designChoices
)
where
import Control.Exception hiding (TypeError)
import Data.ByteString
import qualified Data.ByteString.Lazy as BL
import Data.Int
import Data.Kind
import Data.List.NonEmpty
import Data.Proxy
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import Data.Text.Lazy.Builder (Builder)
import qualified Data.Text.Lazy.Builder as TB
import qualified Data.Text.Lazy.Builder.Int as TB
import qualified Data.Text.Lazy.Builder.RealFloat as TB
import Data.Word
import GHC.TypeLits
-- | A typeclass for user-facing output.
--
-- @since 0.0.1.0
class Display a where
{-# MINIMAL displayBuilder | displayPrec #-}
-- | Implement this method to describe how to convert your value to 'Builder'.
displayBuilder :: a -> Builder
displayBuilder = displayPrec 0
-- | The method 'displayList' is provided to allow for a specialised
-- way to render lists of a certain value.
-- This is used to render the list of 'Char' as a string of characters
-- enclosed in double quotes, rather than between square brackets and
-- separated by commas.
--
-- === Example
--
-- > import qualified Data.Text.Lazy.Builder as TB
-- >
-- > instance Display Char where
-- > displayBuilder c = TB.fromText $ T.singleton c
-- > displayList cs = TB.fromText $ T.pack cs
-- >
-- > instance Display a => Display [a] where
-- > -- In this instance, 'displayBuilder' is defined in terms of 'displayList', which for most types
-- > -- is defined as the default written in the class declaration.
-- > -- But when a ~ Char, there is an explicit implementation that is selected instead, which
-- > -- provides the rendering of the character string between double quotes.
-- > displayBuilder = displayList
--
-- ==== How implementations are selected
--
-- > displayBuilder ([1,2,3] :: [Int])
-- > → displayBuilder @[Int] = displayBuilderList @Int
-- > → Default `displayList`
-- >
-- > displayBuilder ("abc" :: [Char])
-- > → displayBuilder @[Char] = displayBuilderList @Char
-- > → Custom `displayList`
displayList :: [a] -> Builder
displayList [] = "[]"
displayList (x : xs) = displayList' xs ("[" <> displayBuilder x)
where
displayList' :: [a] -> Builder -> Builder
displayList' [] acc = acc <> "]"
displayList' (y : ys) acc = displayList' ys (acc <> "," <> displayBuilder y)
-- | The method 'displayPrec' allows you to write instances that
-- require nesting. The precedence parameter can be thought of as a
-- suggestion coming from the surrounding context for how tightly to bind. If the precedence
-- parameter is higher than the precedence of the operator (or constructor, function, etc.)
-- being displayed, then that suggests that the output will need to be surrounded in parentheses
-- in order to bind tightly enough (see 'displayParen').
--
-- For example, if an operator constructor is being displayed, then the precedence requirement
-- for its arguments will be the precedence of the operator. Meaning, if the argument
-- binds looser than the surrounding operator, then it will require parentheses.
--
-- Note that function/constructor application has an effective precedence of 10.
--
-- === Examples
--
-- > instance Display a => Display (Maybe a) where
-- > -- In this instance, we define 'displayPrec' rather than 'displayBuilder' as we need to decide
-- > -- whether or not to surround ourselves in parentheses based on the surrounding context.
-- > -- If the precedence parameter is higher than 10 (the precedence of constructor application)
-- > -- then we indeed need to surround ourselves in parentheses to avoid malformed outputs
-- > -- such as @Just Just 5@.
-- > -- We then set the precedence parameter of the inner 'displayPrec' to 11, as even
-- > -- constructor application is not strong enough to avoid parentheses.
-- > displayPrec _ Nothing = "Nothing"
-- > displayPrec prec (Just a) = displayParen (prec > 10) $ "Just " <> displayPrec 11 a
--
-- > data Pair a b = a :*: b
-- > infix 5 :*: -- arbitrary choice of precedence
-- > instance (Display a, Display b) => Display (Pair a b) where
-- > displayPrec prec (a :*: b) = displayParen (prec > 5) $ displayPrec 6 a <> " :*: " <> displayPrec 6 b
displayPrec ::
-- | The precedence level passed in by the surrounding context
Int ->
a ->
Builder
displayPrec _ = displayBuilder
-- | Convert a value to a readable 'Text'.
--
-- === Examples
-- >>> display 3
-- "3"
--
-- >>> display True
-- "True"
--
-- @since 0.0.1.0
display :: Display a => a -> Text
display a = TL.toStrict $ TB.toLazyText $ displayBuilder a
-- | 🚫 You should not try to display functions!
--
-- 💡 Write a 'newtype' wrapper that represents your domain more accurately.
-- If you are not consciously trying to use 'display' on a function,
-- make sure that you are not missing an argument somewhere.
--
-- @since 0.0.1.0
instance CannotDisplayBareFunctions => Display (a -> b) where
displayBuilder = undefined
-- | @since 0.0.1.0
type family CannotDisplayBareFunctions :: Constraint where
CannotDisplayBareFunctions =
TypeError
( 'Text "🚫 You should not try to display functions!"
':$$: 'Text "💡 Write a 'newtype' wrapper that represents your domain more accurately."
':$$: 'Text " If you are not consciously trying to use `display` on a function,"
':$$: 'Text " make sure that you are not missing an argument somewhere."
)
-- | 🚫 You should not try to display strict ByteStrings!
--
-- 💡 Always provide an explicit encoding.
-- Use 'Data.Text.Encoding.decodeUtf8'' or 'Data.Text.Encoding.decodeUtf8With' to convert from UTF-8
--
-- @since 0.0.1.0
instance CannotDisplayByteStrings => Display ByteString where
displayBuilder = undefined
-- | 🚫 You should not try to display lazy ByteStrings!
--
-- 💡 Always provide an explicit encoding.
-- Use 'Data.Text.Encoding.decodeUtf8'' or 'Data.Text.Encoding.decodeUtf8With' to convert from UTF-8
--
-- @since 0.0.1.0
instance CannotDisplayByteStrings => Display BL.ByteString where
displayBuilder = undefined
type family CannotDisplayByteStrings :: Constraint where
CannotDisplayByteStrings =
TypeError
( 'Text "🚫 You should not try to display ByteStrings!"
':$$: 'Text "💡 Always provide an explicit encoding"
':$$: 'Text "Use 'Data.Text.Encoding.decodeUtf8'' or 'Data.Text.Encoding.decodeUtf8With' to convert from UTF-8"
)
-- | A utility function that surrounds the given 'Builder' with parentheses when the Bool parameter is True.
-- Useful for writing instances that may require nesting. See the 'displayPrec' documentation for more
-- information.
--
-- @since 0.0.1.0
displayParen :: Bool -> Builder -> Builder
displayParen b txt = if b then "(" <> txt <> ")" else txt
-- | This wrapper allows you to create an opaque instance for your type,
-- useful for redacting sensitive content like tokens or passwords.
--
-- === Example
--
-- > data UserToken = UserToken UUID
-- > deriving Display
-- > via (OpaqueInstance "[REDACTED]" UserToken)
--
-- > display $ UserToken "7a01d2ce-31ff-11ec-8c10-5405db82c3cd"
-- > "[REDACTED]"
--
-- @since 0.0.1.0
newtype OpaqueInstance (str :: Symbol) (a :: Type) = Opaque a
-- | This wrapper allows you to create an opaque instance for your type,
-- useful for redacting sensitive content like tokens or passwords.
--
-- @since 0.0.1.0
instance KnownSymbol str => Display (OpaqueInstance str a) where
displayBuilder _ = TB.fromString $ symbolVal (Proxy @str)
-- | This wrapper allows you to rely on a pre-existing 'Show' instance in order to
-- derive 'Display' from it.
--
-- === Example
--
-- > data AutomaticallyDerived = AD
-- > -- We derive 'Show'
-- > deriving stock Show
-- > -- We take advantage of the 'Show' instance to derive 'Display' from it
-- > deriving Display
-- > via (ShowInstance AutomaticallyDerived)
--
-- @since 0.0.1.0
newtype ShowInstance (a :: Type)
= ShowInstance a
deriving newtype
( -- | @since 0.0.1.0
Show
)
-- | This wrapper allows you to rely on a pre-existing 'Show' instance in order to derive 'Display' from it.
--
-- @since 0.0.1.0
instance Show e => Display (ShowInstance e) where
displayBuilder s = TB.fromString $ show s
-- @since 0.0.1.0
newtype DisplayDecimal e
= DisplayDecimal e
deriving newtype
(Integral, Real, Enum, Ord, Num, Eq)
-- @since 0.0.1.0
instance Integral e => Display (DisplayDecimal e) where
displayBuilder = TB.decimal
-- @since 0.0.1.0
newtype DisplayRealFloat e
= DisplayRealFloat e
deriving newtype
(RealFloat, RealFrac, Real, Ord, Eq, Num, Fractional, Floating)
-- @since 0.0.1.0
instance RealFloat e => Display (DisplayRealFloat e) where
displayBuilder = TB.realFloat
-- | @since 0.0.1.0
deriving via (ShowInstance ()) instance Display ()
-- | @since 0.0.1.0
deriving via (ShowInstance Bool) instance Display Bool
-- | @since 0.0.1.0
-- 'displayList' is overloaded, so that when the @Display [a]@ instance calls 'displayList',
-- we end up with a nice string instead of a list of chars between brackets.
--
-- >>> display [1, 2, 3]
-- "[1,2,3]"
--
-- >>> display ['h', 'e', 'l', 'l', 'o']
-- "hello"
instance Display Char where
-- This instance's implementation is used in the haddocks of the typeclass.
-- If you change it, reflect the change in the documentation.
displayBuilder c = TB.fromText $ T.singleton c
displayList cs = TB.fromText $ T.pack cs
-- | Lazy 'TL.Text'
--
-- @since 0.0.1.0
instance Display TL.Text where
displayBuilder = TB.fromLazyText
-- | Strict 'Data.Text.Text'
--
-- @since 0.0.1.0
instance Display Text where
displayBuilder = TB.fromText
-- | @since 0.0.1.0
instance Display a => Display [a] where
{-# SPECIALIZE instance Display [String] #-}
{-# SPECIALIZE instance Display [Char] #-}
{-# SPECIALIZE instance Display [Int] #-}
-- In this instance, 'displayBuilder' is defined in terms of 'displayList', which for most types
-- is defined as the default written in the class declaration.
-- But when @a ~ Char@, there is an explicit implementation that is selected instead, which
-- provides the rendering of the character string between double quotes.
displayBuilder = displayList
-- | @since 0.0.1.0
instance Display a => Display (NonEmpty a) where
displayBuilder (a :| as) = displayBuilder a <> TB.fromString " :| " <> displayBuilder as
-- | @since 0.0.1.0
instance Display a => Display (Maybe a) where
-- In this instance, we define 'displayPrec' rather than 'displayBuilder' as we need to decide
-- whether or not to surround ourselves in parentheses based on the surrounding context.
-- If the precedence parameter is higher than 10 (the precedence of constructor application)
-- then we indeed need to surround ourselves in parentheses to avoid malformed outputs
-- such as @Just Just 5@.
-- We then set the precedence parameter of the inner 'displayPrec' to 11, as even
-- constructor application is not strong enough to avoid parentheses.
displayPrec _ Nothing = "Nothing"
displayPrec prec (Just a) = displayParen (prec > 10) $ "Just " <> displayPrec 11 a
-- | @since 0.0.1.0
deriving via (DisplayRealFloat Double) instance Display Double
-- | @since 0.0.1.0
deriving via (DisplayRealFloat Float) instance Display Float
-- | @since 0.0.1.0
deriving via (DisplayDecimal Int) instance Display Int
-- | @since 0.0.1.0
deriving via (DisplayDecimal Int8) instance Display Int8
-- | @since 0.0.1.0
deriving via (DisplayDecimal Int16) instance Display Int16
-- | @since 0.0.1.0
deriving via (DisplayDecimal Int32) instance Display Int32
-- | @since 0.0.1.0
deriving via (DisplayDecimal Int64) instance Display Int64
-- | @since 0.0.1.0
deriving via (DisplayDecimal Integer) instance Display Integer
-- | @since 0.0.1.0
deriving via (DisplayDecimal Word) instance Display Word
-- | @since 0.0.1.0
deriving via (DisplayDecimal Word8) instance Display Word8
-- | @since 0.0.1.0
deriving via (DisplayDecimal Word16) instance Display Word16
-- | @since 0.0.1.0
deriving via (DisplayDecimal Word32) instance Display Word32
-- | @since 0.0.1.0
deriving via (DisplayDecimal Word64) instance Display Word64
-- | @since 0.0.1.0
deriving via (ShowInstance IOException) instance Display IOException
-- | @since 0.0.1.0
deriving via (ShowInstance SomeException) instance Display SomeException
-- | @since 0.0.1.0
instance (Display a, Display b) => Display (a, b) where
displayBuilder (a, b) = "(" <> displayBuilder a <> "," <> displayBuilder b <> ")"
-- | @since 0.0.1.0
instance (Display a, Display b, Display c) => Display (a, b, c) where
displayBuilder (a, b, c) = "(" <> displayBuilder a <> "," <> displayBuilder b <> "," <> displayBuilder c <> ")"
-- | @since 0.0.1.0
instance (Display a, Display b, Display c, Display d) => Display (a, b, c, d) where
displayBuilder (a, b, c, d) = "(" <> displayBuilder a <> "," <> displayBuilder b <> "," <> displayBuilder c <> "," <> displayBuilder d <> ")"
-- $designChoices
--
-- === A “Lawless Typeclass”
--
-- The 'Display' typeclass does not contain any law. This is a controversial choice for some people,
-- but the truth is that there are not any laws to ask of the consumer that are not already enforced
-- by the type system and the internals of the 'Data.Text.Internal.Text' type.
--
-- === "🚫 You should not try to display functions!"
--
-- Sometimes, when using the library, you may encounter this message:
--
-- > • 🚫 You should not try to display functions!
-- > 💡 Write a 'newtype' wrapper that represents your domain more accurately.
-- > If you are not consciously trying to use `display` on a function,
-- > make sure that you are not missing an argument somewhere.
--
-- The 'display' library does not allow the definition and usage of 'Display' on
-- bare function types (@(a -> b)@).
-- Experience and time have shown that due to partial application being baked in the language,
-- many users encounter a partial application-related error message when a simple missing
-- argument to a function is the root cause.
--
-- There may be legitimate uses of a 'Display' instance on a function type.
-- But these usages are extremely dependent on their domain of application.
-- That is why it is best to wrap them in a newtype that can better
-- express and enforce the domain.
--
-- === "🚫 You should not try to display ByteStrings!"
--
-- An arbitrary ByteStrings cannot be safely converted to text without prior knowledge of its encoding.
--
-- As such, in order to avoid dangerously blind conversions, it is recommended to use a specialised
-- function such as 'Data.Text.Encoding.decodeUtf8'' or 'Data.Text.Encoding.decodeUtf8With' if you wish to turn a UTF8-encoded ByteString
-- to Text.