Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

using EntwineTest to compare results when the underlying type is a tuple - kind of awkward #6

Closed
heckj opened this issue Jul 19, 2019 · 3 comments

Comments

@heckj
Copy link

commented Jul 19, 2019

First off - I'm the last to say I'm an expert at Swift, or the vagaries of type theory and effectively using a statically type language with generics like we have.

I was creating some tests to using EntwineTests virtual scheduler to illuminate how some of the Combine operators work - specifically combineLatest and some of the others that merge pipelines together. (ref: heckj/swiftui-notes#56)

What I hit my shins on was that the equatable conformance with Entwine's Signal relies entirely on the underlying type's conformance - totally good, except that the type can be a tuple (and is, in the case of combineLatest, which is merging the output types of the upstream pipelines.

I came up with two options to do the validation regardless, although both are really quite ugly hacks. I'm opening this issue to see if you had any brilliant insights into how this might be done more nicely.

The first was the use the same testing hack I use for comparing Error enums - using the debugDescription() method on the enumeration to generate a string, and comparing that. It's stringly-type-erasing the compiler details away, while providing fairly useful tests.

The second was to create a one-off function that specifically broke down a TestSequence item (itself a tuple of VirtualTime and a Signal enum instance) and return a boolean value if they match up.

Although it's functioning and correct, it feels ugly and awkward, so I was hoping you might have some ideas on how this might be made more elegant.

Although the timing was very useful to show for my combineLatest tests, I also realized that the key of what I was interested in validating was the ordered list of data results, and a lot of the type information around it wasn't as interesting for my specific test.

I'm not sure if there'd be significant value in making a helper operator to pull out just a sequence of the OutputType and make that available from under TestSequence, but it might be useful as an idea.

@tcldr

This comment has been minimized.

Copy link
Owner

commented Jul 20, 2019

Sure, I'll comment on the issue you linked to also, but so there's a reference here:

Tuples in Swift still leave a little to be desired, they don't auto-synthesise Equatable or Hashable conformance, and in fact there is no generic way you can conform a tuple to these protocols. From the discussions I've read from the Swift team, tuples often add an additional layer of complication when they're implementing new features and seem to be a general thorn in their sides!

However, they are particularly ergonomic from a coding perspective, so hopefully they'll resolve these issues at some point.

With EntwineTest, the solution I use to make the TestSequence conform to Equatable is to map the tuples to an intermediate struct, I then make that struct conform to Equatable and compare those instead.

You could generalise it, but as I'm only using it in the one place I didn't want to bloat the codebase.

But it's simple enough to do.

The theory behind it is that any tuple can also be represented as a struct. So if you can create an intermediate struct that maps from a tuple and back, you then should have all you need to conform to whatever behaviours you need.

The big thing to remember is that you will need an intermediate struct for each arity of tuple. So for two, three, and four element tuples, you will need the intermediate structs of:

Tuple2<T0, T1>
Tuple3<T0, T1, T2>
Tuple4<T0, T1, T2, T3>

In terms of implementation it's as simple as:

struct Tuple2<T0, T1> {
    
    let t0: T0
    let t1: T1
    
    init(_ tuple: (T0, T1)) {
        self.t0 = tuple.0
        self.t1 = tuple.1
    }
    
    var raw: (T0, T1) { (t0, t1) }
}

And then Equatable and Hashable conformance is just:

extension Tuple2: Equatable where T0: Equatable, T1: Equatable {}
extension Tuple2: Hashable where T0: Hashable, T1: Hashable {}

Specifically with Entwine, though I think the generalised Tuples solution is probably outside the scope, what would probably help is some additional mapping functions so If you have made some intermediate structs – you can use them with ease.

Enabling something like:

    func testTuples() {
        
        let testScheduler = TestScheduler(initialClock: 0)
        
        let testablePublisher: TestablePublisher<(Int, String), Never> = testScheduler.createAbsoluteTestablePublisher([
            (  0, .input((0, "A"))),
            (200, .input((0, "B"))),
            (400, .input((0, "C"))),
        ])
        
        let testableSubscriber = testScheduler.start { testablePublisher }
        
        let results = testableSubscriber.recordedOutput
        let expected: TestSequence<(Int, String), Never> = [
            (200, .subscription),
            (200, .input((0, "B"))),
            (400, .input((0, "C"))),
        ]
        
        XCTAssertEqual(results.mapInput(Tuple2.init), expected.mapInput(Tuple2.init))
    }

Where Tuple2 is the struct above.

Does that help? I'll push those mapping utils to master so you can try them out.

@heckj

This comment has been minimized.

Copy link
Author

commented Jul 20, 2019

thank you for the explanation, that helps considerably, and I'll definitely try out the utilities to map to structs and back. Once you know how to do this, and how to attack it (your description is excellent, for example) it becomes pretty clear to formulate a plan to put it all together.

There's a downside in that a lot of people won't either know or want to do the extra work involving making a struct for the intermediate tuple collections just to make them testable, so in the back of my head I'm a little concerned that they simply wont, and the powerful feature of this library would get missed or neglected (as well as crappier tests would tend to exist)

I apologize, as I'm sure it sounds like I'm complaining about the wonderful feature work and solutions you're providing here - that's not my intent. The core of the question in head is "how does this get made easily apparent to someone else coming in after me?" and I genuinely don't have a good answer there, even as it relates to the my writing/authoring work with Using Combine and other texts.

@heckj heckj closed this Jul 20, 2019

@tcldr

This comment has been minimized.

Copy link
Owner

commented Jul 22, 2019

Glad it was useful, @heckj! And I totally agree, fwiw: It's not completely obvious how to solve these issues when you come across them. The challenge is deciding what should be part of this library and what should be outside of it.

If you make use of tuples frequently, then including a set of intermediate tuple structs in your project is probably a good idea. Maybe there is room for a library that includes just these intermediate structs. Historically I've avoided libraries for smaller utilities that can be copy and pasted into a project though. Maybe a gist?

But as you say, it isn't going to be immediately obvious to most people that this is what they need. Intuition would likely suggest that if you can use the == operator on something, then whatever that thing is must conform to Equatable. Not so, however, and reaching that understanding can be a painful! (Speaking from experience.)

I think a large part of that is down to Swift's generic system actually quite a way from being 'feature complete'. The Swift team themselves have tried to outline a vague plan for generics, initially with the Swift Generics Manifesto, and then more recently Joe Groff's post on the Swift forums Improving the UI of Generics.

What this means in practice is that things that you expect to work intuitively, just aren't available yet. Most recently it took me a while to work out that using the new some keyword to return a publisher was going to pretty useless with publishers as there is no way of constraining to the publisher's output or failure types. However, if you look at the plan – it's likely to arrive at some point. (So eventually we can get rid of all of those .eraseToAnyPublisher() calls.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
2 participants
You can’t perform that action at this time.