FAFO on GitHub

Having prepared better than we did yesterday morning, let’s proceed with wrapping our frozenset with a class of our own.

Aside
I know better than to use system collections for specialized purposes. I think I was just so interested in whether I could make good use of frozenset that I didn’t think whether I should. Where was Dr Ian Malcolm when I needed him? Anyway, we’re on our way to wrapping that system set with our own class now. We do not expect to get things right the first time: we expect to make things better, repeatedly, until satisfied.

Today, I hope to be satisfied with what I get, as I was yesterday afternoon … if not the morning.

Added in Post
Yes!

I believe that we have our access to the implementation member of the XSet down to just four methods, one of which is optional. They are __contains__, __iter__, __hash__, and the optional __repr__, which is a method that formats an object for printing, as seen by the programmer. There’s another method for printing a more user-oriented view. Weird, but interesting. Or do I mean interesting, but weird?

I think we will begin with an abstract class, so that when we build our classes, we are reminded to implement these methods. In the case of our wrapper for the frozenset, I think we’ll be able to just forward to the frozenset. Other implementation classes may require a bit more work.

I think it is time to break out my classes into separate files. I should do even more than that, because, as things stand, I haven’t separated tests from implementation. Perhaps you won’t mind waiting a few moments while I organize things.

OK, I’ve learned a few things about moving things to files. Main learning: make a package, not just a folder. Now let’s build our abstract class in a new source file, x_impl.py.

It has been a while since I made one of these. I think this is good:

from abc import ABC, abstractmethod

class XImplementation(ABC):
    @abstractmethod
    def __contains__(self, item):
        return False

    @abstractmethod
    def __iter__(self):
        raise NotImplemented

    @abstractmethod
    def __hash__(self):
        raise NotImplemented

    @abstractmethod
    def __repr__(self):
        raise NotImplemented

Commit: Initial abstract class XImplementation.

I think there’s nothing for it now, but to try to build our new implementation. Let’s have some tests for it to begin with. I start with this:

from src.x_impl import XImplementation

class XFrozen(XImplementation):
    pass

class TestXFrozen:
    def test_exists(self):
        fs = frozenset((1, 2, 3))
        frozen = XFrozen(fs)

PyCharm objects that my new class does not implement the abstract methods, exactly my cunning plan, and also warns me that XFrozen does not yet expect a parameter. No argument1, I comply:

class XFrozen(XImplementation):
    def __init__(self, fs):
        self.data = fs

    def __contains__(self, item):
        return item in self.data

    def __iter__(self):
        return iter(self.data)

    def __hash__(self):
        return hash(self.data)

    def __repr__(self):
        return repr(self.data)

I rather expect this to be what I need. Let’s do some tests though. I think I’ll just construct some XSets, replace their frozenset with an XFrozen, and test them. They should work normally.

class TestXFrozen:
    def test_exists(self):
        fs = frozenset((1, 2, 3))
        frozen = XFrozen(fs)
        assert 1 in frozen
        assert 4 not in frozen

    def test_in_use(self):
        xs = XSet.classical_set(("a", "b", "c"))
        xs.implementation = XFrozen(xs.implementation)
        assert xs.includes("a", None)
        assert xs.excludes("a", 1)
        assert xs.excludes("d", None)
        elements = set()
        scopes = set()
        for e,s in xs:
            elements.add(e)
            scopes.add(s)
        assert elements == {"a", "c", "b"}
        assert scopes == {XSet.null}

Green. Commit: initial tests and class XFrozen(XImplementation).

It would take more will power than I have not to plug the XFrozen in as our standard implementation and see what happens. Here goes:

class XSet:
    def __init__(self, a_list):
        def is_2_tuple(a):
            return isinstance(a, tuple) and len(a) == 2

        self.implementation = frozenset(a_list)
        if not all(is_2_tuple(a) for a in self.implementation):
            raise AttributeError

That becomes:

    def __init__(self, a_list):
        def is_2_tuple(a):
            return isinstance(a, tuple) and len(a) == 2

        self.implementation = XFrozen(frozenset(a_list))
        if not all(is_2_tuple(a) for a in self.implementation):
            raise AttributeError

