Skip to content

Commit

Permalink
Improve documentation, and add more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Z-snails committed Jan 21, 2023
1 parent 6130fc4 commit 601d865
Show file tree
Hide file tree
Showing 4 changed files with 289 additions and 141 deletions.
99 changes: 71 additions & 28 deletions src/Multiplate.idr
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
||| Multiplate allows for traversals over mutually recursive data types,
||| while removing a lot of the boilerplate.
|||
||| After writting some initial boilerplate, you can write much shorter
||| and clearer transformations, as you don't need to manually recurse on each subterm.
module Multiplate

import Control.Monad.Identity
Expand All @@ -16,6 +21,10 @@ Projector p a = forall f. p f -> a -> f a
||| Additionally new plates can be built from a function
||| which is generic over the type of nodes.
|||
||| This works fine with indexed data types -
||| see `tests/Multiplate/Tests/DeBruijn.idr` for an expression using De Bruijn indexes.
||| You may need to beta-expand in the definition of `mkPlate` (ie change `build expr` to `build (\p => expr p)`)
|||
||| @ p a plate parametised by an applicative functor
public export
interface Multiplate (0 p : (Type -> Type) -> Type) where
Expand All @@ -29,23 +38,23 @@ interface Multiplate (0 p : (Type -> Type) -> Type) where
mkPlate : Applicative f => (forall a. Projector p a -> a -> f a) -> p f

||| A plate which 'does nothing' ie applies `pure` to each node.
public export total
public export %inline
purePlate : Multiplate p => Applicative f => p f
purePlate = mkPlate (\_ => pure)

||| Apply a natural transform to a plate
public export
public export %inline
applyNaturalTransform : Multiplate p => Applicative g => (forall a. f a -> g a) -> p f -> p g
applyNaturalTransform f p = mkPlate $ \proj, x => f $ proj p x

public export
public export %inline
fromIdentity : Multiplate p => Applicative f => p Identity -> p f
fromIdentity = applyNaturalTransform (pure . runIdentity)

infixl 5 `andThenM`

||| Compose 2 plates, by applying them from left to right.
public export
public export %inline
andThenM : Monad m => Multiplate p => Lazy (p m) -> Lazy (p m) -> p m
andThenM p1 p2 = mkPlate $ \proj, x => do
x' <- proj p1 x
Expand All @@ -54,14 +63,14 @@ andThenM p1 p2 = mkPlate $ \proj, x => do
infixl 5 `andThenId`

||| Compose 2 plates, where the second is based on the identity functor
public export
public export %inline
andThenId : Multiplate p => Applicative m => Lazy (p m) -> Lazy (p Identity) -> p m
andThenId p1 p2 = mkPlate $ \proj, x => proj p1 x <&> \x' => runIdentity (proj p2 x')

infixl 5 `iAndThen`

||| Compose 2 plates, where the first is based on the identity functor
public export
public export %inline
idAndThen : Multiplate p => Applicative m => Lazy (p Identity) -> Lazy (p m) -> p m
idAndThen p1 p2 = mkPlate $ \proj, x =>
let x' = runIdentity $ proj p1 x
Expand All @@ -70,22 +79,29 @@ idAndThen p1 p2 = mkPlate $ \proj, x =>
infixr 4 `orElse`

||| Compose 2 plates, by trying the first and then the second
public export
public export %inline
orElse : Alternative m => Multiplate p => Lazy (p m) -> Lazy (p m) -> p m
orElse p1 p2 = mkPlate $ \proj, x => proj p1 x <|> proj p2 x

||| Apply a transformation to the whole family of a node.
||| This happens in a pre-order, ie children are mapped before parents.
||| This happens in a post-order, ie children are mapped before parents.
public export covering
mapFamily : Multiplate p => Monad m => p m -> p m
mapFamily p = multiplate (mapFamily p) `andThenM` p
postorderMap : Multiplate p => Monad m => p m -> p m
postorderMap p = multiplate (postorderMap p) `andThenM` p

