moving to using the GADT

The problem this exposes has to do with requires. As implemented,
requires yields either a Property HasInfo or a Property NoInfo depending
on its inputs. That works. But look what happens when it's used:

*Propellor.Types> let foo = IProperty "foo" (return NoChange) mempty mempty
*Propellor.Types> let bar = IProperty "bar" (return NoChange) mempty mempty
*Propellor.Types> foo `requires` bar

<interactive>:17:5:
    No instance for (Requires (Property HasInfo) (Property HasInfo) r0)
      arising from a use of `requires'
    The type variable `r0' is ambiguous
    Possible fix: add a type signature that fixes these type variable(s)
    Note: there is a potential instance available:
      instance Requires
                 (Property HasInfo) (Property HasInfo) (Property HasInfo)
        -- Defined at Propellor/Types.hs:167:10
    Possible fix:
      add an instance declaration for
      (Requires (Property HasInfo) (Property HasInfo) r0)
    In the expression: foo `requires` bar
    In an equation for `it': it = foo `requires` bar

This can be avoided by specifying the result type:

*Propellor.Types> (foo `requires` bar) :: Property HasInfo
property "foo"

But then when multiple `requires` are given, the result type has to be
given each time:

*Propellor.Types> (foo `requires` bar `requires` bar) :: Property HasInfo
<interactive>:22:6:
    No instance for (Requires (Property HasInfo) (Property HasInfo) x0)
      arising from a use of `requires'
    The type variable `x0' is ambiguous
    Possible fix: add a type signature that fixes these type variable(s)
    Note: there is a potential instance available:
      instance Requires
                 (Property HasInfo) (Property HasInfo) (Property HasInfo)
        -- Defined at Propellor/Types.hs:167:10
    Possible fix:
      add an instance declaration for
      (Requires (Property HasInfo) (Property HasInfo) x0)
    In the first argument of `requires', namely `foo `requires` bar'
    In the expression:
        (foo `requires` bar `requires` bar) :: Property HasInfo
    In an equation for `it':
        it = (foo `requires` bar `requires` bar) :: Property HasInfo

<interactive>:22:21:
    No instance for (Requires x0 (Property HasInfo) (Property HasInfo))
      arising from a use of `requires'
    The type variable `x0' is ambiguous
    Possible fix: add a type signature that fixes these type variable(s)
    Note: there are several potential instances:
      instance Requires
                 (Property NoInfo) (Property HasInfo) (Property HasInfo)
        -- Defined at Propellor/Types.hs:175:10
      instance Requires
                 (Property HasInfo) (Property HasInfo) (Property HasInfo)
        -- Defined at Propellor/Types.hs:167:10
    Possible fix:
      add an instance declaration for
      (Requires x0 (Property HasInfo) (Property HasInfo))
    In the expression:
        (foo `requires` bar `requires` bar) :: Property HasInfo
    In an equation for `it':
        it = (foo `requires` bar `requires` bar) :: Property HasInfo

*Propellor.Types> (((foo `requires` bar) :: Property HasInfo) `requires` bar) :: Property HasInfo
property "foo"

Yuggh!
This commit is contained in:
Joey Hess 2015-01-24 16:54:49 -04:00
parent 414ee7eee6
commit 45c94ffdd7
3 changed files with 110 additions and 66 deletions

View File

@ -45,7 +45,7 @@ mainProperties host = do
FailedChange -> exitWith (ExitFailure 1)
_ -> exitWith ExitSuccess
where
ps = hostProperties host
ps = map ignoreInfo $ hostProperties host
-- | Runs a Propellor action with the specified host.
--
@ -66,13 +66,12 @@ runEndAction host res (EndAction desc a) = actionMessageOn (hostName host) desc
-- | For when code running in the Propellor monad needs to ensure a
-- Property.
--
-- Note that the Info of the Property is not propigated out, so it will
-- not be visible to propellor, unless you arrange for it to be propigated.
ensureProperty :: Property -> Propellor Result
-- This can only be used on a Property that has NoInfo.
ensureProperty :: Property NoInfo -> Propellor Result
ensureProperty = catchPropellor . propertySatisfy
-- | Ensures a list of Properties, with a display of each as it runs.
ensureProperties :: [Property] -> Propellor Result
ensureProperties :: [Property NoInfo] -> Propellor Result
ensureProperties ps = ensure ps NoChange
where
ensure [] rs = return rs

View File