46 tests run. One fails, calling for XFrozen to understand len. Let’s remove that check: I’m not sure yet how to handle set length / cardinality. That done, all 47 tests run … even the one that is wrapping an XFroozen in an XFrozen. I should fix that up, but this is too good a place to stop.

Commit: XFrozen is now the base implementation for XSets. All green! Life is good!

Let’s sum up.

Summary

The thing that I tried to bash in Sunday morning, with lots of learning but no success, went in smoothly in two short sessions, one Sunday afternoon and one Monday morning. Is this good news about Sunday afternoon and Monday morning, or bad news about Sunday morning?

On Sunday morning, my intuition was that wrapping the frozenset with a class of our own was really pretty trivial. And the two sessions after that kind of prove that my intuition was correct: it was easy in sessions 2 and 3, after a failed attempt in session 1. What was different?

In session 1, I just wrapped the frozenset in an empty class and tried to work through the many failing tests. Along the way I found many things that needed to be done, but wasn’t able to untangle things enough to get them all working, and ultimately, I lost the thread. All the plates I had been spinning fell to the floor and broke.

But I had learned a lot about spinning plates during the process, and we applied that in sessions 2 and 3.

In session 2, we did two main things: we narrowed the interfaces as much as we could, notably by implementing is_subset and __eq__ with their set-theoretic definitions rather than deferring to the frozenset ability to check for subsets and equality. Along the way, we identified the four methods that remained in our interface between XSet and its implementations.

Then, in session 3, this one, we proceeded in a more strict, good practice style, first building an abstract lass for our implementations, demanding the four necessary methods, and then we wrote some tests for our new concrete implementation before plugging it in as the official base implementation. Those tests passed immediately. I think there were two main reasons why that was the case:

  1. We had implemented the key subset and equality methods in XSet, so that all that really had to work in an implementation was containment and iteration, both of which are trivial in our XFrozen class.
  2. We had been careful, using our abstract class, to ensure that we had all the methods we needed,

It almost had to work. And it did.

Is there a lesson here? Did I somehow “waste” Sunday morning?

I don’t think the time was all wasted. I needed to learn, if nothing else, that it wasn’t quite as simple as I thought it was. I might have been able to learn that by spending a few hours thinking rather than a few hours thinking and coding, but generally I’d rather learn from both thinking and coding, rather than just one or the other. Definitely not coding without thinking. and generally not thinking without coding. Coding keeps my thinking honest.

I think I’d grant that I was too optimistic, or too impatient, Sunday morning, and that going a bit slower might have let me finish this in two sessions rather than three. But that’s hindsight, and while I will dial down my child-like optimism a bit, I think that the only change I really “should” make would be to be more inclined to decide up front that something is an experiment and to be better prepared to ditch it at the end. That would reduce any disappointment with how the spike turns out: they’re not supposed to turn out to be good enough to keep.

Most of my peers will throw out a spike even if it looks perfect. I have great confidence that I can and will clean up and loose ends or infelicities in the code so I am willing—perhaps too willing—to keep something that started out as an experiment. If this is a flaw in my being, well, it’s not the worst one.

Bottom line, in three sessions, one full of learning and a bit of confusion, two demonstrating some of my better habits, we have converted our XSet implementation from direct use of system classes to using classes of our own. Along the way we have improved the interface between XSet and its implementations, formalized that interface in an abstract class, and even wrote a small but strong test of the new class.

We are better fixed to move forward with specialized implementations of XSet, one of the things we “need”.

Looking forward …

I honestly do not know where this path leads. I feel that I have here by far the “best” cut at Extended Set Theory that I’ve ever managed. It’s almost entirely useless at the moment, but it feels right in a way that few of my other attempts ever have. I want to push it to the point where it can do a reasonable amount of work, including some kind of file-based representation or the like.

I don’t know. I’ll keep pushing to see where this leads. Questions and comments are welcome, to my mastodon dot social account, ronjeffries

See you next time!



  1. He thinks he’s funny.