applicative-splice-0.0.0.0: Control/Applicative/Splice.hs
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DisambiguateRecordFields #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE FlexibleContexts #-}
-- | This module defines a quasiquoter for making it easier to program with
-- applicatives and monads.
--
-- = Basic usage
--
-- The 'appl' quasiquoter takes almost any Haskell expression, and expands to the
-- same Haskell expression, wrapped in a call to 'pure'. For example,
--
-- @
-- [appl| 4 + 5 |]
-- -- expands to:
-- pure (4 + 5)
-- @
--
-- Additionally, the expression given to 'appl' may contain "splices". A splice
-- is a dollar sign immediately followed by a parenthesized expression.
-- The parentheses can be dropped if the expression consists of a single
-- identifier. Here are examples of splices.
--
-- @
-- $(f a b)
-- $([0..7])
-- $getLine
-- @
--
-- The syntax for splices are stolen from Template Haskell
-- splices. Therefore whitespaces are not allowed after the dollar sign.
--
-- The expression inside a splice should have the type @f a@ for some
-- 'Applicative' @f@ and some type @a@. The splice itself should be
-- placed in a context where a value of type @a@ is expected. Then 'appl' expands
-- to an applicative expression that "embeds" the result of the applicative
-- action in the pure context. For example,
--
-- @
-- [appl| f $x (4 + $y) |]
-- -- expands to:
-- (\\a b -> f a (4 + b)) \<$> x \<*> y
-- @
--
-- In terms of types, the dollar sign is like a function of type @forall a. f a -> a@,
-- although it is not a real function.
--
-- The type of the 'appl' expression will be in the same applicative as the
-- splices. This also means mutliple splices in the same block must
-- share the same applicative.
--
-- = Special case: functor splices
--
-- When an 'appl' block contains exactly one splice, the type of the expression
-- inside the splice can be @f a@ for any functor @f@, and it doesn't have to be
-- an applicative. The expansion will only contain a call to '<$>', not 'pure'
-- or '<*>'.
--
-- @
-- [appl| (\"Length\", length $m) |]
-- -- expands to:
-- (\\a -> (\"Length\", length a)) \<$> m
-- @
--
-- = Nested splices for monads
--
-- When your applicative is also a monad, you can have splices inside another
-- splice. For example,
--
-- @
-- [appl| $(putStrLn $ "Hello, " ++ $getLine) |]
-- -- expands to:
-- getLine >>= (\\a -> putStrLn $ "Hello, " ++ a)
-- @
--
-- As in this case, no call to '<$>' is generated if the whole 'appl' block
-- consists of a single splice.
module Control.Applicative.Splice
( appl
) where
import Control.Monad.State
import Control.Monad.Writer
import Data.Generics
import qualified Language.Haskell.Exts as H
import qualified Language.Haskell.Meta as Meta
import qualified Language.Haskell.TH as TH
import qualified Language.Haskell.TH.Quote as TH
data ApplicativeExp var exp
= AdoJoin [(var, ApplicativeExp var exp)] exp
-- ado { v0 <- join e0; v1 <- join e1; ...; return eN }
deriving (Functor)
-- | The applicative-splice quasiquoter.
appl :: TH.QuasiQuoter
appl = TH.QuasiQuoter
{ quoteExp = genExp
, quotePat = err
, quoteType = err
, quoteDec = err
}
where
err _ = fail "This quasiquoter can only be used in an expression"
genExp :: String -> TH.ExpQ
genExp str = do
e <- case H.parseExpWithMode parseMode str of
H.ParseOk e -> return e
H.ParseFailed loc msg -> fail $ show loc ++ ": " ++ msg
unApplicative $ bimapApplicativeExp Meta.toName Meta.toExp $ quote e
where
parseMode = H.defaultParseMode
{ H.extensions = map H.EnableExtension [H.TemplateHaskell]
}
bimapApplicativeExp
:: (a -> c)
-> (b -> d)
-> ApplicativeExp a b
-> ApplicativeExp c d
bimapApplicativeExp f g = go
where
go (AdoJoin binds body) = AdoJoin (map goBind binds) (g body)
goBind (var, app) = (f var, go app)
quote :: H.Exp -> ApplicativeExp H.Name H.Exp
quote e0 = evalState (go e0) 0
where
go e = do
(body, binds) <- runWriterT (everywhereM' (mkM onSplice) e)
return $ AdoJoin binds body
onSplice (H.SpliceExp splice) = do
v <- gensym
e' <- lift $ go e
tell [(v, e')]
return $ H.Var $ H.UnQual v
where
!e = case splice of
H.IdSplice str -> H.Var $ H.UnQual $ H.Ident str
H.ParenSplice x -> x
onSplice e = return e
gensym = do
n <- get
put $! n + 1
return $ argVar n
argVar :: Int -> H.Name
argVar = H.Ident . ("applicative_splice_" ++) . show
unApplicative :: ApplicativeExp TH.Name TH.Exp -> TH.ExpQ
unApplicative (AdoJoin (unzip -> (vars, args)) body) = case args of
[] -> [| pure $(pure body) |]
first:rest -> foldl mkAp (join $ mkFmap <$> fun <*> unJoin first) rest
where
fun = TH.lamE (map TH.varP vars) (pure body)
mkAp f a = [| $f <*> $(unJoin a) |]
mkFmap f a
| isTrivialIdentity f = pure a
| otherwise = [| $(pure f) <$> $(pure a) |]
isTrivialIdentity (TH.LamE [TH.VarP v] e) = e == TH.VarE v
isTrivialIdentity _ = False
unJoin :: ApplicativeExp TH.Name TH.Exp -> TH.ExpQ
unJoin (AdoJoin binds body) = foldr bind (pure body) binds
where
bind (v, a) b = [| $(unJoin a) >>= \ $(TH.varP v) -> $b |]
everywhereM' :: (Monad m) => GenericM m -> GenericM m
everywhereM' f x = do
x' <- f x
gmapM (everywhereM' f) x'
_view :: TH.ExpQ -> TH.ExpQ
_view eq = do
e <- eq
let str = TH.pprint e
[| putStrLn str |]