@ -12,13 +12,13 @@ import Data.Maybe
import Data.Monoid
import Control.Applicative
pureInfoProperty :: Desc -> Info -> Property
pureInfoProperty :: Desc -> Info -> Property HasInfo
pureInfoProperty desc i = mkProperty ("has " ++ desc) (return NoChange) i mempty
askInfo :: (Info -> Val a) -> Propellor (Maybe a)
askInfo f = asks (fromVal . f . hostInfo)
os :: System -> Property
os :: System -> Property HasInfo
os system = pureInfoProperty ("Operating " ++ show system) $
mempty { _os = Val system }
@ -34,11 +34,11 @@ getOS = askInfo _os
-- When propellor --spin is used to deploy a host, it checks
-- if the host's IP Property matches the DNS. If the DNS is missing or
-- out of date, the host will instead be contacted directly by IP address.
ipv4 :: String -> Property
ipv4 :: String -> Property HasInfo
ipv4 = addDNS . Address . IPv4
-- | Indidate that a host has an AAAA record in the DNS.
ipv6 :: String -> Property
ipv6 :: String -> Property HasInfo
ipv6 = addDNS . Address . IPv6
-- | Indicates another name for the host in the DNS.
@ -47,7 +47,7 @@ ipv6 = addDNS . Address . IPv6
-- to use their address, rather than using a CNAME. This avoids various
-- problems with CNAMEs, and also means that when multiple hosts have the
-- same alias, a DNS round-robin is automatically set up.
alias :: Domain -> Property
alias :: Domain -> Property HasInfo
alias d = pureInfoProperty ("alias " ++ d) $ mempty
{ _aliases = S.singleton d
-- A CNAME is added here, but the DNS setup code converts it to an
@ -55,7 +55,7 @@ alias d = pureInfoProperty ("alias " ++ d) $ mempty
, _dns = S.singleton $ CNAME $ AbsDomain d
}
addDNS :: Record -> Property
addDNS :: Record -> Property HasInfo
addDNS r = pureInfoProperty (rdesc r) $ mempty { _dns = S.singleton r }
where
rdesc (CNAME d) = unwords ["alias", ddesc d]

View File

