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
Added CompositeDataSource
type
#11
Conversation
Reads to me like a standard update
7c3b20b
to
bf03566
Compare
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.
bf03566
to
32feb65
Compare
Without this, a consumer of this pod can't create arbitrary new types which conform to `DataSourceType`
Requesting review(s): @heiberg @eric-robinson @thorfroelich |
There was a problem hiding this 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 |
There was a problem hiding this comment.
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)") |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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() {} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 🤔
There was a problem hiding this comment.
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.
* `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
There was a problem hiding this 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:
DataSourceType
should be covariant over theItem
type. So given a transformationA -> B
we should be able to implement a transformation which can convert anyDataSourceType<A>
to aDataSourceType<B>
. This is essentiallymap
, so we might as well call it that.- 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.
I'm aligned with the approach above, however this new type was not meant to solve heterogeneous Composition. |
Depends on #10
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.