Type is a -> Maybe Mismatch
, where a
is the type of the object being asserted about.
Can think of this as:
newtype Matcher a = Matcher (a -> Maybe Mismatch)
This is at least a functor, where
instance Functor (Matcher a) where
fmap f (Matcher g) = g . f
i.e. that fmap
is equivalent to AfterPreprocessing
.
It's unclear whether it's also Applicative
or Monad
. If it were, pure
would be:
pure _ = Matcher (const None)
Alternatively, is b -> a -> Maybe Mismatch
, where b
is the input that defines what is being matched against.
Also, Mismatch
could be thought of as being a Mismatch a
, where a
is the thing being asserted about. Note the Python implementation of Mismatch
doesn't actually store the object that mismatched.
AfterPreprocessing
implements functor-like behaviour. We could make this more obvious by adding an after
method to the Matcher
base class.
e.g.
class Matcher(object):
# ...
def after(self, f):
return AfterPreprocessing(f, self)
This would be useful inline:
self.assertThat(thing, Equals(42).after(len))
But less useful for defining new matchers.
For that, we could perhaps use a classmethod:
class Matcher(object):
@classmethod
def after(cls, f):
return lambda *args, **kwargs: AfterPreprocessing(f, cls(*args, **kwargs))
testtools currently inherits from Mismatch
an awful lot. This is mostly unnecessary. Instead, we should almost always construct and return Mismatch
directly.
We could support this by adding an on_mismatch
method to Matcher
base, e.g.
self.assertThat(thing, Equals(42).after(len).on_mismatch(lambda m: Mismatch("Wrong thing: " + m.describe())))
There are implementation complexities to sort out, but they are resolvable. As for .after
, we may want to make it a class method to allow for factories.
In particular:
catMaybes :: [Maybe a] -> [a]
mapMaybe :: (a -> Maybe b) -> [a] -> [b]
Probably there are also relevant fold functions.
Some of our higher-order matchers might perhaps be better facilated by functions that operate on sequences and dictionaries of Mismatch
objects.
For example, a Python function that takes a list of potential mismatches and returns None
if all of them pass, or a combined mismatch of the ones that don't.
Or a Python function that returns None
if any of them pass, or a combined mismatch of all of them.
The Nothing is Something talk by Sandi Metz talks about defining specific objects that implement the behavior you want when "nothing" happens.
In the case of matchers, "nothing" is when we get a match. What could we gain by having such an object?
For algebraic completeness, it is almost certainly worth defining the simplest possible matcher:
class _Always(Matcher):
def match(self, object):
return None
ALWAYS = _Always()
The next simplest is this:
class _Never(Matcher):
def __init__(self, mismatch):
super(_Never, self).__init__()
self._mismatch = mismatch
def match(self, object):
return self._mismatch
never = _Never
testing-cabal/testtools@master...jml:mismatch-subclasses starts deleting the subclasses. Already has reduced LoC (it looks bigger because of documentation).