morley-prelude-0.5.1: src/Morley/Prelude/FromIntegral.hs
-- SPDX-FileCopyrightText: 2021 Oxhead Alpha
-- SPDX-License-Identifier: LicenseRef-MIT-OA
{-# LANGUAGE NoImplicitPrelude #-}
-- | Safe(r) converters from @Integral@ types
module Morley.Prelude.FromIntegral
( IntBaseType
, CheckIntSubType
, fromIntegral
, fromIntegralMaybe
, fromIntegralNoOverflow
, fromIntegralOverflowing
, fromIntegralToRealFrac
) where
import Control.Exception (ArithException(..))
import Data.Bits (Bits)
import Data.IntCast (IntBaseType, IsIntSubType, intCast, intCastMaybe)
import Data.Ratio ((%))
import GHC.TypeLits (ErrorMessage(..), TypeError)
import System.IO.Unsafe (unsafePerformIO)
import Universum hiding (fromInteger, fromIntegral)
import Universum qualified (fromIntegral)
-- | Constraint synonym equivalent to @'IsIntSubType' a b ~ 'True@, but with
-- better error messages
type CheckIntSubType a b =
( CheckIntSubTypeErrors a b (IsIntSubType a b)
, IsIntSubType a b ~ 'True )
type family CheckIntSubTypeErrors a b (z :: Bool) :: Constraint where
CheckIntSubTypeErrors _ _ 'True = ()
CheckIntSubTypeErrors a b 'False =
TypeError ('Text "Can not safely cast '"
':<>: 'ShowType a ':<>: 'Text "' to '" ':<>: 'ShowType b ':<>: 'Text "':"
':$$: 'Text "'" ':<>: 'ShowType a ':<>: 'Text "' is not a subtype of '" ':<>: 'ShowType b
':<>: 'Text "'"
)
{- | Statically safe converter between 'Integral'
types, which is just 'intCast' under the hood.
It is used to turn the value of type @a@ into
the value of type @b@ such that @a@ is subtype
of @b@. It is needed to prevent silent unsafe
conversions.
>>> fromIntegral @Int @Word 1
...
... error:
... Can not safely cast 'Int' to 'Word':
... 'Int' is not a subtype of 'Word'
...
>>> fromIntegral @Word @Natural 1
1
-}
fromIntegral :: (Integral a, Integral b, CheckIntSubType a b) => a -> b
fromIntegral = intCast
-- | Statically safe converter between 'Integral'
-- types, which is just 'intCastMaybe' under the
-- hood. Unlike 'fromIntegral' accept any @a@ and
-- @b@. Return @Just value@ if conversion is
-- possible at runtime and @Nothing@ otherwise.
fromIntegralMaybe :: (Integral a, Integral b, Bits a, Bits b) => a -> Maybe b
fromIntegralMaybe = intCastMaybe
-- | Statically safe converter between 'Integral'
-- and 'RealFrac' types. Could be applied to cast
-- common types like @Float@, @Double@ and @Scientific@.
--
-- It is primarily needed to replace usages of
-- 'Unsafe.fromIntegral', which are safe actually
-- as integral numbers are being casted to
-- fractional ones.
fromIntegralToRealFrac :: (Integral a, RealFrac b, CheckIntSubType a Integer) => a -> b
fromIntegralToRealFrac = fromRational . (% 1) . fromIntegral
{- | Runtime-safe converter between 'Integral' types, which is just
'Universum.fromIntegral' under the hood.
It is needed to semantically distinguish usages, where overflow is intended,
from those that have to fail on overflow. E.g. @Int8 -> Word8@ with intended
bits reinterpretation from lossy @Integer -> Int@.
>>> fromIntegralOverflowing @Int8 @Word8 (-1)
255
>>> fromIntegralOverflowing @Natural @Int8 450
-62
Please note that like @fromIntegral@ from @base@, this will throw on some
conversions!
>>> fromIntegralOverflowing @Int @Natural (-1)
*** Exception: arithmetic underflow
See 'fromIntegralNoOverflow' for an alternative that doesn't throw.
-}
fromIntegralOverflowing :: (Integral a, Num b) => a -> b
fromIntegralOverflowing = Universum.fromIntegral
{- | Statically safe converter between 'Integral' types
checking for overflow/underflow. Returns @Right value@ if conversion does not
produce overflow/underflow and @Left ArithException@ with corresponding
'ArithException' (@Overflow@/@Underflow@) otherwise.
Note the function is strict in its argument.
>>> fromIntegralNoOverflow @Int @Word 123
Right 123
>>> fromIntegralNoOverflow @Int @Word (-123)
Left arithmetic underflow
>>> fromIntegralNoOverflow @Int @Integer (-123)
Right (-123)
>>> fromIntegralNoOverflow @Int @Natural (-123)
Left arithmetic underflow
>>> fromIntegralNoOverflow @Int @Int8 127
Right 127
>>> fromIntegralNoOverflow @Int @Int8 128
Left arithmetic overflow
-}
fromIntegralNoOverflow :: (Integral a, Integral b) => a -> Either ArithException b
fromIntegralNoOverflow !a = do
b <- tryFromIntegral a
case compare (toInteger a) (toInteger b) of
EQ -> Right b
LT -> Left Underflow
GT -> Left Overflow
where
tryFromIntegral x = unsafePerformIO $
(let !y = Universum.fromIntegral x in pure (Right y))
`catch` \case
Overflow -> pure $ Left Overflow
Underflow -> pure $ Left Underflow
e -> throwM e