Skip to content

Instantly share code, notes, and snippets.

@jml

jml/matchers.md Secret

Last active December 18, 2015 12:04
Show Gist options
  • Save jml/3a30695b6c22d98f1be0 to your computer and use it in GitHub Desktop.
Save jml/3a30695b6c22d98f1be0 to your computer and use it in GitHub Desktop.

Matchers

General type thoughts

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.

Python consequences

Matcher as functor

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))

Mismatch as data type

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.

Mine Data.Maybe for Mismatch helpers

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.

Match (aka NonMismatch)

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?

Simplest possible matcher

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
@jml
Copy link
Author

jml commented Dec 18, 2015

testing-cabal/testtools@master...jml:mismatch-subclasses starts deleting the subclasses. Already has reduced LoC (it looks bigger because of documentation).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment