packages feed

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.