packages feed

ghc-symbol-0: src/GHC/Symbol.hs

{-# LANGUAGE MagicHash             #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TemplateHaskellQuotes #-}
{-# LANGUAGE Trustworthy           #-}
{-# LANGUAGE TypeApplications      #-}
{-# LANGUAGE TypeFamilies          #-}
{-# OPTIONS_GHC -Wno-orphans #-}
-- | A (slightly unsafe) term level implementation for 'Symbol'.
--
-- As 'Symbol' in @base@ doesn't have any term-level operations,
-- it's sometimes necessary to have two copies of the same data,
-- the first one using 'String' on term level and
-- second one using 'Symbol' to be promtoed to type level.
--
-- In GHC-9.2 the similar problem was fixed for @Nat@ and @Natural@,
-- which were distinct types (kinds) before that.
--
-- As 'String' is a list of 'Char's, we cannot make @type 'Symbol' = 'String'@,
-- but we could do @newtype 'Symbol' = MkSymbol 'String'@.
-- This module /fakes/ that by using 'unsafeCoerce' under the hood.
--
-- Fleshing out 'Symbol' on term level is suggested in
-- https://gitlab.haskell.org/ghc/ghc/-/issues/10776#note_109601
-- in 2015.
--
-- This implementation is slightly unsafe, as currently 'Symbol' is defined
-- as empty data type:
--
-- @
-- data 'Symbol'
-- @
--
-- This means that you can write
--
-- @
-- dangerous :: Symbol -> Int
-- dangerous x = case x of
-- @
--
-- and because GHC sees through everything, and knows that 'Symbol' is empty,
-- the above compiles without a warning.
--
-- If 'Symbol' was defined as @newtype Symbol = Symbol Any@, the
-- above problem would go away, and also implementation of this
-- module would be safer, as 'unsafeCoerce'ing from lifted type to 'Any'
-- and back is guaranteed to work.
--
-- Of course life would be easier if we just had
--
-- @
-- newtype 'Symbol' = MkSymbol 'String'
-- @
--
-- but until that is done, you may find this module useful.
--
-- /Note:/ 'Symbol' is not @Text@. @Text@ has an invariant: it represents /valid/ Unicode text.
-- 'Symbol' is just a list of characters (= Unicode codepoints), like 'String'.
-- E.g.
--
-- >>> "\55555" :: String
-- "\55555"
--
-- >>> "\55555" :: Symbol
-- "\55555"
--
-- but @text@ replaces surrogate codepoints:
--
-- >>> "\55555" :: Text
-- "\65533"
--
-- 'Symbol' could use some packed representation of list of characters,
-- if also 'KnownSymbol' would use it as well. Currently
-- 'KnownSymbol' dictionary carries a 'String', so having
-- 'Symbol' be a 'String' is justified'.
--
module GHC.Symbol (
    -- * Symbol type
    Symbol,
    symbolToString,
    consSymbol,
    unconsSymbol,
    -- * Type level
    KnownSymbol,
    symbolVal,
    symbolVal',
    AppendSymbol,
    CmpSymbol,
    someSymbolVal,
    SomeSymbol (..),
    sameSymbol,
) where

import Control.DeepSeq            (NFData (..))
import Data.Binary                (Binary (..))
import Data.String                (IsString (..))
import GHC.Exts                   (IsList (..), Proxy#)
import GHC.TypeLits
       (AppendSymbol, CmpSymbol, KnownSymbol, SomeSymbol, Symbol, sameSymbol)
import Language.Haskell.TH.Syntax (Lift (..))
import Text.Printf                (PrintfArg (..))
import Unsafe.Coerce              (unsafeCoerce)

import qualified GHC.TypeLits as GHC

-- $setup
-- >>> :set -XOverloadedStrings -XTypeApplications -XDataKinds
-- >>> import Data.Text (Text)
-- >>> import Data.Proxy (Proxy (..))

-------------------------------------------------------------------------------
-- Conversions
-------------------------------------------------------------------------------

symbolToString :: Symbol -> String
symbolToString = unsafeCoerce @Symbol @String

symbolFromString :: String -> Symbol
symbolFromString = unsafeCoerce @String @Symbol

-------------------------------------------------------------------------------
-- Public interface
-------------------------------------------------------------------------------

-- this is not exported though, use mempty or "".
emptySymbol :: Symbol
emptySymbol = symbolFromString ""

-- | Prepend a character to a 'Symbol'
--
-- >>> consSymbol 'a' "cute"
-- "acute"
--
consSymbol :: Char -> Symbol -> Symbol
consSymbol c s = symbolFromString (c : symbolToString s)

-- | Inverse of 'consSymbol'
--
-- >>> unconsSymbol ""
-- Nothing
--
-- >>> unconsSymbol "mother"
-- Just ('m',"other")
--
unconsSymbol :: Symbol -> Maybe (Char, Symbol)
unconsSymbol s = case symbolToString s of
    []   -> Nothing
    c:s' -> Just (c, symbolFromString s')

-- instances

-- |
--
-- >>> "foo" :: Symbol
-- "foo"
--
instance Show Symbol where
    showsPrec d s = showsPrec d (symbolToString s)

instance Read Symbol where
    readsPrec d s = [ (symbolFromString x, s') | ~(x, s') <- readsPrec d s ]

instance Eq Symbol where
    x == y = symbolToString x == symbolToString y

instance Ord Symbol where
    compare x y = compare (symbolToString x) (symbolToString y)

-- |
--
-- >>> "foo" :: Symbol
-- "foo"
--
instance IsString Symbol where
    fromString = symbolFromString

instance Semigroup Symbol where
     x <> y = symbolFromString (symbolToString x ++ symbolToString y)

instance Monoid Symbol where
    mempty = emptySymbol
    mappend = (<>)

instance NFData Symbol where
    rnf s = rnf (symbolToString s)

instance Binary Symbol where
    get = fmap symbolFromString get
    put s = put (symbolToString s)

instance Lift Symbol where
    liftTyped s = [|| symbolFromString s' ||] where s' = symbolToString s

instance PrintfArg Symbol where
    formatArg s = formatArg (symbolToString s)

instance IsList Symbol where
    type Item Symbol = Char
    fromList = symbolFromString
    toList   = symbolToString

-------------------------------------------------------------------------------
-- TypeLits
-------------------------------------------------------------------------------

-- |
--
-- >>> symbolVal (Proxy @"foobar")
-- "foobar"
--
symbolVal :: forall n proxy. KnownSymbol n => proxy n -> Symbol
symbolVal p = symbolFromString (GHC.symbolVal p)

symbolVal' :: forall n. KnownSymbol n => Proxy# n -> Symbol
symbolVal' p = symbolFromString (GHC.symbolVal' p)

-- |
--
-- >>> someSymbolVal "foobar"
-- "foobar"
--
someSymbolVal :: Symbol -> SomeSymbol
someSymbolVal s = GHC.someSymbolVal (symbolToString s)