packages feed

hasquant-0.5.0.2: QuantLib/Syntax.hs

{-# LANGUAGE RankNTypes, ScopedTypeVariables #-}
module QuantLib.Syntax
  (
    free1st
  , free2nd
  , free1st'
  , free2nd'
  , freeNth
  , freeNth'
  , cutAt
  , cutAt'
  , cut
  )
where

import Control.Monad(replicateM)
import Control.Monad.Trans.Class(lift)
import Control.Monad.Trans.State.Strict(StateT, get, modify', runStateT)
import Data.Data(Data, gmapM, cast)
import Data.List(isPrefixOf, nub)
import Language.Haskell.TH

-- |make a function with the first argument put at the last position, sort of any arity flip to
-- make it easier to chain monadic calls
--
-- > -- advance :: Calendar -> Date -> (Int, TimeUnit) -> BusinessDayConvention -> Bool -> IO Date
-- > calendar Null >>= $(free1st 'advance) d (3, Months) Following False
free1st :: Name -> ExpQ
free1st = freeNth 1

-- |same as 'free1st', but frees the second argument instead of the first
--
-- > -- blackConstantVol :: Calendar -> Date -> Quote -> DayCounter -> IO BlackVolTermStructure
-- > calendar TARGET >>= $(free2nd 'blackConstantVol) settl volQ dc
free2nd :: Name -> ExpQ
free2nd = freeNth 2

-- |like 'free1st', but for a target 'reify' can't see — a local @where@\/@let@ binding, or an
-- expression that isn't a name at all (a lambda, a section, an operator) — so the arity is
-- supplied explicitly and the target becomes the generated function's first argument instead.
-- Top-level bindings and typeclass methods don't need this; 'free1st' handles both.
--
-- > -- parRate is a local where-binding here, so 'parRate wouldn't reify
-- > forM curves $ $(free1st' 3) parRate (bondSettle :| ds) dc
free1st' :: Int -> ExpQ
free1st' = freeNth' 1

-- |same as 'free1st'', but frees the second argument instead of the first
free2nd' :: Int -> ExpQ
free2nd' = freeNth' 2

-- |the general form behind 'free1st'\/'free2nd': frees the @i@-th (1-based) argument of a
-- reified function, moving it to the trailing position
--
-- > $(freeNth 3 'f) a1 a2 a4 a3  ==  f a1 a2 a3 a4
freeNth :: Int -> Name -> ExpQ
freeNth i = cutAt [i]

-- |the general form behind 'free1st''\/'free2nd'': like 'freeNth', but with a user-supplied
-- arity instead of one discovered via 'reify', and taking the target as its first argument
-- (see 'free1st'')
freeNth' :: Int -> Int -> ExpQ
freeNth' i = cutAt' [i]

-- |number of arguments a reified signature takes
arity :: Type -> Q Int
arity as = go as
  where
    go (AppT (AppT ArrowT _) x) = (1 +) <$> go x
    go (ForallT _ _ a) = go a
    go (AppT _ _) = return 0
    go (ConT _) = return 0
    go (VarT _) = return 0
    go x = fail $ "QuantLib.Syntax: unsupported signature part: " ++ show x
                  ++ ", full signature: " ++ show as

-- ClassOpI covers typeclass methods: their reified type carries the class context as a
-- ForallT, which 'arity' skips over, so those work just as well as plain bindings here
reifiedArity :: Name -> Q Int
reifiedArity n = do
  info <- reify n
  case info of
    VarI _ as _ -> arity as
    ClassOpI _ as _ -> arity as
    _ -> fail $ "QuantLib.Syntax: " ++ show n ++ " is not a function binding, reified as: "
                ++ show info

checkIndices :: [Int] -> Int -> Q ()
checkIndices is an
  | null is = fail "QuantLib.Syntax: no argument positions given"
  | length (nub is) /= length is = fail $ "QuantLib.Syntax: duplicate argument positions in " ++ show is
  | any (\i -> i < 1 || i > an) is = fail $ "QuantLib.Syntax: position(s) out of range [1," ++ show an ++ "]: " ++ show is
  | otherwise = return ()

genCutAt :: [Int] -> Int -> Name -> ExpQ
genCutAt is an fn = do
  checkIndices is an
  vars <- replicateM an (newName "x")
  let idxVars = zip [1..] vars
      free = [v | (i, v) <- idxVars, i `elem` is]
      bound = [v | (i, v) <- idxVars, i `notElem` is]
  return $ LamE (map VarP (bound ++ free)) (foldl AppE (VarE fn) (map VarE vars))

-- |generalizes 'free1st'\/'free2nd'\/'freeNth': frees an arbitrary subset of argument positions
-- (by 1-based index) instead of a single hardcoded one, moving them to the trailing position in
-- their original relative order
--
-- > -- f :: A -> B -> C -> D -> R
-- > $(cutAt [1,3] 'f) b d a c  ==  f a b c d
cutAt :: [Int] -> Name -> ExpQ
cutAt is n = reifiedArity n >>= \an -> genCutAt is an n

-- |like 'cutAt', but with a user-supplied arity instead of one discovered via 'reify', for a
-- target 'reify' can't see (same reason as 'free1st''); the target becomes the generated
-- function's first argument
cutAt' :: [Int] -> Int -> ExpQ
cutAt' is an = do
  n <- newName "f"
  LamE [VarP n] <$> genCutAt is an n

isHole :: Name -> Bool
isHole n = "_" `isPrefixOf` nameBase n

-- the lambda parameter each hole seen so far introduced, accumulated in reverse order of first
-- occurrence (see 'cut'). An anonymous @_@ is keyed by Nothing and never shares; a named @_x@
-- is keyed by its own name, so a later @_x@ reuses the parameter rather than adding one.
type HoleQ = StateT [(Maybe String, Name)] Q

holeParam :: Name -> HoleQ Name
holeParam n
  | anonymous = fresh Nothing
  | otherwise = do
      seen <- get
      maybe (fresh (Just (nameBase n))) return (lookup (Just (nameBase n)) seen)
  where
    anonymous = nameBase n == "_"
    fresh k = do
      v <- lift (newName "h")
      modify' ((k, v) :)
      return v

-- a hole appearing inside a *nested* quotation bracket within the cut'd expression would also
-- get replaced, since this doesn't track quotation depth -- not expected to matter for any call
-- site in this codebase, so not worth the extra bookkeeping
replaceHoles :: Exp -> HoleQ Exp
replaceHoles = go
  where
    go :: Exp -> HoleQ Exp
    go (UnboundVarE n) | isHole n = VarE <$> holeParam n
    go e = gmapM step e
      where
        step :: forall d. Data d => d -> HoleQ d
        step x = case cast x of
          Just (ex :: Exp) -> do
            ex' <- go ex
            case cast ex' of
              Just r -> return r
              Nothing -> fail "QuantLib.Syntax.cut: impossible cast"
          Nothing -> gmapM step x

-- |'cut'-style partial application via placeholders (in the spirit of SRFI's @cut@): mark the
-- argument(s) to leave free with a bare @_@ or a named hole (@_x@) -- GHC's own typed-hole
-- syntax, valid in any expression position -- inside a quoted expression. Each hole becomes a
-- trailing lambda parameter, ordered by first occurrence, left to right. Repeating a named hole
-- reuses the parameter its first occurrence introduced, so the same argument can be fed to
-- several positions at once; a bare @_@ is anonymous and always gets a parameter of its own.
-- Unlike 'freeNth'\/'cutAt', this never reifies the target, so it works equally well on
-- ordinary functions, typeclass methods, local bindings, and operators.
--
-- > $(cut [| advance _ (3, Months) Following False |]) `fmap` calendar Null
-- > -- one argument fed to two positions:
-- > $(cut [| f _x 2 _x |]) 1  ==  f 1 2 1
cut :: ExpQ -> ExpQ
cut eq = do
  e <- eq
  (e', holes) <- runStateT (replaceHoles e) []
  return $ LamE (map (VarP . snd) (reverse holes)) e'

-- vim: set ft=haskell ff=unix ts=8 sts=2 sw=2 et: