dhall-recursive-adt-0.1.0.1: src/Dhall/Deriving/Recursive.hs
{-# LANGUAGE UndecidableInstances #-}
-- | Convert /recursive/ ADTs from and to Dhall with only /some/ boilerplate.
--
-- This module is intended to be used with
-- [@DerivingVia@](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#extension-DerivingVia).
--
-- Click "Dhall.Deriving.Recursive#example" for a compact example.
module Dhall.Deriving.Recursive
( -- * Walkthrough
-- $walkthrough
-- * Compact example
-- $example
-- * The @DerivingVia@ newtype
RecADT (..),
-- * Utilities
-- $utilities
RecFromDhall,
recAuto,
recAutoWith,
RecToDhall,
recInject,
recInjectWith,
)
where
import Data.Fix (Fix)
import Data.Functor.Contravariant (contramap)
import Data.Functor.Foldable (Base, Corecursive, Recursive, refix)
import qualified Dhall as D
-- | Intended to be used with @DerivingVia@, as explained in the module documentation.
--
-- [@a@]: your (recursive) ADT
-- We could work without UndecidableInstances if we added a type parameter f (~ Base a).
-- This wouldn't be a disaster, but it is a bit less convenient.
newtype RecADT a = RecADT {unRecADT :: a}
instance RecFromDhall a => D.FromDhall (RecADT a) where
autoWith = fmap RecADT . recAutoWith
instance RecToDhall a => D.ToDhall (RecADT a) where
injectWith = contramap unRecADT . recInjectWith
type RecFromDhall a = (D.FromDhall (Base a (D.Result (Base a))), Corecursive a)
recAuto :: RecFromDhall a => D.Decoder a
recAuto = recAutoWith D.defaultInputNormalizer
recAutoWith :: forall a. RecFromDhall a => D.InputNormalizer -> D.Decoder a
recAutoWith = fmap refix . D.autoWith @(Fix (Base a))
type RecToDhall a = (D.ToDhall (Base a (D.Result (Base a))), Recursive a)
recInject :: RecToDhall a => D.Encoder a
recInject = recInjectWith D.defaultInputNormalizer
recInjectWith :: forall a. RecToDhall a => D.InputNormalizer -> D.Encoder a
recInjectWith = contramap refix . D.injectWith @(Fix (Base a))
-- $setup
--
-- >>> import Numeric.Natural (Natural)
-- >>> import GHC.Generics (Generic)
-- >>> import Dhall (FromDhall, ToDhall)
-- >>> :set -XDerivingVia
-- >>> :set -XDeriveAnyClass
-- >>> :set -XDeriveFunctor
-- >>> :set -XDeriveFoldable
-- >>> :set -XDeriveTraversable
-- >>> :set -XDeriveGeneric
-- >>> :set -XOverloadedStrings
-- >>> :set -XQuasiQuotes
-- >>> :set -XStandaloneDeriving
-- >>> :set -XTemplateHaskell
-- >>> :set -XTypeApplications
-- >>> :set -XTypeFamilies
-- $walkthrough
--
-- Out of the box, [dhall](https://hackage.haskell.org/package/dhall) can not derive 'D.FromDhall'/'D.ToDhall'
-- for recursive ADTs like this one
--
-- >>> :{
-- data Expr = Lit Natural
-- | Add Expr Expr
-- | Mul Expr Expr
-- deriving stock Show
-- :}
--
-- As Dhall has no recursion by design, it might be non-obvious how to encode this type in Dhall. Luckily,
-- the Church\/Böhm-Berarducci encoding exists, which is essentially a fancy name for the Go4
-- [Visitor pattern](https://en.wikipedia.org/wiki/Visitor_pattern), as explained in
-- [this blog post](https://www.haskellforall.com/2021/01/the-visitor-pattern-is-essentially-same.html).
-- Also make sure check out the
-- [official How-to on translating recursive code to Dhall](https://docs.dhall-lang.org/howtos/How-to-translate-recursive-code-to-Dhall.html).
--
-- Explicitly, consider the /non-recursive/ ADT
--
-- >>> :{
-- data ExprF a = LitF Natural
-- | AddF a a
-- | MulF a a
-- :}
--
-- For any type @a@, this is a non-recursive sum type, which we can directly translate to a Dhall union type:
--
-- > let ExprF = λ(a : Type) → < LitF : Natural | AddF : { _1 : a, _2 : a } | MulF : { _1 : a, _2 : a } >
--
-- We can derive 'D.FromDhall'\/'D.ToDhall' for @ExprF a@. Let us use the default 'GHC.Generics.Generic'-powered
-- version here, but we could also use e.g. "Dhall.Deriving" for customization (we use
-- [@StandaloneDeriving@](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#extension-StandaloneDeriving)
-- here to be able to define the instances one by one).
--
-- >>> deriving stock instance Generic (ExprF a)
-- >>> deriving anyclass instance FromDhall a => FromDhall (ExprF a)
-- >>> deriving anyclass instance ToDhall a => ToDhall (ExprF a)
--
-- Right now, @Expr@ and @ExprF@ are completely unrelated. The open type family 'Base' and the type classes
-- 'Recursive' and 'Corecursive' from [recursion-schemes](https://hackage.haskell.org/package/recursion-schemes)
-- are exactly what we need.
--
-- We can derive 'Recursive' and 'Corecursive' for @Expr@ as long as we derive 'GHC.Generics.Generic' for it, and
-- define @'Base' Expr ≔ ExprF@ as well as derive 'Functor' for @ExprF@.
--
-- >>> import Data.Functor.Foldable (Base, Recursive, Corecursive)
-- >>> type instance Base Expr = ExprF
-- >>> deriving stock instance Functor ExprF
-- >>> deriving stock instance Generic Expr
-- >>> deriving anyclass instance Recursive Expr
-- >>> deriving anyclass instance Corecursive Expr
--
-- With this setup, we can finally derive 'D.FromDhall'\/'D.ToDhall' for @Expr@:
--
-- >>> deriving via RecADT Expr instance FromDhall Expr
-- >>> deriving via RecADT Expr instance ToDhall Expr
--
-- To see how this works, consider this expression representing @(1 + 2) * (3 + 4)@:
--
-- >>> expr = Mul (Add (Lit 1) (Lit 2)) (Add (Lit 3) (Lit 4))
-- >>> import qualified Dhall as D
-- >>> encodedExpr = D.embed D.inject expr
--
-- In Dhall, we can construct an @Expr@ like this:
--
-- >>> import qualified Dhall as D
-- >>> import Yasi
-- >>> :{
-- dhallExpr <- D.inputExpr [i|
-- let ExprF = λ(a : Type) → < LitF : Natural | AddF : { _1 : a, _2 : a } | MulF : { _1 : a, _2 : a } >
-- let Expr = ∀(a : Type) → (ExprF a → a) → a
-- --
-- let Lit : Natural → Expr = λ(n : Natural) →
-- λ(a : Type) → λ(make : ExprF a → a) → make ((ExprF a).LitF n)
-- --
-- let Add : Expr → Expr → Expr = λ(x : Expr) → λ(y : Expr) →
-- λ(a : Type) → λ(make : ExprF a → a) → make ((ExprF a).AddF { _1 = x a make, _2 = y a make })
-- --
-- let Mul : Expr → Expr → Expr = λ(x : Expr) → λ(y : Expr) →
-- λ(a : Type) → λ(make : ExprF a → a) → make ((ExprF a).MulF { _1 = x a make, _2 = y a make })
-- --
-- in Mul (Add (Lit 1) (Lit 2)) (Add (Lit 3) (Lit 4))
-- |]
-- :}
--
-- In fact, we have
--
-- >>> import Dhall.Core (judgmentallyEqual)
-- >>> encodedExpr `judgmentallyEqual` dhallExpr
-- True
-- >>> D.extract @Expr D.auto dhallExpr
-- Success (Mul (Add (Lit 1) (Lit 2)) (Add (Lit 3) (Lit 4)))
-- $example
-- #example#
--
-- We can still shorten the boilerplate a bit using 'Data.Functor.Foldable.TH.makeBaseFunctor'.
--
-- >>> import Data.Functor.Foldable.TH (makeBaseFunctor)
-- >>> :{
-- data Tree a = Leaf a | Branch (Tree a) (Tree a)
-- makeBaseFunctor ''Tree
-- :}
--
-- >>> deriving stock instance Generic (TreeF a b)
-- >>> deriving anyclass instance (FromDhall a, FromDhall b) => FromDhall (TreeF a b)
-- >>> deriving anyclass instance (ToDhall a, ToDhall b) => ToDhall (TreeF a b)
--
-- >>> deriving via RecADT (Tree a) instance FromDhall a => FromDhall (Tree a)
-- >>> deriving via RecADT (Tree a) instance ToDhall a => ToDhall (Tree a)
--
-- The test suite contains an expanded example of this situation.
-- $utilities
--
-- If you don't want to derive 'D.FromDhall'/'D.ToDhall' with @DerivingVia@, use these functions instead.