cpython-3.8.0: lib/CPython/Simple/Instances.hs
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module CPython.Simple.Instances where
import Control.Exception (Exception(..), throwIO)
import Control.Monad ((<=<))
import Data.Text (Text)
import qualified Data.Text as T
import Data.Typeable
import qualified CPython.Constants as Py
import qualified CPython.Protocols.Object as Py
import qualified CPython.Types as Py
import qualified CPython.Types.Tuple as Py (fromTuple)
-- | `ToPy` instances indicate that a type can be marshalled from Haskell to Python automatically
--
-- For example, @ToPy Integer@ indicates that we know how to take a Haskell `Integer` and convert
-- it into a Python `int` object
class ToPy a where
-- | Takes some Haskell type, and converts it to a Python object by going over FFI
--
-- Generally you'll only need to call `toPy` manually on some type when writing your own `ToPy` instances for another type
toPy :: a -> IO Py.SomeObject
-- | `FromPy` instances indicate that a type can be marshalled from Python to Haskell automatically
--
-- For example, @FromPy Integer@ indicates that we know how to take some Python object and convert
-- it into a Haskell Integer. If the Python object is `int`, then we can cast properly. Failed casts throw a `PyCastException`
class FromPy a where
-- | Takes some Python object, and converts it to the corresponding Haskell type by going over FFI. Might throw a `PyCastException`
--
-- Generally you'll only need to call `fromPy` manually on some type when writing your own `FromPy` instances for another type
fromPy :: Py.SomeObject -> IO a
-- | An exception representing a failed cast from a Python object to Haskell value, usually because the expected type of the Python object was not correct.
--
-- Carries a `String` which represents the name of the expected Haskell type which caused a failed cast. If using `easyFromPy`, this `String` is found with `typeRep`
data PyCastException = PyCastException String
deriving (Show)
instance Exception PyCastException where
displayException (PyCastException typename) =
"FromPy could not cast to " ++ typename
-- | Helper that lets you convert a Haskell value to a Python object by providing both a Python conversion function (from the Haskell type, over FFI, to some Python Object) as well as the Haskell value
--
-- Lets you define `toPy` with just a Python conversion function
easyToPy
:: Py.Object p
=> (h -> IO p) -- ^ python to- conversion, e.g. Py.toFloat
-> h -- ^ haskell type being converted
-> IO Py.SomeObject -- ^ Python object
easyToPy convert = fmap Py.toObject . convert
-- | Helper that takes a conversion function and a Python object, and casts the Python object
-- into a Haskell value.
--
-- Lets you define `fromPy` with just a Python conversion function
--
-- We use `Proxy` to infer the type name for use in case of a failed cast. In the context of defining an instance, this type will be inferrable, so you can just provide a `Proxy` value
easyFromPy
:: (Py.Concrete p, Typeable h)
=> (p -> IO h) -- ^ python from- conversion, e.g. Py.fromFloat
-> Proxy h -- ^ proxy for the type being converted to
-> Py.SomeObject -- ^ python object to cast from
-> IO h -- ^ Haskell value
easyFromPy convert typename obj = do
casted <- Py.cast obj
case casted of
Nothing -> throwIO $ PyCastException (show $ typeRep typename)
Just x -> convert x
instance ToPy Bool where
toPy b = if b then Py.true else Py.false
instance FromPy Bool where
fromPy pyB = do
isTrue <- Py.isTrue pyB
isFalse <- Py.isFalse pyB
case (isTrue, isFalse) of
(True, False) -> pure True
(False, True) -> pure False
(False, False) -> throwIO . PyCastException . show $ typeRep (Proxy :: Proxy Bool)
(True, True) -> throwIO . PyCastException $ (show $ typeRep (Proxy :: Proxy Bool)) ++
". Python object was True and False at the same time. Should be impossible."
instance ToPy Integer where
toPy = easyToPy Py.toInteger
instance FromPy Integer where
fromPy = easyFromPy Py.fromInteger Proxy
instance ToPy Double where
toPy = easyToPy Py.toFloat
instance FromPy Double where
fromPy = easyFromPy Py.fromFloat Proxy
instance ToPy Text where
toPy = easyToPy Py.toUnicode
instance FromPy Text where
fromPy = easyFromPy Py.fromUnicode Proxy
instance ToPy Char where
toPy = easyToPy Py.toUnicode . T.singleton
instance FromPy Char where
fromPy c = T.head <$> easyFromPy Py.fromUnicode Proxy c
instance ToPy String where
toPy = easyToPy Py.toUnicode . T.pack
instance FromPy String where
fromPy s = T.unpack <$> easyFromPy Py.fromUnicode Proxy s
instance (FromPy a, FromPy b) => FromPy (a, b) where
fromPy val = do
[pyA, pyB] <- easyFromPy Py.fromTuple Proxy val
a <- fromPy pyA
b <- fromPy pyB
pure (a, b)
instance (ToPy a, ToPy b) => ToPy (a, b) where
toPy (a, b) = do
pyA <- toPy a
pyB <- toPy b
easyToPy Py.toTuple [pyA, pyB]
instance (FromPy a, FromPy b, FromPy c) => FromPy (a, b, c) where
fromPy val = do
[pyA, pyB, pyC] <- easyFromPy Py.fromTuple Proxy val
a <- fromPy pyA
b <- fromPy pyB
c <- fromPy pyC
pure (a, b, c)
instance (ToPy a, ToPy b, ToPy c) => ToPy (a, b, c) where
toPy (a, b, c) = do
pyA <- toPy a
pyB <- toPy b
pyC <- toPy c
easyToPy Py.toTuple [pyA, pyB, pyC]
instance (FromPy a, FromPy b, FromPy c, FromPy d) => FromPy (a, b, c, d) where
fromPy val = do
[pyA, pyB, pyC, pyD] <- easyFromPy Py.fromTuple Proxy val
a <- fromPy pyA
b <- fromPy pyB
c <- fromPy pyC
d <- fromPy pyD
pure (a, b, c, d)
instance (ToPy a, ToPy b, ToPy c, ToPy d) => ToPy (a, b, c, d) where
toPy (a, b, c, d) = do
pyA <- toPy a
pyB <- toPy b
pyC <- toPy c
pyD <- toPy d
easyToPy Py.toTuple [pyA, pyB, pyC, pyD]
instance FromPy a => FromPy (Maybe a) where
fromPy val = do
isNone <- Py.isNone val
if isNone
then pure Nothing
else Just <$> fromPy val
instance ToPy a => ToPy (Maybe a) where
toPy Nothing = Py.none
toPy (Just a) = toPy a
instance FromPy a => FromPy [a] where
fromPy val = do
list <- easyFromPy Py.fromList Proxy val
mapM fromPy list
instance ToPy a => ToPy [a] where
toPy val = do
list <- mapM toPy val
Py.toObject <$> Py.toList list
instance FromPy () where
fromPy _ = pure ()