Complicated generics problem

I'm trying to reason through some generics and have hit a wall and was hoping to get some democratic wisdom going on my problem.

Here's what I'd like to do:

protocol Field { }
protocol Field1: Field { }
protocol Field2: Field { }
struct Value<F: Field> { ... }

_ = Value<Field1>()
_ = Value<Field2>()
_ = Value<Field1 & Field2>()

This, of course, does not work, because generics require a concrete type as the generic parameter, and protocols (even derived protocols) do not satisfy this requirement.

However, I can't change the Field1 and Field2 protocols to be structs, because then they can't be &'d together.

My current "workaround" is to not have a Value struct at all, but instead to have:

struct Field1Value: Field1 { ... }
struct Field2Value: Field2 { ... }
struct Field1Field2Value: Field1, Field2 { ... }

As it turns out, this means I end up defining nearly 2 dozen almost identical structs (because I have 8 different Field protocols). I'm hoping to come up with a more "elegant" way of defining these types, but I'm not seeing a way to do it.

Do you?

The actual error (which would have been useful if you included in your post) is:

“error: protocol type 'Field1' cannot conform to 'Field' because only concrete types can conform to protocols”

You can begin to work around this by removing the constraint from the declaration of the struct:

struct Value<F> { ... }

extension Value where F: Field { ... }

However, this still does not let Value<Field1> utilize the contents of the constrained extension.

What are you actually trying to achieve?

Hm, this is really interesting. Removing the : Field constraint on the generic type is possible in my situation, because my initializer will be internal to my library anyway...

Well, I'm really trying to avoid a whole bunch of copy-pasting of code. Right now I've got 29 copy-and-pasted structs that are all pretty much identical to each other, except for which protocols they conform to. This means that if I change one, I have to change 28 others, too. I'm trying to do all the "type-specific" variation based on constrained extensions, but the fundamental problem of "too many structs" still remains.

I've looked in to code-generation approaches (like Sourcery), but for simplicity I like to keep my dependencies at a minimum.

Yeah, I've been playing around with this in a playground and it looks like I'll run in to similar compiler problems when trying to do the constrained extensions.

extension Value where F: Field { ... }

Attempting to use a method/property from an extension like that on a Value<Field1> results in the same kind of compiler error:

Using Field1 as a concrete type conforming to protocol Field is not supported

Well, if that is indeed your actual goal, then the simplest solution is to erase the entire file. No code at all, hence no copy-pasting.

• • •

I repeat my question: what are you actually trying to achieve?

1 Like

You have only showed us a small part of the solution you want to use (but can’t because of the current type system). You haven’t described the problem you are trying to solve in much detail though. We need more information in order to help.

There's no need to be flippant.

My goal is exactly what I said. Right now I've got 29 identical structs that differ only in which protocols they conform to (from GitHub - davedelong/time: Building a better date/time library for Swift)

(Identical initializers omitted for brevity)

public struct Era: EraField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct Year: EraField, YearField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct YearMonth: EraField, YearField, MonthField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct YearMonthDay: EraField, YearField, MonthField, DayField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct YearMonthDayHour: EraField, YearField, MonthField, DayField, HourField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct YearMonthDayHourMinute: EraField, YearField, MonthField, DayField, HourField, MinuteField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct YearMonthDayHourMinuteSecond: EraField, YearField, MonthField, DayField, HourField, MinuteField, SecondField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct YearMonthDayHourMinuteSecondNanosecond: EraField, YearField, MonthField, DayField, HourField, MinuteField, SecondField, NanosecondField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct Month: MonthField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct MonthDay: MonthField, DayField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct MonthDayHour: MonthField, DayField, HourField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct MonthDayHourMinute: MonthField, DayField, HourField, MinuteField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct MonthDayHourMinuteSecond: MonthField, DayField, HourField, MinuteField, SecondField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct MonthDayHourMinuteSecondNanosecond: MonthField, DayField, HourField, MinuteField, SecondField, NanosecondField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct Day: DayField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct DayHour: DayField, HourField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct DayHourMinute: DayField, HourField, MinuteField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct DayHourMinuteSecond: DayField, HourField, MinuteField, SecondField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct DayHourMinuteSecondNanosecond: DayField, HourField, MinuteField, SecondField, NanosecondField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct Hour: HourField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct HourMinute: HourField, MinuteField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct HourMinuteSecond: HourField, MinuteField, SecondField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct HourMinuteSecondNanosecond: HourField, MinuteField, SecondField, NanosecondField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct Minute: MinuteField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct MinuteSecond: MinuteField, SecondField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct MinuteSecondNanosecond: MinuteField, SecondField, NanosecondField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct Second: SecondField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct SecondNanosecond: SecondField, NanosecondField {
    public let region: Region
    public let dateComponents: DateComponents
}
public struct Nanosecond: NanosecondField {
    public let region: Region
    public let dateComponents: DateComponents
}

In my library, these types are organized in 13 different files, which makes them difficult to discover. It also over-emphasizes their relative importance compared to the rest of the library. These are, fundamentally, simple model objects. However, the fact that they're scattered across so many files makes the overall domain seem more complicated than it already is. It increases maintenance time and mental load.

I would love to find a way to have a single CalendarValue<...> type, where the generic parameters indicate the particular values/constraints/concepts represented by the value. This would mean I could focus on the parts of my library that are actually useful, instead of the bookkeeping necessary to make sure I'm keeping all 29 structs in sync.

1 Like

I’m not positive I understand what you’re trying to do, but here’s one approach that might work:

public struct CalendarValue<T> {
    public let region: Region 
    public let dateComponents: DateComponents
}
public enum Era: EraField {}
extension CalendarValue: EraField where T: EraField {}
public enum Year: YearField {}
extension CalendarValue: YearField where T: YearField {}
public enum YearMonth: YearField, MonthField {}
extension CalendarValue: MonthField where T: MonthField {}

This uses empty enums as “tags” that carry intended conformance information so CalendarValue<Era> has the necessary conformances, and so on. You will need to declare 29 of these, but they are now just tags used with a single CalendarValue used for runtime values instead of being totally independent structs.

2 Likes

Hm, this is really interesting, thank you!

Your use of the term "tag" also seems appropriate. What I'm kind of trying to do is create a single "generic" type that I can "tag" with various protocols, and have that type automatically "grow" the appropriate properties and methods (via constrained extensions) based on what it's been tagged with.

1 Like

You’re welcome! Glad it sounds like it will work. :slight_smile:

Yeah, that’s what I was guessing. It looks like it might work well in your case since all variations of this type store data of the same types. Do you plan to put initializers in the constrained extensions which guarantee invariants based on the tags? If so, one potential issue you might run into is that any initializers constrained to YearField will be available on CalendarValue<YearMonth>.

4 Likes