Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UnionType and UnionInputType, analogues of RecordType and RecordInputType #775

Merged
merged 7 commits into from
Jan 16, 2019
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions dhall/src/Dhall.hs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ module Dhall
-- * Types
, Type(..)
, RecordType(..)
, UnionType(..)
, InputType(..)
, Interpret(..)
, InvalidType(..)
Expand All @@ -62,6 +63,8 @@ module Dhall
, pair
, record
, field
, union
, constructor
, GenericInterpret(..)
, GenericInject(..)

Expand All @@ -71,6 +74,10 @@ module Dhall
, inputFieldWith
, inputField
, inputRecord
, UnionInputType(..)
, inputConstructorWith
, inputConstructor
, inputUnion

-- * Miscellaneous
, rawInput
Expand All @@ -88,8 +95,11 @@ module Dhall
import Control.Applicative (empty, liftA2, (<|>), Alternative)
import Control.Exception (Exception)
import Control.Monad.Trans.State.Strict
import Control.Monad (guard)
import Data.Coerce (coerce)
import Data.Functor.Contravariant (Contravariant(..), (>$<))
import Data.Functor.Contravariant.Divisible (Divisible(..), divided)
import Data.Maybe (fromMaybe, listToMaybe, mapMaybe)
import Data.Monoid ((<>))
import Data.Scientific (Scientific)
import Data.Sequence (Seq)
Expand Down Expand Up @@ -1371,6 +1381,66 @@ field key valueType =
( Data.Functor.Compose.Compose extractBody )
)

{-| The 'UnionType' monoid allows you to build a 'Type' parser
from a Dhall union

For example, let's take the following Haskell data type:

> data Status = Queued Natural
> | Result Text
> | Errored Text

And assume that we have the following Dhall union that we would like to
parse as a @Status@:

> < Result = "Finish succesfully"
> | Queued : Natural
> | Errored : Text
> >

Our parser has type 'Type' @Status@, but we can't build that out of any
smaller parsers, as 'Type's cannot be combined (they are only 'Functor's).
However, we can use a 'UnionType' to build a 'Type' for @Status@:

> status :: Type Status
> status =
> union
> ( Queued <$> constructor "Queued" natural
> <> Result <$> constructor "Result" string
> <> Errored <$> constructor "Errored" string
> )

-}
newtype UnionType a =
UnionType
( Data.Functor.Compose.Compose (Dhall.Map.Map Text) Type a )
deriving (Functor)

instance Semigroup (UnionType a) where
(<>) = coerce ((<>) :: Dhall.Map.Map Text (Type a) -> Dhall.Map.Map Text (Type a) -> Dhall.Map.Map Text (Type a))

instance Monoid (UnionType a) where
mempty = coerce (mempty :: Dhall.Map.Map Text (Type a))

-- | Run a 'UnionType' parser to build a 'Type' parser.
union :: Dhall.Map.Map Text (Type a) -> Type a
mstksg marked this conversation as resolved.
Show resolved Hide resolved
union mp = Type
{ extract = extractF
, expected = Union expect
}
where
expect = Dhall.expected <$> mp
extractF e0 = do
UnionLit fld e1 rest <- Just e0
t <- Dhall.Map.lookup fld mp
guard $ rest == Dhall.Map.delete fld expect
Dhall.extract t e1

-- | Parse a single constructor of a union
constructor :: Text -> Type a -> UnionType a
constructor key valueType = UnionType
( Data.Functor.Compose.Compose (Dhall.Map.singleton key valueType) )

