packages feed

autoapply-0.1.0.0: src/AutoApply.hs

module AutoApply
  ( autoapply
  , autoapplyDecs
  ) where

import           Control.Applicative
import           Control.Arrow                  ( (>>>) )
import           Control.Monad
import           Control.Monad.Logic            ( LogicT
                                                , observeManyT
                                                )
import           Control.Monad.Logic.Class      ( ifte )
import           Control.Monad.Trans           as T
import           Control.Monad.Trans.Except
import           Control.Unification
import           Control.Unification.IntVar
import           Control.Unification.Types
import           Data.Foldable
import           Data.Functor
import           Data.Functor.Fixedpoint
import           Data.Maybe
import           Data.Traversable
import           Language.Haskell.TH
import           Language.Haskell.TH.Desugar

-- | @autoapply args fun@ creates an expression which is equal to @fun@ applied
-- to as many of the values in @args@ as possible.
autoapply :: [Name] -> Name -> Q Exp
autoapply givens fun = do
  givenInfos <- for givens $ fmap (uncurry Given) . reifyVal "Argument"
  funInfo    <- uncurry Function <$> reifyVal "Function" fun
  autoapply1 givenInfos funInfo

-- | @autoapplyDecs mkName args funs@ will wrap every function in @funs@ by
-- applying it to as many of the values in @args@ as possible. The new function
-- name will be @mkName@ applied to the wrapped function name.
--
-- Type signatures are not generated, so you may want to add these yourself or
-- turn on @NoMonomorphismRestriction@ if you have polymorphic constraints.
autoapplyDecs :: (String -> String) -> [Name] -> [Name] -> Q [Dec]
autoapplyDecs getNewName givens funs = do
  givenInfos <- for givens $ fmap (uncurry Given) . reifyVal "Argument"
  funInfos   <- for funs $ fmap (uncurry Function) . reifyVal "Function"
  let mkFun fun = do
        exp' <- autoapply1 givenInfos fun
        pure $ FunD (mkName . getNewName . nameBase . fName $ fun)
                    [Clause [] (NormalB exp') []]
  traverse mkFun funInfos

-- | A given is something we can try to pass as an argument
data Given = Given
  { gName :: Name
  , gType :: DType
  }
  deriving (Show)

-- | A function we are wrapping
data Function = Function
  { fName :: Name
  , fType :: DType
  }
  deriving (Show)

autoapply1 :: [Given] -> Function -> Q Exp
autoapply1 givens fun = do
  -- In this function we:
  --
  -- - Instantiate the command type with new unification variables
  -- - Split it into arguments and return type
  -- - Try to unify it with every 'Given' at every argument
  --   - If we can unify the monad of the 'Given' with that of the functions and
  --     unify the argument type, use that.
  --   - If nothing matches we just use an 'Argument'
  -- - Take the first result of all these tries

  let (fmap varBndrName -> cmdVars, _preds, args, ret) = unravel (fType fun)
      defaultMaybe m = ifte m (pure . Just) (pure Nothing)
      liftQ :: Q a -> IntBindingT TypeF (LogicT Q) a
      liftQ = T.lift . T.lift

      -- Use LogicT so we can backtrack on failure
      genProvs :: LogicT Q [ArgProvenance]
      genProvs = evalIntBindingT $ do
        instArgs <- traverse (inst cmdVars . snd <=< liftQ . typeDtoF) args

        -- This is @Just (m, a)@ when m is Applicative
        retMonad <- case ret of
          DAppT m a -> liftQ (isInstance ''Applicative [sweeten m]) >>= \case
            False -> pure Nothing
            True  -> do
              m' <- inst cmdVars . snd <=< liftQ . typeDtoF $ m
              a' <- inst cmdVars . snd <=< liftQ . typeDtoF $ a
              pure $ Just (m', a')
          _ -> pure Nothing

        -- A list of (type to unify, predicate to use this match, the given
        -- providing the value).
        --
        -- The predicate is there to make sure we only match unifiable monads
        instGivens <- fmap concat . for givens $ \g@Given {..} -> do
          -- The Given applied as is
          nonApp <- do
            instTy <- uncurry inst <=< liftQ . typeDtoF $ gType
            v      <- liftQ $ newName "g"
            pure (instTy, pure (), BoundPure v g)
          -- The given, but in an applicative context, only possible if we can
          -- unify the monad and there is a Monad instance
          app <- case stripForall gType of
            (vars, DAppT m a) | Just (cmdM, _) <- retMonad ->
              liftQ (isInstance ''Applicative [sweeten m]) >>= \case
                False -> pure Nothing
                True  -> do
                  m' <- inst vars . snd <=< liftQ . typeDtoF $ m
                  a' <- inst vars . snd <=< liftQ . typeDtoF $ a
                  v  <- liftQ $ newName "g"
                  let predicate = do
                        _ <- unify m' cmdM
                        pure ()
                  pure $ Just (a', predicate, Bound v g)
            _ -> pure Nothing
          pure ([nonApp] <> toList app)

        as <- for instArgs $ \argTy ->
          defaultMaybe . asum $ instGivens <&> \(givenTy, predicate, g) ->
            runExceptT
                (do
                  predicate
                  freshGivenTy <- freshen givenTy
                  unify freshGivenTy argTy
                )
              >>= \case
                    Left  (_ :: UFailure TypeF IntVar) -> empty
                    Right _                            -> pure g
        for (zip args as) $ \case
          (_, Just p ) -> pure p
          (t, Nothing) -> (`Argument` t) <$> liftQ (newName "a")

  argProvenances <-
    note "\"Impossible\" Finding argument provenances failed"
    .   listToMaybe
    =<< observeManyT 1 genProvs
  unless (length argProvenances == length args) $ fail
    "\"Impossible\", incorrect number of argument provenances were found"

  let bindGiven = \case
        BoundPure _ _ -> Nothing
        Bound     n g -> Just $ BindS (VarP n) (VarE (gName g))
        Argument  _ _ -> Nothing
      bs   = catMaybes (bindGiven <$> argProvenances)
      ret' = applyDExp
        (DVarE (fName fun))
        (argProvenances <&> \case
          Bound     n _           -> DVarE n
          BoundPure _ (Given n _) -> DVarE n
          Argument  n _           -> DVarE n
        )
  exp' <- dsDoStmts (bs <> [NoBindS (sweeten ret')])

  -- Typing the arguments here is important, if we don't then some skolems
  -- might escape!
  --
  -- Consider wrapping @f :: (forall a. a) -> ()@ (and supplying no arguments).
  -- We end up with the splice @myF x = f x@, and the @a@ in the argument to
  -- @f@ escapes. We can fix this by typing the pattern explicitly, thusly @myF
  -- (x :: forall a. a) = f x@
  pure $ LamE [ SigP (VarP n) (sweeten t) | Argument n t <- argProvenances ]
              (sweeten exp')

data ArgProvenance
  = Bound Name Given
    -- ^ Comes from a monadic binding
  | BoundPure Name Given
    -- ^ Comes from a pure binding, i.e. let ... in
  | Argument Name DType
    -- ^ Comes from an argument to the wrapped function
  deriving (Show)

----------------------------------------------------------------
-- Haskell types as a fixed point of TypeF
----------------------------------------------------------------

data TypeF a
  = AppF a a
  | VarF Name
  | ConF Name
  | ArrowF
  | LitF TyLit
  deriving (Show, Functor, Foldable, Traversable)

-- TODO: Derive this with generics
instance Unifiable TypeF where
  zipMatch (AppF l1 r1) (AppF l2 r2) =
    Just (AppF (Right (l1, l2)) (Right (r1, r2)))
  zipMatch (VarF n1) (VarF n2) | n1 == n2 = Just (VarF n1)
  zipMatch (ConF n1) (ConF n2) | n1 == n2 = Just (ConF n1)
  zipMatch ArrowF ArrowF                  = Just ArrowF
  zipMatch (LitF l1) (LitF l2) | l1 == l2 = Just (LitF l1)
  zipMatch _ _                            = Nothing

-- | Returns the type as a @Fix TypeF@ along with any quantified names. Drops
-- any context.
typeDtoF :: MonadFail m => DType -> m ([Name], Fix TypeF)
typeDtoF = traverse go . stripForall
 where
  go = \case
    DForallT{} -> fail "TODO: Higher ranked types"
    DAppT l r  -> do
      l' <- go l
      r' <- go r
      pure $ Fix (AppF l' r')
    DAppKindT t _ -> go t
    DSigT     t _ -> go t
    DVarT n       -> pure . Fix $ VarF n
    DConT n       -> pure . Fix $ ConF n
    DArrowT       -> pure . Fix $ ArrowF
    DLitT l       -> pure . Fix $ LitF l
    DWildCardT    -> fail "TODO: Wildcards"

varBndrName :: DTyVarBndr -> Name
varBndrName = \case
  DPlainTV n    -> n
  DKindedTV n _ -> n

-- | Raise foralls on the spine of the function type to the top
--
-- For example @forall a. a -> forall b. b@ becomes @forall a b. a -> b@
raiseForalls :: DType -> DType
raiseForalls = uncurry3 DForallT . go
 where
  go = \case
    DForallT vs ctx t ->
      let (vs', ctx', t') = go t in (vs <> vs', ctx <> ctx', t')
    l :~> r -> let (vs, ctx, r') = go r in (vs, ctx, l :~> r')
    t       -> ([], [], t)

pattern (:~>) :: DType -> DType -> DType
pattern l :~> r = DArrowT `DAppT` l `DAppT` r

-- | Instantiate a type with unification variables
inst
  :: BindingMonad TypeF IntVar m
  => [Name]
  -> Fix TypeF
  -> m (UTerm TypeF IntVar)
inst ns t = do
  vs <- sequence [ (n, ) <$> freeVar | n <- ns ]
  let go (Fix f) = case f of
        AppF l r                       -> UTerm (AppF (go l) (go r))
        VarF n | Just v <- lookup n vs -> UVar v
        VarF n                         -> UTerm (VarF n)
        ConF n                         -> UTerm (ConF n)
        ArrowF                         -> UTerm ArrowF
        LitF l                         -> UTerm (LitF l)
  pure $ go t

----------------------------------------------------------------
-- Utils
----------------------------------------------------------------

reifyVal :: String -> Name -> Q (Name, DType)
reifyVal d n = dsReify n >>= \case
  Just (DVarI name ty _) -> pure (name, ty)
  _                      -> fail $ d <> " " <> show n <> " isn't a value"

stripForall :: DType -> ([Name], DType)
stripForall = raiseForalls >>> \case
  DForallT vs _ ty -> (varBndrName <$> vs, ty)
  ty               -> ([], ty)

uncurry3 :: (a -> b -> c -> d) -> (a, b, c) -> d
uncurry3 f (a, b, c) = f a b c

note :: MonadFail m => String -> Maybe a -> m a
note s = maybe (fail s) pure