Add autoWithFix helper function for Interpreting recursive data types (#1161) (#1195)

* Add autoWithFix helper function (#1161)

* Fix some dependencies. Remove TypeApplications extension.

* Pin recursion-schemes to appease hydra.

* Implement review suggestions.

* Fix error in documentation.
This commit is contained in:
MaxOw 2019-08-09 20:09:09 +00:00 committed by mergify[bot]
parent 268c79631d
commit ec8bcf0d63
8 changed files with 238 additions and 5 deletions

View File

@ -413,6 +413,7 @@ Extra-Source-Files:
tests/lint/success/*.dhall
tests/diff/*.dhall
tests/diff/*.txt
tests/recursive/*.dhall
tests/regression/*.dhall
tests/tutorial/*.dhall
@ -451,6 +452,7 @@ Library
either >= 5 && < 5.1,
exceptions >= 0.8.3 && < 0.11,
filepath >= 1.4 && < 1.5 ,
free >= 4.0 && < 6.0 ,
haskeline >= 0.7.2.1 && < 0.8 ,
lens-family-core >= 1.0.0 && < 2.1 ,
megaparsec >= 6.5.0 && < 7.1 ,
@ -461,6 +463,7 @@ Library
prettyprinter >= 1.2.0.1 && < 1.3 ,
prettyprinter-ansi-terminal >= 1.1.1 && < 1.2 ,
profunctors >= 3.1.2 && < 5.5 ,
recursion-schemes >= 5.0.1 && < 6.0 ,
repline >= 0.2.1.0 && < 0.3 ,
serialise >= 0.2.0.0 && < 0.3 ,
scientific >= 0.3.0.0 && < 0.4 ,
@ -597,6 +600,7 @@ Test-Suite tasty
semigroups ,
serialise ,
spoon < 0.4 ,
recursion-schemes >= 5.0.1 && < 6.0 ,
tasty >= 0.11.2 && < 1.3 ,
tasty-hunit >= 0.9.2 && < 0.11,
tasty-quickcheck >= 0.9.2 && < 0.11,

View File

@ -1,3 +1,4 @@
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveGeneric #-}
@ -6,13 +7,16 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TupleSections #-}
{-| Please read the "Dhall.Tutorial" module, which contains a tutorial explaining
how to use the language, the compiler, and this library
-}
@ -52,6 +56,8 @@ module Dhall
, toMonadic
, fromMonadic
, auto
, InterpretFix
, autoWithFix
, genericAuto
, InterpretOptions(..)
, defaultInterpretOptions
@ -104,12 +110,14 @@ module Dhall
import Control.Applicative (empty, liftA2, Alternative)
import Control.Exception (Exception)
import Control.Monad.Trans.Free (FreeF (..))
import Control.Monad.Trans.State.Strict
import Control.Monad (guard)
import Control.Monad (guard, (<=<))
import Data.Coerce (coerce)
import Data.Either.Validation (Validation(..), ealt, eitherToValidation, validationToEither)
import Data.Functor.Contravariant (Contravariant(..), (>$<), Op(..))
import Data.Functor.Contravariant.Divisible (Divisible(..), divided)
import Data.Functor.Foldable (Base, Corecursive, elgot)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Monoid ((<>))
import Data.Scientific (Scientific)
@ -120,7 +128,7 @@ import Data.Text.Prettyprint.Doc (Pretty)
import Data.Typeable (Typeable)
import Data.Vector (Vector)
import Data.Word (Word8, Word16, Word32, Word64)
import Dhall.Core (Expr(..), Chunks(..))
import Dhall.Core (Expr(..), Chunks(..), Var(..))
import Dhall.Import (Imported(..))
import Dhall.Parser (Src(..))
import Dhall.TypeCheck (DetailedTypeError(..), TypeError, X)
@ -135,6 +143,7 @@ import qualified Control.Exception
import qualified Control.Monad.Trans.State.Strict as State
import qualified Data.Foldable
import qualified Data.Functor.Compose
import qualified Data.Functor.Foldable
import qualified Data.Functor.Product
import qualified Data.Maybe
import qualified Data.List.NonEmpty
@ -900,6 +909,121 @@ instance (Interpret a, Interpret b) => Interpret (a, b)
auto :: Interpret a => Type a
auto = autoWith defaultInterpretOptions
newtype DhallFix f t = DhallFix { unDhallFix :: t }
newtype FixVar = FixVar { unFixVar :: Expr Src X }
-- This is a helper instance used for implementation of Interpret for DhallFix
instance Interpret FixVar where
autoWith _ = Type {..}
where
extract (App (Var (V "_" 0)) e0) = pure (FixVar e0)
extract e = typeError expected e
expected = "t"
type InterpretFix f t =
( Traversable f
, Corecursive t
, Interpret (f FixVar)
, Base t ~ f
)
instance InterpretFix f t => Interpret (DhallFix f t) where
autoWith opts = Type
{ extract = extr . Dhall.Core.alphaNormalize, expected = expe }
where
extr (Lam _ _ (Lam _ _ e)) = DhallFix <$> buildF e
extr e = typeError expe e
-- buildF = elgot f g <=< extract (auto @FixVar)
viaMonadic a b = fromMonadic . (toMonadic . a <=< toMonadic . b)
buildF = elgot f g `viaMonadic` extract (auto :: Type FixVar)
g = repack . extract xF_t . unFixVar
repack (Failure e) = Left (Failure e)
repack (Success s) = Right (Free s)
f (Free s) = Data.Functor.Foldable.embed <$> sequenceA s
f (Pure x) = typeError expe x
expe = Pi "t" (Dhall.Core.Const Dhall.Core.Type) -- = ∀(t : Type)
$ Pi "_" (Pi "_" (expected xF_t) "t") -- → ∀(F t → t)
$ "t" -- → t
xF_t = autoWith opts :: Type (f FixVar)
{-| `autoWithFix` allows you to implement `Interpret` instance for a recursive
data type. For example, given a Haskell expression:
> data Expr
> = Lit Natural
> | Add Expr Expr
> | Mul Expr Expr
> deriving (Generic, Show)
>
> makeBaseFunctor ''Expr
> deriving instance Generic (ExprF a)
> instance Interpret a => Interpret (ExprF a)
> instance Interpret Expr where autoWith = autoWithFix
And a dhall file:
> -- expr.dhall
> λ(t : Type)
> let ExprF =
> < LitF : { _1 : Natural }
> | AddF : { _1 : t, _2 : t }
> | MulF : { _1 : t, _2 : t }
> >
>
> in λ(Fix : ExprF t)
> let Lit = λ(x : Natural) Fix (ExprF.LitF { _1 = x })
> let Add = λ(a : t) λ(b : t) Fix (ExprF.AddF { _1 = a, _2 = b })
> let Mul = λ(a : t) λ(b : t) Fix (ExprF.MulF { _1 = a, _2 = b })
> in Add (Mul (Lit 3) (Lit 7)) (Add (Lit 1) (Lit 2))
We're now able to interpret it:
> > input @Expr auto "./expr.dhall"
> Add (Mul (Lit 3) (Lit 7)) (Add (Lit 1) (Lit 2))
Alternatively, we could factor out some helper expressions:
> let ExprF =
> λ(t : Type)
> < LitF : { _1 : Natural }
> | AddF : { _1 : t, _2 : t }
> | MulF : { _1 : t, _2 : t }
> >
>
> let Expr
> = λ(t : Type)
> λ(Fix : ExprF t t)
> let E = ExprF t in
> { Lit = λ(x : Natural) Fix (E.LitF { _1 = x })
> , Add = λ(a : t) λ(b : t) Fix (E.AddF { _1 = a, _2 = b })
> , Mul = λ(a : t) λ(b : t) Fix (E.MulF { _1 = a, _2 = b })
> }
And then the use location becomes:
> λ(t : Type)
> λ(Fix : ExprF t t)
> let E = Expr t Fix in
> E.Add
> (E.Mul (E.Lit 3) (E.Lit 7))
> (E.Add (E.Lit 1) (E.Lit 2))
>
In general your dhall expression needs to be of type:
> (t : Type) (F t t) t
Where F is an F-Algebra generating your data type.
-}
autoWithFix :: forall f t. InterpretFix f t => InterpretOptions -> Type t
autoWithFix opts = unDhallFix <$> (autoWith opts :: Type (DhallFix f t))
{-| `genericAuto` is the default implementation for `auto` if you derive
`Interpret`. The difference is that you can use `genericAuto` without
having to explicitly provide an `Interpret` instance for a type as long as

View File

@ -1,16 +1,23 @@
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Dhall.Test.Dhall where
import Control.Exception (SomeException, try)
import Data.Functor.Foldable.TH (makeBaseFunctor)
import Data.String (fromString)
import Data.Text (Text)
import Dhall (Inject, Interpret)
import Dhall.Core (Expr(..))
@ -19,17 +26,30 @@ import Numeric.Natural (Natural)
import Test.Tasty
import Test.Tasty.HUnit
import qualified Data.Text
import qualified Dhall
import qualified Dhall.Core
import qualified Dhall.Import
import qualified Dhall.Map
import qualified Dhall.Parser
data AExpr
= Lit Natural
| Add AExpr AExpr
| Mul AExpr AExpr
deriving (Generic, Show, Eq)
makeBaseFunctor ''AExpr
deriving instance Generic (AExprF a)
instance Interpret a => Interpret (AExprF a)
instance Interpret AExpr where autoWith = Dhall.autoWithFix
tests :: TestTree
tests =
testGroup "Input"
[ shouldShowDetailedTypeError
, shouldHandleUnionLiteral
, shouldHaveWorkingAutoWithFix
, shouldHaveWorkingGenericAuto
, shouldHandleUnionsCorrectly
, shouldTreatAConstructorStoringUnitAsEmptyAlternative
@ -37,7 +57,7 @@ tests =
data MyType = MyType { foo :: String , bar :: Natural }
wrongDhallType :: Dhall.Type MyType
wrongDhallType :: Dhall.Type MyType
wrongDhallType = Dhall.Type { .. }
where expected =
Dhall.Core.Record
@ -51,7 +71,7 @@ wrongDhallType = Dhall.Type { .. }
shouldShowDetailedTypeError :: TestTree
shouldShowDetailedTypeError = testCase "detailed TypeError" $ do
inputEx :: Either SomeException MyType <-
try ( Dhall.input wrongDhallType "{ bar = 0, foo = \"foo\" }")
try ( Dhall.input wrongDhallType "{ bar = 0, foo = \"foo\" }")
let expectedMsg =
"\ESC[1;31mError\ESC[0m: Invalid Dhall.Type \n\
@ -69,7 +89,7 @@ shouldShowDetailedTypeError = testCase "detailed TypeError" $ do
\ \n"
let assertMsg = "The exception message did not match the expected output"
case inputEx of
Left ex -> assertEqual assertMsg expectedMsg (show ex)
Right _ -> fail "The extraction using a wrong type succeded"
@ -96,6 +116,35 @@ shouldTreatAConstructorStoringUnitAsEmptyAlternative = testCase "Handle unit con
Dhall.embed exampleInputType () @=? Field (Union (Dhall.Map.singleton "A" Nothing)) "A"
makeExprDhall :: AExpr -> Text
makeExprDhall expr = Data.Text.unlines
[ "let ExprF = ./tests/recursive/exprf.dhall"
, "let Expr = ./tests/recursive/expr.dhall"
, "in"
, "λ(t : Type) →"
, "λ(fix : ExprF t → t) →"
, "let E = Expr t fix"
, "let Lit = E.Lit"
, "let Add = E.Add"
, "let Mul = E.Mul in " <> (fromString $ show expr)
]
shouldHaveWorkingAutoWithFix :: TestTree
shouldHaveWorkingAutoWithFix = testGroup "autoWithFix"
[ testCase "works for a recursive expression" $ do
-- This expression could theoretically by generated by quickcheck
-- but I don't think that would provide much value, and would only slow
-- things down. Thoughts?
let expectedExpr = Add (Mul (Lit 3) (Lit 7)) (Add (Lit 1) (Lit 2))
expr <- Dhall.input Dhall.auto (makeExprDhall expectedExpr)
assertEqual "autoWithFix didn't give us what we wanted" expectedExpr expr
, testCase "passes a shadowing sanity check" $ do
let expectedExpr = Lit 2
expr <- Dhall.input Dhall.auto "./tests/recursive/expr0.dhall"
assertEqual "autoWithFix didn't give us what we wanted" expectedExpr expr
]
data CompilerFlavor3 =
GHC3 | GHCJS3 | Helium3
deriving (Generic, Show, Eq)

View File

@ -0,0 +1,12 @@
let ExprF = ./exprf.dhall
let Expr
= λ(e : Type)
→ λ(f : ExprF e → e)
→ let E = ExprF e in
{ Lit = λ(x : Natural) → f (E.LitF { _1 = x })
, Add = λ(a : e) → λ(b : e) → f (E.AddF { _1 = a, _2 = b })
, Mul = λ(a : e) → λ(b : e) → f (E.MulF { _1 = a, _2 = b })
}
in Expr

View File

@ -0,0 +1,17 @@
let ExprF = ./exprf.dhall
let Expr = ./expr.dhall
-- This code is deliberately convoluted to test shadowing, normalization, etc.
let h = 8
let result
= λ(a : Type)
→ λ(h : ExprF a → a)
→ let f = h
let g = a
let a = 1
let h = 1
let E = Expr g f
in E.Lit (h + a)
in result

View File

@ -0,0 +1,8 @@
let ExprF
= λ(a : Type) →
< LitF : { _1 : Natural }
| AddF : { _1 : a, _2 : a }
| MulF : { _1 : a, _2 : a }
>
in ExprF

18
nix/recursion-schemes.nix Normal file
View File

@ -0,0 +1,18 @@
{ mkDerivation, base, base-orphans, comonad, free, HUnit, stdenv
, template-haskell, th-abstraction, transformers
}:
mkDerivation {
pname = "recursion-schemes";
version = "5.1.3";
sha256 = "b21736e54b1d5fb0149624e95f2d8d8fd3413bb0972eda6a70e5753d3c9f1528";
revision = "1";
editedCabalFile = "0zxcmga4fy42arbyv1kzbyfnsghnll3prkpva58x654x4cg4diwk";
libraryHaskellDepends = [
base base-orphans comonad free template-haskell th-abstraction
transformers
];
testHaskellDepends = [ base HUnit template-haskell transformers ];
homepage = "http://github.com/ekmett/recursion-schemes/";
description = "Representing common recursion patterns as higher-order functions";
license = stdenv.lib.licenses.bsd2;
}

View File

@ -38,6 +38,7 @@ extra-deps:
- turtle-1.5.14
- unliftio-core-0.1.2.0
- yaml-0.10.4.0
- recursion-schemes-5.1.3
flags:
transformers-compat:
four: true