{-| The 'RecordInputType' divisible (contravariant) functor allows you to build
an 'InputType' injector for a Dhall record.

Expand Down Expand Up @@ -1453,4 +1523,84 @@ inputRecord (RecordInputType inputTypeRecord) = InputType makeRecordLit recordTy
recordType = Record $ declared <$> inputTypeRecord
makeRecordLit x = RecordLit $ (($ x) . embed) <$> inputTypeRecord

{-| The 'UnionInputType' monoid allows you to build
an 'InputType' injector for a Dhall record.

For example, let's take the following Haskell data type:

> data Status = Queued Natural
> | Result Text
> | Errored Text

And assume that we have the following Dhall union that we would like to
parse as a @Status@:

> < Result = "Finish succesfully"
> | Queued : Natural
> | Errored : Text
> >

Our injector has type 'InputType' @Status@, but we can't build that out of any
smaller injectors, as 'InputType's cannot be combined.
However, we can use an 'InputUnionType' to build an 'InputType' for @Status@:

> injectStatus :: InputType Status
> injectStatus =
> inputUnion
> ( inputConstructorWith "Queued" inject (\case Queued n -> Just n; _ -> Nothing)
> <> inputConstructorWith "Result" inject (\case Result t -> Just t; _ -> Nothing)
> <> inputConstructorWith "Errored" inject (\case Errored e -> Just e; _ -> Nothing)
> )

Or, since we are simply using the `Inject` instance to inject each branch, we could write

> injectStatus :: InputType Status
> injectStatus =
> inputUnion
> ( inputConstructor "Queued" (\case Queued n -> Just n; _ -> Nothing)
> <> inputConstructor "Result" (\case Result t -> Just t; _ -> Nothing)
> <> inputConstructor "Errored" (\case Errored e -> Just e; _ -> Nothing)
> )

Note that the resulting 'InputType' is __not total__ unless we
appropriately aggregate each possibility. If we forgot to handle
a branch, the 'InputType' will throw a runtime exception when used on
an unhandled branch.
-}
newtype UnionInputType a
= UnionInputType (Dhall.Map.Map Text (Expr Src X, a -> Maybe (Expr Src X)))

instance Contravariant UnionInputType where
contramap f (UnionInputType u) = UnionInputType $ (fmap . flip (.)) f <$> u

instance Semigroup (UnionInputType a) where
UnionInputType x <> UnionInputType y = UnionInputType (x <> y)

instance Monoid (UnionInputType a) where
mempty = UnionInputType mempty

inputUnion :: UnionInputType a -> InputType a
inputUnion (UnionInputType inputTypeUnion) = InputType makeUnionLit (Union declare)
where
declare = fst <$> inputTypeUnion
makeUnionLit x = fromMaybe (errorWithoutStackTrace unmatched)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to avoid the partial implementation by changing UnionInputType to:

data UnionInputType a = UnionInputType (Map Text (Expr Src X)) (a -> Maybe (Map Expr Src X))

Copy link
Contributor Author

@mstksg mstksg Jan 8, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This representation makes more sense, since we only consume injector in the asum'd form. However, I don't quite see how this can directly help us fix the partiality.

However, this representation gives a hint about how we could make it non-partial; we might be able to write a variant of inputUnion that gives a fallback option that would be used if anything was Maybe.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

17fd6c3 now incorporates this new representation! It's actually nicer because we get a derivable Contravariant and almost a derivable Monoid instance (waiting for base-4.12, which has Monoid for :*:).

Still curious about what you had in mind for resolving the partiality issue, though! :)

. listToMaybe
. mapMaybe (uncurry (match x))
$ Dhall.Map.toList inputTypeUnion
unmatched = "inputUnion: UnionInputType is incomplete"
match x fd (_, p) = (\r -> UnionLit fd r (Dhall.Map.delete fd declare)) <$> p x

inputConstructorWith
:: Text
-> InputType b
-> (a -> Maybe b)
-> UnionInputType a
inputConstructorWith name inputType projector = UnionInputType $
Dhall.Map.singleton name (declared inputType, fmap (embed inputType) . projector)

inputConstructor
:: Inject b
=> Text
-> (a -> Maybe b)
-> UnionInputType a
inputConstructor name = inputConstructorWith name inject