||| Apply a transformation to the whole family of a node.
||| This happens in a pre-order, ie parents are mapped before children.
public export covering
preorderMap : Multiplate p => Monad m => p m -> p m
preorderMap p = p `andThenM` multiplate (preorderMap p)

||| Append the result of 2 plates which each return `Const`
public export
public export %inline
append : Multiplate p => Monoid o => Lazy (p (Const o)) -> Lazy (p (Const o)) -> p (Const o)
append p1 p2 = mkPlate $ \proj, x => proj p1 x <+> proj p2 x

||| Apply a fold to the whole family of a node.
||| This applies to the parent node, followed by children.
|||
||| The result, when applied to `x` looks like:
||| ```
Expand All @@ -98,12 +114,21 @@ public export covering
preorderFold : Multiplate p => Monoid o => p (Const o) -> p (Const o)
preorderFold p = p `append` multiplate (preorderFold p)

||| Apply a fold to the whole family of a node.
||| This applies to the children, followed by the parent node.
|||
||| The result when applied to `x` looks like:
||| ```
||| ...
||| <+> grandchildren x
||| <+> children x
||| <+> x
public export covering
postorderFold : Multiplate p => Monoid o => p (Const o) -> p (Const o)
postorderFold p = multiplate (postorderFold p) `append` p

||| Remove a `Maybe` from a transformation by providing a plate which generates a default value.
public export
public export %inline
catchWith : Multiplate p => Applicative f => p Maybe -> p f -> p f
catchWith p def = mkPlate $ \proj, x => case proj p x of
Just x' => pure x'
Expand All @@ -112,39 +137,57 @@ catchWith p def = mkPlate $ \proj, x => case proj p x of
||| Remove a `Maybe` from a transformation by returning the original value unaltered.
|||
||| This is equivalent to `catchWith purePlate`
public export
public export %inline
catch : Multiplate p => Applicative f => p Maybe -> p f
catch p = mkPlate $ \proj, x => case proj p x of
Just x' => pure x'
Nothing => pure x

||| Plates tend to have one field per mutually recursive data type.
||| This interface allows for projecting the transform of @ a
||| or making a new plate, which applies a transform to @ a.
||| Plates tend to consist of a fixed set of fields.
||| This interface allows for projecting the transform of @ a.
|||
||| @ p the plate
||| @ a the field
public export
interface Multiplate p => HasField p a where
interface Multiplate p => HasProjection p a where
||| Project a transform of a specific field out of a plate.
total
project : Projector p a
||| Inject a transform to create a new plate.
total
inject : Applicative f => (a -> f a) -> p f
inject f = update f purePlate
||| Update the transform of a given field.
total
update : (a -> f a) -> p f -> p f

||| Run a transformation in the identity monad.
|||
||| To run transforms in a different Monad use `project`
public export
traverseFor : HasField p a => p Identity -> a -> a
public export %inline
traverseFor : HasProjection p a => p Identity -> a -> a
traverseFor p x = runIdentity $ project p x

||| Run a fold
public export
foldFor : HasField p a => p (Const o) -> a -> o
public export %inline
foldFor : HasProjection p a => p (Const o) -> a -> o
foldFor p x = runConst $ project p x

||| Plates tend to consist of a fixed set of fields.
||| In addition to being able to project a field,
||| this interface allows for creating a plate from a transform.
|||
||| This is seperate from `HasProjection` as it is typically not
||| possible to implement for plates with indexed data types.
|||
||| @ p the plate
||| @ a the field
public export
interface HasProjection p a => HasField p a where
||| Inject a transform to create a new plate,
||| by filling in other transforms with `pure`.
total
inject : Applicative f => (a -> f a) -> p f
inject f = update f purePlate
||| Update the transform of a given field,
||| by replacing it with a new transform.
total
update : (a -> f a) -> p f -> p f

