The trivial
property combinator is a bit of a code smell. It's almost
always better for a property to do the work to return MadeChange
accurately. While it doesn't matter if a property directly attached to a
Host is trivial, it can matter a great deal when eg, a disk image needs to
be regenerated when a property makes a change to the source chroot.
So, I'd like to move propellor to having all properties return an accurate
MadeChange. Of course, it's up to the implementation to get that right, but
avoiding trivial
would go a long way.
At the same time, it's sometimes useful to use trivial along the way to a non-trivial property.
trivial (cmdProperty "apt-get" ["-y", "install", "foo"])
`changesFile` "/var/lib/dpkg/status"
Here the cmdProperty normally returns MadeChange, so trivial is used to throw that innaccurate value away and the changesFile combinator checks for changes.
(The alternative would be for cmdProperty to normally return NoChange, and then have changesFile cause MadeChange to be returned. However, this approach has plenty of foot-shooting potential of its own, for example using cmdProperty and forgetting to check if it made any changes. If trivial is a code smell, making cmdProperty and similar generic property building tools trivial by default is surely not good..)
So, could this be fixed at the type level?
UncheckedProperty as an alternative to Property
Perhaps it would make sense to
have a UncheckedProperty, which could be used for things like
cmdProperty
. Combinators like changesFile
would convert it to a
Property.
(A trivial
combinator could still be written of course, but it wouldn't be
necessary in cases like the above example anymore, so it would be more
clearly a code smell to use trivial
.)
If UncheckedProperty was added, we'd want all the usual property
combinators to also work with it. Including requires
. This is entirely
doable, but it's going to need quite a lot of duplicated code.
For instance, there are 4 instances currently to handle combining properties with and without info; here's one of them:
instance Combines (Property HasInfo) (Property HasInfo) where
combineWith f _ (IProperty d1 a1 i1 cs1) y@(IProperty _d2 a2 _i2 _cs2) =
IProperty d1 (f a1 a2) i1 (y : cs1)
Adding UncheckedProperty to the mix, we need another 4 instances for combining two of those. Plus 4 more for Property + UncheckedProperty = UncheckedProperty. Plus 4 more for combining UncheckedProperty + Property! Each of those instances has to be implemented separately. The code duplication doesn't stop at instances; also need constructors for UncheckedProperty, etc.
extending Property
Another approach would be Property i Unchecked|Checked
. But that seems
overcomplicating for the end user, since most properties that users will
deal with are not checked.
minimal UncheckedProperty
Maybe add UncheckedProperty, but without the combining instances?
How about this simple interface:
unchecked :: Property i -> UncheckedProperty i
checkResult :: ResultCheckable p => IO a -> (a -> IO Result) -> p i -> Property i
-- Both Property and UncheckedProperty are ResultCheckable.
changesFile :: Checkable p => p i -> FilePath -> Property i
changesFile p f = checkWith getstat comparestat p
where
getstat = ...
comparestat old = do
new <- getstat
return $ if old == new then MadeChange else NoChange
Then, cmdProperty would construct a regular property, but apply unchecked
to it. Users of cmdProperty
would need to apply changesFile or a similar
check to it before combining it with any other properties.