packages feed

core-text-0.3.0.0: lib/Core/Text/Rope.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

-- |
-- If you're accustomed to working with text in almost any other programming
-- language, you'd be aware that a \"string\" typically refers to an in-memory
-- /array/ of characters. Traditionally this was a single ASCII byte per
-- character; more recently UTF-8 variable byte encodings which dramatically
-- complicates finding offsets but which gives efficient support for the
-- entire Unicode character space. In Haskell, the original text type,
-- 'String', is implemented as a list of 'Char' which, because a Haskell list
-- is implemented as a /linked-list of boxed values/, is wildly inefficient at
-- any kind of scale.
--
-- In modern Haskell there are two primary ways to represent text.
--
-- First is via the [rather poorly named] @ByteString@ from the __bytestring__
-- package (which is an array of bytes in pinned memory). The
-- "Data.ByteString.Char8" submodule gives you ways to manipulate those arrays
-- as if they were ASCII characters. Confusingly there are both strict
-- (@Data.ByteString@) and lazy (@Data.ByteString.Lazy@) variants which are
-- often hard to tell the difference between when reading function signatures
-- or haddock documentation. The performance problem an immutable array backed
-- data type runs into is that appending a character (that is, ASCII byte) or
-- concatonating a string (that is, another array of ASCII bytes) is very
-- expensive and requires allocating a new larger array and copying the whole
-- thing into it. This led to the development of \"builders\" which amortize
-- this reallocation cost over time, but it can be cumbersome to switch
-- between @Builder@, the lazy @ByteString@ that results, and then having to
-- inevitably convert to a strict @ByteString@ because that's what the next
-- function in your sequence requires.
--
-- The second way is through the opaque @Text@ type of "Data.Text" from the
-- __text__ package, which is well tuned and high-performing but suffers from
-- the same design; it is likewise backed by arrays. Rather surprisingly, the
-- storage backing Text objects are encoded in UTF-16, meaning every time you
-- want to work with unicode characters that came in from /anywhere/ else and
-- which inevitably are UTF-8 encoded you have to convert to UTF-16 and copy
-- into a new array, wasting time and memory.
--
-- In this package we introduce 'Rope', a text type backed by the 2-3
-- 'Data.FingerTree.FingerTree' data structure from the __fingertree__
-- package. This is not an uncommon solution in many languages as finger trees
-- support exceptionally efficient appending to either end and good
-- performance inserting anywhere else (you often find them as the backing
-- data type underneath text editors for this reason). Rather than 'Char' the
-- pieces of the rope are 'Data.Text.Short.ShortText' from the __text-short__
-- package, which are UTF-8 encoded and in normal memory managed by the
-- Haskell runtime. Conversion from other Haskell text types is not /O(1)/
-- (UTF-8 validity must be checked, or UTF-16 decoded, or...), but in our
-- benchmarking the performance has been comparable to the established types
-- and you may find the resultant interface for combining chunks is comparable
-- to using a Builder, without being forced to use a Builder.
--
-- 'Rope' is used as the text type throughout this library. If you use the
-- functions within this package (rather than converting to other text types)
-- operations are quite efficient. When you do need to convert to another type
-- you can use 'fromRope' or 'intoRope' from the 'Textual' typeclass.
--
-- Note that we haven't tried to cover the entire gamut of operations or
-- customary convenience functions you would find in the other libraries; so
-- far 'Rope' is concentrated on aiding interoperation, being good at
-- appending (lots of) small pieces, and then efficiently taking the resultant
-- text object out to a file handle, be that the terminal console, a file, or
-- a network socket.
module Core.Text.Rope
  ( -- * Rope type
    Rope,
    emptyRope,
    singletonRope,
    replicateRope,
    replicateChar,
    widthRope,
    splitRope,
    insertRope,
    containsCharacter,
    findIndexRope,

    -- * Interoperation and Output
    Textual (fromRope, intoRope, appendRope),
    hWrite,

    -- * Internals
    unRope,
    nullRope,
    unsafeIntoRope,
    Width (..),
  )
where