||| Inject a pure transformation into a plate.
public export %inline
injectPure : HasField p a => Applicative f => (a -> a) -> p f
injectPure f = inject $ pure . f
117 changes: 4 additions & 113 deletions test/src/Main.idr
Original file line number Diff line number Diff line change
@@ -1,117 +1,8 @@
module Main

import public Multiplate
import Control.Monad.State
import Control.Applicative.Const
import Data.List
import Data.Maybe

mutual
data Expr
= Add Expr Expr
| Sub Expr Expr
| Lit Integer
| Var String
| Block (List Stmt) Expr

data Stmt
= Let String Expr
| Return Expr

record ExprPlate f where
constructor MkExprPlate
expr : Expr -> f Expr
stmt : Stmt -> f Stmt

Multiplate ExprPlate where
multiplate p = MkExprPlate
(\case
Add l r => Add <$> p.expr l <*> p.expr r
Sub l r => Sub <$> p.expr l <*> p.expr r
Lit x => pure $ Lit x
Var v => pure $ Var v
Block xs e => Block <$> traverse p.stmt xs <*> p.expr e)
(\case
Let var val => Let var <$> p.expr val
Return e => Return <$> p.expr e)
mkPlate build = MkExprPlate (build expr) (build stmt)

HasField ExprPlate Expr where
project = expr
update f p = { expr := f } p

HasField ExprPlate Stmt where
project = stmt
update f p = { stmt := f } p

exampleExpr : Expr
exampleExpr = Add (Add (Lit 10) (Lit 22)) (Sub (Lit 8) (Lit 2))

constFold : ExprPlate Identity
constFold = inject $ \case
Add (Lit x) (Lit y) => pure $ Lit (x + y)
Sub (Lit x) (Lit y) => pure $ Lit (x - y)
x => pure x

covering
doGetLetBound : ExprPlate (Const (List String))
doGetLetBound = preorderFold $ inject $ \case
Let f _ => MkConst [f]
_ => neutral

covering
getLetBound : HasField ExprPlate a => a -> List String
getLetBound = foldFor doGetLetBound

filterM : Applicative f => (a -> f Bool) -> List a -> f (List a)
filterM pred [] = pure []
filterM pred (x :: xs) = go <$> pred x <*> filterM pred xs
where
go : Bool -> List a -> List a
go True xs = x :: xs
go False xs = xs

-- note: this doesn't deal with scoping
inlineLet : ExprPlate (State (List (String, Expr)))
inlineLet = MkExprPlate
{ stmt = \case
x@(Let var val) => x <$ modify ((var, val) ::) -- add the defintion to the environment
x => pure x
, expr = \case
Var v => do -- replace variables with their definitions
Just val <- gets $ lookup v
| Nothing => pure $ Var v
pure val
Block xs e => Block <$> filterM (\case -- remove lets which have been added to the environment
Let v _ => gets $ isNothing . lookup v
_ => pure True) xs <*> pure e
x => pure x
}

removeEmptyBlock : ExprPlate Identity
removeEmptyBlock = inject $ \case
Block [] e => pure e
x => pure x

longExample : Expr
longExample = Block
[ Let "foo" (Lit 10)
, Let "bah" (Add (Var "foo") (Var "foo"))
]
(Sub (Var "bah") (Var "foo"))

covering
doInlineLet : HasField ExprPlate a => a -> a
doInlineLet = evalState [] . project
(mapFamily $ inlineLet
`andThenId` removeEmptyBlock)

covering
doEverything : HasField ExprPlate a => a -> a
doEverything = evalState [] . project
(mapFamily $ inlineLet
`andThenId` removeEmptyBlock
`andThenId` constFold)
import Multiplate.Tests.Basic
import Multiplate.Tests.DeBruijn

main : IO ()
main = putStrLn "Tests passed"
main = do
basicMain
Loading

0 comments on commit 601d865

Please sign in to comment.