@ -2,24 +2,31 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
module Propellor.Types
( Host(..)
, Property(..)
, HasInfo
, NoInfo
, Desc
, mkProperty
, propertyDesc
, propertySatisfy
, propertyInfo
, propertyChildren
, RevertableProperty(..)
, mkRevertableProperty
, requires
, IsProp(..)
, Desc
, Info(..)
, Propellor(..)
, EndAction(..)
, module Propellor.Types.OS
, module Propellor.Types.Dns
, module Propellor.Types.Result
, ignoreInfo
) where
import Data.Monoid
@ -43,7 +50,7 @@ import qualified Propellor.Types.Dns as Dns
-- properties and their collected info.
data Host = Host
{ hostName :: HostName
, hostProperties :: [Property]
, hostProperties :: [Property HasInfo]
, hostInfo :: Info
}
deriving (Show)
@ -65,96 +72,134 @@ newtype Propellor p = Propellor { runWithHost :: RWST Host [EndAction] () IO p }
-- properties. It's passed the combined Result of the entire Propellor run.
data EndAction = EndAction Desc (Result -> Propellor Result)
type Desc = String
-- | The core data type of Propellor, this represents a property
-- that the system should have, and an action to ensure it has the
-- property.
data Property = IProperty (GProperty HasInfo) | SProperty (GProperty NoInfo)
data Property i where
IProperty :: Desc -> Propellor Result -> Info -> [Property HasInfo] -> Property HasInfo
SProperty :: Desc -> Propellor Result -> [Property NoInfo] -> Property NoInfo
data HasInfo
data NoInfo
-- | Constructs a Property
mkProperty
:: Desc -- ^ description of the property
-> Propellor Result -- ^ action to run to satisfy the property (must be idempotent; may run repeatedly)
-> Info -- ^ info associated with the property
-> [Property] -- ^ child properties
-> Property
mkProperty d a i cs
| isEmpty i && all isEmpty (map propertyInfo cs) =
SProperty (GSProperty d a cs)
| otherwise = IProperty (GIProperty d a i cs)
-> [Property i] -- ^ child properties
-> Property HasInfo
mkProperty d a i cs = IProperty d a i (map toIProperty cs)
instance Show Property where
toIProperty :: Property i -> Property HasInfo
toIProperty p@(IProperty {}) = p
toIProperty (SProperty d s cs) = IProperty d s mempty (map toIProperty cs)
toSProperty :: Property i -> Property NoInfo
toSProperty (IProperty d s _ cs) = SProperty d s (map toSProperty cs)
toSProperty p@(SProperty {}) = p
-- | Makes a version of a Proprty without its Info.
-- Use with caution!
ignoreInfo :: Property HasInfo -> Property NoInfo
ignoreInfo = toSProperty
instance Show (Property i) where
show p = "property " ++ show (propertyDesc p)
-- | This GADT allows creating operations that only act on Properties
-- that do not add Info to their Host.
data GProperty i where
GIProperty :: Desc -> Propellor Result -> Info -> [Property] -> GProperty HasInfo
GSProperty :: Desc -> Propellor Result -> [Property] -> GProperty NoInfo
propertyDesc :: Property i -> Desc
propertyDesc (IProperty d _ _ _) = d
propertyDesc (SProperty d _ _) = d
data HasInfo
data NoInfo
propertySatisfy :: Property i -> Propellor Result
propertySatisfy (IProperty _ a _ _) = a
propertySatisfy (SProperty _ a _) = a
propertyDesc :: Property -> Desc
propertyDesc (IProperty (GIProperty d _ _ _)) = d
propertyDesc (SProperty (GSProperty d _ _)) = d
propertySatisfy :: Property -> Propellor Result
propertySatisfy (IProperty (GIProperty _ a _ _)) = a
propertySatisfy (SProperty (GSProperty _ a _)) = a
propertyInfo :: Property -> Info
propertyInfo (IProperty (GIProperty _ _ i _)) = i
propertyInfo (SProperty _) = mempty
propertyInfo :: Property i -> Info
propertyInfo (IProperty _ _ i _) = i
propertyInfo (SProperty {}) = mempty
-- | A Property can include a list of child properties that it also
-- satisfies. This allows them to be introspected to collect their info, etc.
propertyChildren :: Property -> [Property]
propertyChildren (IProperty (GIProperty _ _ _ cs)) = cs
propertyChildren (SProperty (GSProperty _ _ cs)) = cs
propertyChildren :: Property i -> [Property i]
propertyChildren (IProperty _ _ _ cs) = cs
propertyChildren (SProperty _ _ cs) = cs
-- | A property that can be reverted.
data RevertableProperty = RevertableProperty Property Property
data RevertableProperty = RevertableProperty (Property HasInfo) (Property HasInfo)
mkRevertableProperty :: Property i1 -> Property i2 -> RevertableProperty
mkRevertableProperty p1 p2 = RevertableProperty (toIProperty p1) (toIProperty p2)
class IsProp p where
-- | Sets description.
describe :: p -> Desc -> p
toProp :: p -> Property
-- | Indicates that the first property can only be satisfied
-- once the second one is.
requires :: p -> Property -> p
toProp :: p -> Property HasInfo
-- | Gets the info of the property, combined with all info
-- of all children properties.
getInfoRecursive :: p -> Info
instance IsProp Property where
describe (IProperty (GIProperty _ a i cs)) d =
IProperty (GIProperty d a i cs)
describe (SProperty (GSProperty _ a cs)) d =
SProperty (GSProperty d a cs)
toProp p = p
getInfoRecursive (IProperty (GIProperty _ _ i cs)) =
instance IsProp (Property HasInfo) where
describe (IProperty _ a i cs) d = IProperty d a i cs
toProp = id
getInfoRecursive (IProperty _ _ i cs) =
i <> mconcat (map getInfoRecursive cs)
getInfoRecursive (SProperty _) = mempty
x `requires` y = mkProperty (propertyDesc x) satisfy (propertyInfo x) cs
where
satisfy = do
r <- propertySatisfy y
case r of
FailedChange -> return FailedChange
_ -> propertySatisfy x
cs = y : propertyChildren x
instance IsProp (Property NoInfo) where
describe (SProperty _ a cs) d = SProperty d a cs
toProp = toIProperty
getInfoRecursive _ = mempty
instance IsProp RevertableProperty where
-- | Sets the description of both sides.
describe (RevertableProperty p1 p2) d =
RevertableProperty (describe p1 d) (describe p2 ("not " ++ d))
toProp (RevertableProperty p1 _) = p1
(RevertableProperty p1 p2) `requires` y =
RevertableProperty (p1 `requires` y) p2
-- | Return the Info of the currently active side.
getInfoRecursive (RevertableProperty p1 _p2) = getInfoRecursive p1
type Desc = String
class Requires x y r where
-- Indicates that the first property depends on the second,
-- so before the first is ensured, the second will be ensured.
requires :: x -> y -> r
instance Requires (Property HasInfo) (Property HasInfo) (Property HasInfo) where
requires (IProperty d1 a1 i1 cs1) y@(IProperty _d2 a2 _i2 _cs2) =
IProperty d1 (a2 `andThen` a1) i1 (y : cs1)
instance Requires (Property HasInfo) (Property NoInfo) (Property HasInfo) where
requires (IProperty d1 a1 i1 cs1) y@(SProperty _d2 a2 _cs2) =
IProperty d1 (a2 `andThen` a1) i1 (toIProperty y : cs1)
instance Requires (Property NoInfo) (Property HasInfo) (Property HasInfo) where
requires x y = requires y x
instance Requires (Property NoInfo) (Property NoInfo) (Property NoInfo) where
requires (SProperty d1 a1 cs1) y@(SProperty _d2 a2 _cs2) =
SProperty d1 (a2 `andThen` a1) (y : cs1)
instance Requires RevertableProperty (Property HasInfo) RevertableProperty where
requires (RevertableProperty p1 p2) y =
RevertableProperty (p1 `requires` y) p2
instance Requires RevertableProperty (Property NoInfo) RevertableProperty where
requires (RevertableProperty p1 p2) y =
RevertableProperty (p1 `requires` toIProperty y) p2
instance Requires RevertableProperty RevertableProperty RevertableProperty where
requires (RevertableProperty x1 x2) (RevertableProperty y1 y2) =
RevertableProperty
(x1 `requires` y1)
-- when reverting, run actions in reverse order
(y2 `requires` x2)
andThen :: Propellor Result -> Propellor Result -> Propellor Result
x `andThen` y = do
r <- x
case r of
FailedChange -> return FailedChange
_ -> y
-- | Information about a host.
data Info = Info