import Control.DeepSeq (NFData (..))
import Core.Text.Bytes
import qualified Data.ByteString as B (ByteString)
import qualified Data.ByteString.Builder as B
  ( hPutBuilder,
    toLazyByteString,
  )
import qualified Data.ByteString.Lazy as L
  ( ByteString,
    foldrChunks,
    toStrict,
  )
import qualified Data.FingerTree as F
  ( FingerTree,
    Measured (..),
    SearchResult (..),
    ViewL (..),
    empty,
    null,
    search,
    singleton,
    viewl,
    (<|),
    (><),
    (|>),
  )
import Data.Foldable (foldl', foldr', toList)
import Data.Hashable (Hashable, hashWithSalt)
import Data.String (IsString (..))
import qualified Data.Text as T (Text)
import qualified Data.Text.Lazy as U
  ( Text,
    foldrChunks,
    fromChunks,
    toStrict,
  )
import qualified Data.Text.Lazy.Builder as U
  ( Builder,
    fromText,
    toLazyText,
  )
import Data.Text.Prettyprint.Doc (Pretty (..), emptyDoc)
import qualified Data.Text.Short as S
  ( ShortText,
    any,
    append,
    empty,
    findIndex,
    fromByteString,
    fromText,
    length,
    null,
    pack,
    replicate,
    singleton,
    splitAt,
    toBuilder,
    toText,
    unpack,
  )
import qualified Data.Text.Short.Unsafe as S (fromByteStringUnsafe)
import GHC.Generics (Generic)
import System.IO (Handle)

-- |
-- A type for textual data. A rope is text backed by a tree data structure,
-- rather than a single large continguous array, as is the case for strings.
--
-- There are three use cases:
--
-- /Referencing externally sourced data/
--
-- Often we interpret large blocks of data sourced from external systems as
-- text. Ideally we would hold onto this without copying the memory, but (as
-- in the case of @ByteString@ which is the most common source of data) before
-- we can treat it as text we have to validate the UTF-8 content. Safety
-- first. We also copy it out of pinned memory, allowing the Haskell runtime
-- to manage the storage.
--
-- /Interoperating with other libraries/
--
-- The only constant of the Haskell universe is that you won't have the right
-- combination of {strict, lazy} × {@Text@, @ByteString@, @String@, @[Word8]@,
-- etc} you need for the next function call. The 'Textual' typeclass provides
-- for moving between different text representations. To convert between
-- @Rope@ and something else use 'fromRope'; to construct a @Rope@ from
-- textual content in another type use 'intoRope'.
--
-- You can get at the underlying finger tree with the 'unRope' function.
--
-- /Assembling text to go out/
--
-- This involves considerable appending of data, very very occaisionally
-- inserting it. Often the pieces are tiny. To add text to a @Rope@ use the
-- 'appendRope' method as below or the ('Data.Semigroup.<>') operator from
-- "Data.Monoid" (like you would have with a @Builder@).
--
-- Output to a @Handle@ can be done efficiently with 'hWrite'.
newtype Rope
  = Rope (F.FingerTree Width S.ShortText)
  deriving (Generic)

instance NFData Rope where
  rnf (Rope x) = foldMap (\piece -> rnf piece) x

instance Show Rope where
  show text = "\"" ++ fromRope text ++ "\""

instance Eq Rope where
  (==) (Rope x1) (Rope x2) = (==) (stream x1) (stream x2)
    where
      stream x = foldMap S.unpack x

instance Ord Rope where
  compare (Rope x1) (Rope x2) = compare x1 x2

instance Pretty Rope where
  pretty (Rope x) = foldr ((<>) . pretty . S.toText) emptyDoc x

