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

Added CompositeDataSource type #11

Conversation

xavi-matos
Copy link
Contributor

@xavi-matos xavi-matos commented Apr 21, 2021

Depends on #10

It works by generically wrapping two DataSourceTypes while conforming to the same protocol itself.
It subscribes to updates from both data sources, mapping the changes into it's own section space and publishing those updates to subscribers of the composite.
The tests show this mapping is working correctly.

@xavi-matos xavi-matos force-pushed the xmatos/added-CompositeDataSource branch from 7c3b20b to bf03566 Compare April 21, 2021 20:59
It works by generically wrapping two `DataSourceType`s while conforming to the same protocol itself.
It subscribes to updates from both data sources, mapping the changes into it's own section space and publishing those updates to subscribers of the composite.
The tests show this mapping is working correctly.
@xavi-matos xavi-matos force-pushed the xmatos/added-CompositeDataSource branch from bf03566 to 32feb65 Compare April 21, 2021 21:07
Without this, a consumer of this pod can't create arbitrary new types which conform to `DataSourceType`
@xavi-matos
Copy link
Contributor Author

Requesting review(s): @heiberg @eric-robinson @thorfroelich
¡thank you! 😬🙇🏻‍♂️🤎

Copy link

@eric-robinson eric-robinson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a core maintainer on this so just leaving comments. A few nits and one deeper question about the composite Item type.

@@ -0,0 +1,118 @@
import Foundation

public class CompositeDataSource<A: DataSourceType, B: DataSourceType>: DataSourceType

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some helper /// docs would be nice here since this is a bit of a niche type.


firstDataSourceSubscription = firstDataSource.updateHandler.subscribe { [weak self] update in
guard let self = self else { return }
print("firstDataSourceSubscription update: \(update)")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snip this and other print statements?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, thanks for the catch 🙇🏻‍♂️

  • Remove print statements

}
}

public extension CompositeDataSource

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seeing mismatches in access keyword pattern in this file. Suggested approach is:

extension MyType  {
   public …
}

This way we don't inadvertently make public helper methods that might be grouped in the same extension.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only mismatch is due to the requirement that extensions which conform to protocols cannot declare access levels, see screenshot:
No Access Declaration Allowed

I do personally prefer to save columns by grouping extensions by access level.
Let's see what the core maintainer(s) say.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for sure a nit, but I think it's best to follow the patterns establish in other parts of this library. https://github.com/Squarespace/simple-source/blob/master/Sources/DataSource.swift

@@ -60,6 +60,8 @@ public class IndexedUpdateHandler {

private typealias Token = String
private var observers = [Token : Observer]()

public init() {}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be public now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I needed it to be in another module in order to make a new type conforming to DataSourceType and utilizing this constructor for ease of conformance.
I wasn't actually able to conform with the line var updateHandler = IndexedUpdateHandler() until I did this.

{
enum Item {
case A(A.Item)
case B(B.Item)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The call site ergonomics are giving me pause. As a simple example, imagine we have an Int data source for "amounts", and a String data source for "names".. If we have to query the data source, then it's something like:

switch dataSource.item(at: indexPath) {
  case let .A(amount): …
  case let .B(name): …
}

The A/B really stick out for me and don't convey much meaning. Making a bespoke datasource generic over a custom enum can result in something like…

switch dataSource.item(at: indexPath) {
  case let .amount(amount): …
  case let .name(name): …
}

This much more semantic type information and is more understandable imo. I don't have a great idea on how to get closer to this though, just a thought/concern. @heiberg any thoughts on this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the pause, and wanted to bring in eyes before trying to perfect it.

Maybe changing the naming of the casing to case first(A.Item); case second(B.Item) is an improvement, or maybe it's more confusing 🤷🏻‍♂️
At least the language would be consistent with the constructor with that change though 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really sold on the forced Item type here. But I added a separate comment with a possible approach we could take for this.

Xavi Matos added 2 commits April 21, 2021 19:02
* `createDelta` is purely for convenience to simplify construction call sites.
* `send(update:)` seems like something that ought to be public in order for custom `DataSourceType`s to finely control their update signals
Copy link
Contributor

@heiberg heiberg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR @xavi-matos!

I think the idea of more composable data sources is really great, and well worth pursuing.

The proposed solution will definitely solve the issue for anyone with a need to simply concatenate two heterogeneous data sources. But I think the official repo should offer a more generalized way to transform and compose data sources.

In the general case the number of data sources might not be 2. And there might be specific requirements on the item type of the resulting composed data source, which is not customizable with the proposed solution (where the item type is locked to the case A, B enumeration).

I have not had time to explore this in detail, but I could see such a general solution implemented in two parts:

  1. DataSourceType should be covariant over the Item type. So given a transformation A -> B we should be able to implement a transformation which can convert any DataSourceType<A> to a DataSourceType<B>. This is essentially map, so we might as well call it that.
  2. It should be relatively easy to concatenate data sources of matching types by implementing a function with this signature: concatenate<T: DataSourceType>(T, ...) -> T and return a data source which handles the section offsetting. It could even be made an overload of + for projects that don't mind operator overloading, and be thought of a bit like array concatenation.

If we take such an approach we would not be limited to concatenating exactly two data sources, and we would gain more flexibility wrt. the type of the composed result.

Something like this would become possible:

let squares: MyDataSource<Square>
let circles: MyDataSource<Circle>
let triangles: MyDataSource<Triangle>

enum Shape {
    case square(Square)
    case circle(Circle)
    case triangle(Triangle)
}

// This is a DataSourceType with Item == Shape
let shapes = concatenate(
    squares.map(Shape.square),
    circles.map(Shape.circle),
    triangles.map(Shape.triangles)
)

We would need concrete return types for DataSourceType.map (maybe a MappedDataSource), and for concatenation (maybe concatenate becomes the init of a ConcatenatedDataSource).

How does everyone here feel about this strategy? I would be happy to pursue it further. Meanwhile I suggest we hold off on merging this more specialized approach.

@heiberg heiberg closed this Apr 27, 2021
@xavi-matos
Copy link
Contributor Author

I'm aligned with the approach above, however this new type was not meant to solve heterogeneous Composition.
I will proceed with my required solution sans this PR and see if I have anything new to propose to y'all.
¡Thanks for the 👀z! 🙇🏻‍♂️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants