{-# LANGUAGE RankNTypes, LambdaCase #-}
{-| see 'reifyFunctionAtM'.

@-- doctest@

>>> :set +m

-}
module Enumerate.Function.Reify where
import Enumerate.Types
import Enumerate.Function.Types
import Enumerate.Function.Extra

import Control.Monad.Catch (MonadThrow(..), SomeException(..))
import Control.DeepSeq (NFData)

import Control.Arrow ((&&&))


{- | reify a total function.

@

>>> reifyFunction not  -- Prelude 'not'
[(False,True),(True,False)]

@

-}
reifyFunction :: (Enumerable a) => (a -> b) -> [(a,b)]
reifyFunction f = reifyFunctionM (return . f)
{-# INLINABLE reifyFunction #-}

-- | reify a total function at any subset of the domain.
reifyFunctionAt :: [a] -> (a -> b) -> [(a,b)]
reifyFunctionAt domain f = reifyFunctionAtM domain (return . f)
{-# INLINABLE reifyFunctionAt #-}

-- | reify a (safely-)partial function into a map (which is implicitly partial, where @Map.lookup@ is like @($)@.
reifyFunctionM :: (Enumerable a) => (forall m. MonadThrow m => a -> m b) -> [(a,b)]
reifyFunctionM = reifyFunctionAtM enumerated
{-# INLINABLE reifyFunctionM #-}

{- | reify a (safely-)partial function at any domain.

use the functions suffixed with @M@ when your function is explicitly partial,
i.e. of type @(forall m. MonadThrow m => a -> m b)@.
when inside a function arrow, like:

@
reifyFunctionAtM :: [a] -> (forall m. MonadThrow m => a -> m b) -> [(a,b)]
reifyFunctionAtM domain f = ...
@

the @Rank2@ type (and non-concrete types) means that @f@ can only use
parametric polymorphic functions, or the methods of the @MonadThrow@ class
(namely 'throwM'), or methods of @MonadThrow@ superclasses (namely 'return', et cetera).

'MonadThrow' is a class from the @exceptions@ package that generalizes failibility.
it has instances for @Maybe@, @Either@, @[]@, @IO@, and more.

use the functions suffixed with @At@ when your domain isn't 'Enumerable',
or when you want to restrict the domain.

the most general function in this module.

>>> :{
let uppercasePartial :: (MonadThrow m) => Char -> m Char
    uppercasePartial c = case c of
     'a' -> return 'A'
     'b' -> return 'B'
     'z' -> return 'Z'
     _   -> failed "uppercasePartial"
:}

@

>>> reifyFunctionAtM ['a'..'c'] uppercasePartial
[('a','A'),('b','B')]

@

if your function doesn't fail under 'MonadThrow', see:

* 'reifyFunctionAtMaybe'
* 'reifyFunctionAtList'
* 'reifyFunctionAtEither'

-}
reifyFunctionAtM :: [a] -> (Partial a b) -> [(a,b)]
-- reifyFunctionAtM :: (MonadThrow m) => [a] -> (a -> m b) -> m (Map a b)
reifyFunctionAtM domain f
 = concatMap (bitraverse pure id)
 . fmap (id &&& f)
 $ domain
 where
 bitraverse g h (x,y) = (,) <$> g x <*> h y  -- avoid bifunctors dependency

-- | @reifyPredicateAt = 'flip' 'filter'@
reifyPredicateAt :: [a] -> (a -> Bool) -> [a]
reifyPredicateAt = flip filter
-- reifyPredicateAtM domain p = map fst (reifyFunctionAtM domain f)
--  where
--  f x = if p x then return x else throwM (ErrorCall "False")

-- MonadThrow Maybe
-- (e ~ SomeException) => MonadThrow (Either e)
-- MonadThrow []

-- | reify a (safely-)partial function that fails specifically under @Maybe@.
reifyFunctionMaybeAt :: [a] -> (a -> Maybe b) -> [(a, b)]
reifyFunctionMaybeAt domain f = reifyFunctionAtM domain (maybe2throw f)
{-# INLINABLE reifyFunctionMaybeAt #-}

-- | reify a (safely-)partial function that fails specifically under @[]@.
reifyFunctionListAt :: [a] -> (a -> [b]) -> [(a, b)]
reifyFunctionListAt domain f = reifyFunctionAtM domain (list2throw f)
{-# INLINABLE reifyFunctionListAt #-}

-- | reify a (safely-)partial function that fails specifically under @Either SomeException@.
reifyFunctionEitherAt :: [a] -> (a -> Either SomeException b) -> [(a, b)]
reifyFunctionEitherAt domain f = reifyFunctionAtM domain (either2throw f)
{-# INLINABLE reifyFunctionEitherAt #-}

{-| reifies an *unsafely*-partial function (i.e. a function that throws exceptions or that has inexhaustive pattern matching).

forces the function to be strict.

>>> import Data.Ratio (Ratio)

>>> fmap (1/) [0..3 :: Ratio Integer]
[*** Exception: Ratio has zero denominator

>>> let (1/) = reciprocal
>>> reifyFunctionSpoonAt [0..3 :: Ratio Integer] reciprocal
[(1 % 1,1 % 1),(2 % 1,1 % 2),(3 % 1,1 % 3)]

normal caveats from violating purity (via @unsafePerformIO@) and from catchalls (via @(e :: SomeExceptions -> _)@) apply.

-}
reifyFunctionSpoonAt :: (NFData b) => [a] -> (a -> b) -> [(a, b)]
reifyFunctionSpoonAt domain f = reifyFunctionMaybeAt domain (totalizeFunction f)

-- | reify a binary total function
reifyFunction2 :: (Enumerable a, Enumerable b) => (a -> b -> c) -> [(a,[(b,c)])]
reifyFunction2 f = reifyFunction2At enumerated enumerated f
{-# INLINABLE reifyFunction2 #-}

-- | reify a binary total function at some domain
reifyFunction2At :: [a] -> [b] -> (a -> b -> c) -> [(a,[(b,c)])]
reifyFunction2At as bs f = reifyFunction2AtM as bs (\x y -> pure (f x y))
{-# INLINABLE reifyFunction2At #-}

-- | reify a binary (safely-)partial function
reifyFunction2M :: (Enumerable a, Enumerable b) => (forall m. MonadThrow m => a -> b -> m c) -> [(a,[(b,c)])]
reifyFunction2M f = reifyFunction2AtM enumerated enumerated f
{-# INLINABLE reifyFunction2M #-}

-- | reify a binary (safely-)partial function at some domain
reifyFunction2AtM :: [a] -> [b] -> (forall m. MonadThrow m => a -> b -> m c) -> [(a,[(b,c)])]
reifyFunction2AtM as bs f = reifyFunctionAt as (\a -> reifyFunctionAtM bs (f a))