-- |
-- Access the finger tree underlying the @Rope@. You'll want the following
-- imports:
--
-- @
-- import qualified "Data.FingerTree" as F  -- from the __fingertree__ package
-- import qualified "Data.Text.Short" as S  -- from the __text-short__ package
-- @
unRope :: Rope -> F.FingerTree Width S.ShortText
unRope (Rope x) = x
{-# INLINE unRope #-}

-- |
-- The length of the @Rope@, in characters. This is the monoid used to
-- structure the finger tree underlying the @Rope@.
newtype Width = Width Int
  deriving (Eq, Ord, Show, Num, Generic)

instance F.Measured Width S.ShortText where
  measure :: S.ShortText -> Width
  measure piece = Width (S.length piece)

instance Semigroup Width where
  (<>) (Width w1) (Width w2) = Width (w1 + w2)

instance Monoid Width where
  mempty = Width 0
  mappend = (<>)

-- here Maybe we just need type Strand = ShortText and then Rope is
-- FingerTree Strand or Builder (Strand)

instance IsString Rope where
  fromString "" = emptyRope
  fromString xs = Rope . F.singleton . S.pack $ xs

instance Semigroup Rope where
  (<>) text1@(Rope x1) text2@(Rope x2) =
    if F.null x2
      then text1
      else
        if F.null x1
          then text2
          else Rope ((F.><) x1 x2) -- god I hate these operators

instance Monoid Rope where
  mempty = emptyRope
  mappend = (<>)

-- |
-- An zero-length 'Rope'. You can also use @\"\"@ presuming the
-- __@OverloadedStrings@__ language extension is turned on in your source
-- file.
emptyRope :: Rope
emptyRope = Rope F.empty
{-# INLINEABLE emptyRope #-}

-- |
-- A 'Rope' with but a single character.
singletonRope :: Char -> Rope
singletonRope = Rope . F.singleton . S.singleton

-- |
-- Repeat the input 'Rope' @n@ times. The follows the same semantics as other
-- @replicate@ functions; if you ask for zero copies you'll get an empty text
-- and if you ask for lots of @""@ you'll get ... an empty text.
--
-- /Implementation note/
--
-- Rather than copying the input /n/ times, this will simply add structure to hold /n/
-- references to the provided input text.
replicateRope :: Int -> Rope -> Rope
replicateRope count (Rope x) =
  let x' = foldr (\_ acc -> (F.><) x acc) F.empty [1 .. count]
   in Rope x'

-- |
-- Repeat the input 'Char' @n@ times. This is a special case of
-- 'replicateRope' above.
--
-- /Implementation note/
--
-- Rather than making a huge FingerTree full of single characters, this
-- function will allocate a single ShortText comprised of the repeated input
-- character.
replicateChar :: Int -> Char -> Rope
replicateChar count = Rope . F.singleton . S.replicate count . S.singleton

-- |
-- Get the length of this text, in characters.
widthRope :: Rope -> Int
widthRope = foldr' f 0 . unRope
  where
    f piece count = S.length piece + count

nullRope :: Rope -> Bool
nullRope (Rope x) = case F.viewl x of
  F.EmptyL -> True
  (F.:<) piece _ -> S.null piece

-- |
-- Break the text into two pieces at the specified offset.
--
-- Examples:
--
-- @
-- λ> __splitRope 0 \"abcdef\"__
-- (\"\", \"abcdef\")
-- λ> __splitRope 3 \"abcdef\"__
-- (\"abc\", \"def\")
-- λ> __splitRope 6 \"abcdef\"__
-- (\"abcdef\",\"\")
-- @
--
-- Going off either end behaves sensibly:
--
-- @
-- λ> __splitRope 7 \"abcdef\"__
-- (\"abcdef\",\"\")
-- λ> __splitRope (-1) \"abcdef\"__
-- (\"\", \"abcdef\")
-- @
splitRope :: Int -> Rope -> (Rope, Rope)
splitRope i text@(Rope x) =
  let pos = Width i
      result = F.search (\w1 _ -> w1 >= pos) x
   in case result of
        F.Position before piece after ->
          let (Width w) = F.measure before
              (one, two) = S.splitAt (i - w) piece
           in (Rope ((F.|>) before one), Rope ((F.<|) two after))
        F.OnLeft -> (Rope F.empty, text)
        F.OnRight -> (text, Rope F.empty)
        F.Nowhere -> error "Position not found in split. Probable cause: predicate function given not monotonic. This is supposed to be unreachable"

-- |
-- Insert a new piece of text into an existing @Rope@ at the specified offset.
--
-- Examples:
--
-- @
-- λ> __insertRope 3 \"Con\" \"Def 1\"__
-- "DefCon 1"
-- λ> __insertRope 0 \"United \" \"Nations\"__
-- "United Nations"
-- @
insertRope :: Int -> Rope -> Rope -> Rope
insertRope 0 (Rope new) (Rope x) = Rope ((F.><) new x)
insertRope i (Rope new) text =
  let (Rope before, Rope after) = splitRope i text
   in Rope (mconcat [before, new, after])

findIndexRope :: (Char -> Bool) -> Rope -> Maybe Int
findIndexRope predicate = fst . foldl f (Nothing, 0) . unRope
  where
    -- convert this to Maybe monad, maybe
    f :: (Maybe Int, Int) -> S.ShortText -> (Maybe Int, Int)
    f acc piece = case acc of
      (Just j, _) -> (Just j, 0)
      (Nothing, !i) -> case S.findIndex predicate piece of
        Nothing -> (Nothing, i + S.length piece)
        Just !j -> (Just (i + j), 0)

--
-- Manual instance to get around the fact that FingerTree doesn't have a
-- Hashable instance. If this were ever to become a hotspot we could
-- potentially use the Hashed caching type in the finger tree as
--
-- FingerTree Width (Hashed S.ShortText)
--
-- at the cost of endless unwrapping. Another alternative would be to cache
-- hash values in the monoid, changing Width from being a wrapper of Int to
-- a record type with width, hash, and perhaps newlines within the
-- corresponding tree.
--
instance Hashable Rope where
  hashWithSalt salt (Rope x) = foldl' f salt x
    where
      f :: Int -> S.ShortText -> Int
      f num piece = hashWithSalt num piece

-- |
-- Machinery to interpret a type as containing valid Unicode that can be
-- represented as a @Rope@ object.
--
-- /Implementation notes/
--
-- Given that @Rope@ is backed by a finger tree, 'append' is relatively
-- inexpensive, plus whatever the cost of conversion is. There is a subtle
-- trap, however: if adding small fragments of that were obtained by slicing
-- (for example) a large ByteString we would end up holding on to a reference
-- to the entire underlying block of memory. This module is optimized to
-- reduce heap fragmentation by letting the Haskell runtime and garbage
-- collector manage the memory, so instances are expected to /copy/ these
-- substrings out of pinned memory.
--
-- The @ByteString@ instance requires that its content be valid UTF-8. If not
-- an empty @Rope@ will be returned.
--
-- Several of the 'fromRope' implementations are expensive and involve a lot
-- of intermediate allocation and copying. If you're ultimately writing to a
-- handle prefer 'hWrite' which will write directly to the output buffer.
class Textual α where
  -- |
  -- Convert a @Rope@ into another text-like type.
  fromRope :: Rope -> α

  -- |
  -- Take another text-like type and convert it to a @Rope@.
  intoRope :: α -> Rope

  -- |
  -- Append some text to this @Rope@. The default implementation is basically a
  -- convenience wrapper around calling 'intoRope' and 'mappend'ing it to your
  -- text (which will work just fine, but for some types more efficient
  -- implementations are possible).
  appendRope :: α -> Rope -> Rope
  appendRope thing text = text <> intoRope thing

instance Textual (F.FingerTree Width S.ShortText) where
  fromRope = unRope
  intoRope = Rope

instance Textual Rope where
  fromRope = id
  intoRope = id
  appendRope (Rope x2) (Rope x1) = Rope ((F.><) x1 x2)

-- | from "Data.Text.Short"
instance Textual S.ShortText where
  fromRope = foldr S.append S.empty . unRope
  intoRope = Rope . F.singleton
  appendRope piece (Rope x) = Rope ((F.|>) x piece)

-- | from "Data.Text" Strict
instance Textual T.Text where
  fromRope = U.toStrict . U.toLazyText . foldr f mempty . unRope
    where
      f :: S.ShortText -> U.Builder -> U.Builder
      f piece built = (<>) (U.fromText (S.toText piece)) built

  intoRope t = Rope (F.singleton (S.fromText t))
  appendRope chunk (Rope x) = Rope ((F.|>) x (S.fromText chunk))

-- | from "Data.Text.Lazy"
instance Textual U.Text where
  fromRope (Rope x) = U.fromChunks . fmap S.toText . toList $ x
  intoRope t = Rope (U.foldrChunks ((F.<|) . S.fromText) F.empty t)

-- | from "Data.ByteString" Strict
instance Textual B.ByteString where
  fromRope = L.toStrict . B.toLazyByteString . foldr g mempty . unRope
    where
      g piece built = (<>) (S.toBuilder piece) built

  -- If the input ByteString does not contain valid UTF-8 then an empty
  -- Rope will be returned. That's not ideal.
  intoRope b' = case S.fromByteString b' of
    Just piece -> Rope (F.singleton piece)
    Nothing -> Rope F.empty -- bad

  -- ditto
  appendRope b' (Rope x) = case S.fromByteString b' of
    Just piece -> Rope ((F.|>) x piece)
    Nothing -> (Rope x) -- bad

-- | from "Data.ByteString.Lazy"
instance Textual L.ByteString where
  fromRope = B.toLazyByteString . foldr g mempty . unRope
    where
      g piece built = (<>) (S.toBuilder piece) built

  intoRope b' = Rope (L.foldrChunks ((F.<|) . check) F.empty b')
    where
      check chunk = case S.fromByteString chunk of
        Just piece -> piece
        Nothing -> S.empty -- very bad

instance Textual Bytes where
  fromRope = intoBytes . (fromRope :: Rope -> B.ByteString)
  intoRope = intoRope . unBytes

instance Binary Rope where
  fromBytes = intoRope . unBytes
  intoBytes = intoBytes . (fromRope :: Rope -> B.ByteString)

-- |
-- If you /know/ the input bytes are valid UTF-8 encoded characters, then
-- you can use this function to convert to a piece of @Rope@.
unsafeIntoRope :: B.ByteString -> Rope
unsafeIntoRope = Rope . F.singleton . S.fromByteStringUnsafe

-- | from "Data.String"
instance Textual [Char] where
  fromRope (Rope x) = foldr h [] x
    where
      h piece string = (S.unpack piece) ++ string -- ugh
  intoRope = Rope . F.singleton . S.pack

-- |
-- Write the 'Rope' to the given 'Handle'.
--
-- @
-- import "Core.Text"
-- import "Core.System" -- re-exports stdout
--
-- main :: IO ()
-- main =
--   let
--     text :: 'Rope'
--     text = "Hello World"
--   in
--     'hWrite' 'System.IO.stdout' text
-- @
-- because it's tradition.
--
-- Uses 'Data.ByteString.Builder.hPutBuilder' internally which saves all kinds
-- of intermediate allocation and copying because we can go from the
-- 'Data.Text.Short.ShortText's in the finger tree to
-- 'Data.ByteString.Short.ShortByteString' to
-- 'Data.ByteString.Builder.Builder' to the 'System.IO.Handle''s output buffer
-- in one go.
--
-- If you're working in the
-- <https://hackage.haskell.org/package/core-program/docs/Core-Program-Execute.html#t:Program Program>
-- monad, then
-- <https://hackage.haskell.org/package/core-program/docs/Core-Program-Logging.html#v:write write>
-- provides an efficient way to write a @Rope@ to @stdout@.
hWrite :: Handle -> Rope -> IO ()
hWrite handle (Rope x) = B.hPutBuilder handle (foldr j mempty x)
  where
    j piece built = (<>) (S.toBuilder piece) built

-- |
-- Does the text contain this character?
--
-- We've used it to ask whether there are newlines present in a @Rope@, for
-- example:
--
-- @
--     if 'containsCharacter' \'\\n\' text
--         then handleComplexCase
--         else keepItSimple
-- @
containsCharacter :: Char -> Rope -> Bool
containsCharacter q (Rope x) = any j x
  where
    j piece = S.any (\c -> c == q) piece