{-# LANGUAGE CPP #-} {-# OPTIONS_GHC -Wno-missing-import-lists #-} {- ORMOLU_DISABLE -} -- | This module serves as the main entry point for the library. It provides -- the types and operations for typical usage and is usually the only import -- required. The core concept is: -- -- 1. Wrapping a numeric value representing bytes in a new type. -- 2. Attaching phantom labels representing the units (e.g. K, M, ...). -- -- This prevents mistakes, such as adding two different byte sizes or -- converting between sizes incorrectly. -- -- @since 0.1 module Data.Bytes ( -- * Introduction -- $intro Bytes (..), Size (..), -- * Basic Usage -- ** Construction -- $construction -- ** Unknown Size -- $size1 SomeSize, -- *** Sized -- $size2 Sized (..), -- *** Optics -- $size3 -- ** Elimination -- *** RawNumeric -- $elimination1 RawNumeric (..), -- *** HasField -- $elimination2 -- *** Optics -- $elimination3 -- * Transformations -- ** Converting Units Conversion (..), #if MIN_VERSION_base(4, 20, 0) Conv.convert, #endif -- ** Normalization Normalize (..), -- * Algebra -- $algebra module Numeric.Algebra, module Numeric.Literal.Integer, module Numeric.Literal.Rational, -- * Text -- ** Pretty Printing -- $pretty module Data.Bytes.Formatting, -- ** Parsing Parser, parse, -- * Optics -- ** Core _MkBytes, _MkSomeSize, -- ** Size _B, _K, _M, _G, _T, _P, _E, _Z, _Y, -- * Reexports Default (def), ) where {- ORMOLU_ENABLE -} import Data.Bytes.Class.Conversion (Conversion (Converted, convert_)) #if MIN_VERSION_base(4, 20, 0) import Data.Bytes.Class.Conversion qualified as Conv #endif import Data.Bytes.Class.Normalize (Normalize (Norm, normalize)) import Data.Bytes.Class.Parser (Parser, parse) import Data.Bytes.Class.RawNumeric (RawNumeric (Raw, toRaw)) import Data.Bytes.Formatting import Data.Bytes.Internal ( Bytes (MkBytes), SomeSize, _MkBytes, _MkSomeSize, ) import Data.Bytes.Size ( Size (B, E, G, K, M, P, T, Y, Z), Sized (HideSize, hideSize, sizeOf), _B, _E, _G, _K, _M, _P, _T, _Y, _Z, ) import Numeric.Algebra import Numeric.Literal.Integer import Numeric.Literal.Rational -- $intro -- The main idea is to attach phantom labels to the numeric bytes, so we -- can track the size units. This allows us to safely manipulate byte values -- without mixing up units, performing incorrect conversions, etc. -- -- The core types are a newtype wrapper 'Bytes' and the 'Size' units: -- $construction -- There are several ways to construct a 'Bytes' type. -- -- 1. 'FromInteger' -- -- >>> afromInteger 80 :: Bytes M Int -- MkBytes 80 -- -- 2. Directly -- -- >>> MkBytes 80 :: Bytes M Int -- MkBytes 80 -- -- 3. Optics (@optics-core@) -- -- >>> import Optics.Core (review) -- >>> (review _MkBytes 70) :: Bytes G Int -- MkBytes 70 -- -- >>> (review #unBytes 70) :: Bytes G Int -- MkBytes 70 -- $size1 -- We sometimes have to deal with unknown sizes at runtime, which presents -- a problem. We handle this with the @'SomeSize'@ type, which existentially -- quantifies the 'Size': -- $size2 -- Fortunately, we do not have to directly use the constructor or singletons. -- We can instead use the 'Sized' class. -- $size3 -- Once again, we can use optics for this. -- -- >>> import Optics.Core (review) -- >>> review _MkSomeSize (MkBytes 70 :: Bytes G Int) -- MkSomeSize SG (MkBytes 70) -- $elimination1 -- We provide the 'RawNumeric' class for conveniently unwrapping a type -- to the underlying numeric value. -- $elimination2 -- We can use 'GHC.Records.HasField' for this too. -- -- >>> -- {-# LANGUAGE OverloadedRecordDot #-} -- >>> let x = MkBytes 7 :: Bytes G Int -- >>> x.unBytes -- 7 -- -- >>> let y = hideSize x :: SomeSize Int -- >>> y.unSomeSize -- 7 -- $elimination3 -- Optics are another option. The underscore-prefixed optics unwrap one level -- at a time, since we can freely compose them. -- -- >>> import Optics.Core (view, (%)) -- >>> let x = MkBytes 7 :: Bytes G Int -- >>> view _MkBytes x -- 7 -- -- >>> -- notice we have to convert the numeric value since the requested -- >>> -- return type ('M') differs from the original ('G') -- >>> let y = hideSize x :: SomeSize Int -- >>> (view _MkSomeSize y) :: Bytes M Int -- MkBytes 7000 -- -- >>> view (_MkSomeSize % (_MkBytes @M)) y -- 7000 -- -- The @-XOverloadedLabel@ instances unwrap all the way to the underlying numeric -- value. -- -- >>> view #unBytes x -- 7 -- -- >>> view #unSomeSize y -- 7 -- $pretty -- We provide several formatters for pretty-printing different byte types. -- -- >>> import Data.Default (Default (def)) -- >>> let bf = MkFloatingFormatter (Just 2) -- >>> let b = MkBytes @G @Float 20.248 -- >>> formatSized bf def b -- "20.25 gb" -- $algebra -- -- The built-in 'Num' class is abandoned in favor of -- [algebra-simple](https://github.com/tbidne/algebra-simple/)'s -- algebraic hierarchy based on abstract algebra. This is motivated by a -- desire to: -- -- 1. Provide a consistent API. -- 2. Avoid 'Num'\'s infelicities (e.g. nonsense multiplication, -- dangerous 'fromInteger'). -- -- 'Bytes' and 'SomeSize' are both 'Numeric.Algebra.Additive.AGroup.AGroup's. -- A 'Numeric.Algebra.Ring.Ring' instance is not provided because -- multiplication is nonsensical: -- -- \[ -- x \;\textrm{mb} \times y \;\textrm{mb} = xy \;\textrm{mb}^2. -- \] -- -- Fortunately, multiplying bytes by some kind of scalar is both useful /and/ -- has an easy interpretation: 'Bytes' forms a 'Numeric.Algebra.Module.Module' -- over a 'Numeric.Algebra.Ring.Ring' -- (resp. 'Numeric.Algebra.VectorSpace.VectorSpace' over a -- 'Simple.Algebra.Field.Field'). This allows us to multiply a 'Bytes' or -- 'SomeSize' by a scalar in a manner consistent with the above API. -- -- == Examples -- === Addition/Subtraction -- >>> import Numeric.Algebra (ASemigroup ((.+.)), AGroup ((.-.))) -- >>> let mb1 = MkBytes 20 :: Bytes 'M Int -- >>> let mb2 = MkBytes 50 :: Bytes 'M Int -- >>> mb1 .+. mb2 -- MkBytes 70 -- >>> mb1 .-. mb2 -- MkBytes (-30) -- -- >>> let kb = MkBytes 50 :: Bytes 'K Int -- >>> -- mb1 .+. kb -- This would be a type error -- -- === Multiplication -- >>> import Numeric.Algebra (MSemiSpace ((.*))) -- >>> mb1 .* 10 -- MkBytes 200 -- -- === Division -- >>> import Numeric.Algebra (MSpace ((.%))) -- >>> mb1 .% 10 -- MkBytes 2 -- -- One may wonder how the 'Numeric.Algebra.Additive.AGroup.AGroup' instance -- for 'SomeSize' could possibly work. It is possible (indeed, expected) that -- we could have two 'SomeSize's that have different underlying 'Bytes' types. -- To handle this, the 'SomeSize' instance will convert both 'Bytes' to a -- 'Bytes' ''B' before adding/subtracting. -- -- >>> let some1 = hideSize (MkBytes 1000 :: Bytes 'G Double) -- >>> let some2 = hideSize (MkBytes 500_000 :: Bytes 'M Double) -- >>> some1 .+. some2 -- MkSomeSize SB (MkBytes 1.5e12) -- >>> some1 .-. some2 -- MkSomeSize SB (MkBytes 5.0e11) -- -- This respects 'SomeSize'\'s equivalence-class based 'Eq'.