The example is simplified. In reality Date instance stored with TimeZone and Calendar and is relevant to user's delivery address locale, not device local date / time / timezone.
I agree here
The example is simplified. In reality Date instance stored with TimeZone and Calendar and is relevant to user's delivery address locale, not device local date / time / timezone.
I agree here
Completeness is attractive, but more recently I have begun to question it. Any feature added must be maintained, and maintaining unused things is a waste of time.
The questions I have started asking myself is "what problem does this solve", and "is this a real problem"
I have the same questions. In typed-throws proposal multiple types are mentioned as a future direction. The provided example is:
func fetchData() throws(FileSystemError | NetworkError) -> Data
While it a reasonable case, I personally meet the need of sum type in rather rare circumstances.
The most robust motivation for sum types for now (AFAIK) is typed throws / Results. As it said in proposal the feature will be introduced in generalized form.
The sharp edge I see here is that sum type of errors is not the same as sum type of any types.
When we combine (FileSystemError | NetworkError | DataProcessingError) – the whole type is a subtype of error.
But if the feature is introduced in generalized form then something like (String | Calendar | NSObject) will also become possible which is totally different from case with errors.
One possible solution I see is to allow combining only related types so all members of sum type have common super type. But I can't see how it can be achieved, because it is easy to make empty protocol and conform it to every type.
Imagine a project with several related, but different types:
struct MuseumTicket {}
struct BusTicket {}
struct TrainTicket {}
struct TheaterTicket {}
struct FamilyParkTicket {}
struct TravelTicket {}
struct CharityTicket {}
...
Though all the types have ticket suffix, they represent totally different abstractions. Some of them have price property and some don't. Some of them can be personal, group or anonymous.
It is easy to combine something like (BusTicket | FamilyParkTicket | CharityTicket), but such design is not so far from Any typecast or ObjC ID type.
So, the questions are:
It actually says:
Trying to introduce multiple thrown error types directly into the language would introduce nearly all of the complexity of sum types, but without the generality, so this proposal only considers a single thrown error type.
I do not understand it as a commitment, but I see typed throws kind of limited as long as multiple thrown error types are not realized, so I think sum types should be coming, if only for this reason, possibly in some limited form as considered by you (with a common supertype).
Well, I have as an example a result builder where the content could be a String or a class instance of a common root type, or sequences or arrays of those, and recursively so. I am then working with a protocol and when consuming the content, a fatal error is used if getting something that is not expected (no exhaustive switch statement without default possible).
In case you are wondering what crazy stuff I am doing, as already mentioned above, this is part of a library for processing/transforming XML, the protocol is defined there, and the result builder is there, for the fatal error for unknown content see there. See the comment above of why some proposed solutions do not work. If there is indeed better solution, of course any help is welcome, maybe I missed something, but my suspicion is that sum/union types should help here. Maybe “closed protocols” (which cannot be used for types outside the library) might be of some help. Enums are not sensible to use, because I want an easy syntax when using the result builder, some new tricky sugaring for enums could help, but I am not sure about this. In any case I have a situation which seemingly (i.e. to my knowledge) cannot be expressed satisfactorily in current Swift.
…Maybe this is the case with my example, that I somehow do not see the solution. But then from an intuitive thinking, it should be expressible with sum/union types (of a recursive kind), so I wish there is this magical tool that can solve my problem in a direct, easy way. But…
…the thing with a “magical” solution is that there is no magic in a programming language (at least not in an efficient one). General sum/union types without limitations might be “too expensive”, especially when people start using such a feature excessively, or crazy people like me demand recursive definitions.
Well, TypeScript has the magic that I think I need, namely recursive union types (formulation "recursive union types" by me):
type Result = number | string | Result[]
function processResult(result: Result) {
if (Array.isArray(result)) {
result.forEach((item) => {
processResult(item)
})
}
else if (typeof result === "number") {
console.log("number " + result)
}
else if (typeof result === "string") {
console.log("string " + result)
}
}
processResult([1, "hello"]) // prints "number 1" and "string hello"
processResult([1, true]) // error: Type 'boolean' is not assignable to type 'Result'.
...but of course I want to have switch (as for enums) and not if ... else ... in Swift. So a union type for me is like an enum with associated values, but without the ceremony (and allowing recursive definitions).
For this example enum:
struct S { var value = 123 }
enum E {
case a
case b(Int)
case c(S)
case d(S, String)
case e(s: S, str: String)
}
extension E {
var a: E! {
get {
if case .a = self { return self }
else { return nil }
}
set {
precondition(newValue != nil)
self = newValue
}
}
var b: Int! {
get {
if case let .b(v) = self { return v }
else { return nil }
}
set {
precondition(newValue != nil)
self = E.b(newValue)
}
}
var c: S! {
get {
if case let .c(v) = self { return v }
else { return nil }
}
set {
precondition(newValue != nil)
self = E.c(newValue)
}
}
var d: (S, String)! {
get {
if case let .d(x, y) = self { return (x, y) }
else { return nil }
}
set {
precondition(newValue != nil)
self = E.d(newValue!.0, newValue!.1)
}
}
var e: (s: S, str: String)! {
get {
if case let .e(x, y) = self { return (x, y) }
else { return nil }
}
set {
precondition(newValue != nil)
self = E.e(s: newValue!.s, str: newValue!.str)
}
}
}
would result into this neat and tidy syntax:
var v: E = E.a
v.a = E.a
// coercing to Any below to just get rid of warning:
// Coercion of implicitly unwrappable value of type 'Int?'
// to 'Any' does not unwrap optional
print(v.a as Any) // Optional(App.E.a)
print(v.b as Any) // nil
v.b = 42
print(v.b as Any) // Optional(42)
v.c = S()
print(v.c as Any) // Optional(App.S(value: 123))
print(v.c.value) // 123
v.c.value = 42
v.d = (S(), "hello")
print(v.d as Any) // Optional((App.S(value: 123), "hello"))
print(v.d.0) // S(value: 123)
print(v.d.1) // hello
print(v.d.0.value) // 123
v.d.0 = S()
v.d.0.value = 42
v.e = (S(), "hello")
print(v.e as Any) // Optional((s: App.S(value: 123, str: "hello"))
print(v.e.s) // S(value: 123)
print(v.e.str) // hello
print(v.e.s.value) // 123
v.e.s = S()
v.e.s.value = 42
Would that enum syntax work for you?
No, see the example at the top of the README file:
let transformation = XTransformation {
XRule(forElements: "table") { table in
table.insertNext {
XElement("caption") {
"Table: "
table.children({ $0.name.contains("title") }).content
}
}
}
XRule(forElements: "tbody", "tfoot") { tablePart in
tablePart
.children("tr")
.children("th")
.forEach { cell in
cell.name = "td"
}
}
}
See how the <caption> element is being composed.
I've done some changes, see the commit: Remove fatal error. Remove dynamic casts. Make XContentLike protocol unimplementable outside the library
I don't know all design specifics of the library and provided second variant in comments.
There are some other thoughts how the task can be done without sum types, but those approaches require design changes and other features like variadic generics.
Note that this solution is built with polymorphism and suit to SRP – if one day a new XContentLike -type appears then no changes in XNodeSampler class are needed. If union types are used then XNodeSampler class need to be changed every time.
Thanks, I will have a look at it.
Could you elaborate on this?
Update (to all): The questions regarding this specific library might seem like a derivation from the original topic, but maybe is an interesting use case. With the proposed changes I think the described problems can be solved, but at the price of some “artificial” looking ceremony (admittedly ceremony internal to the library, but also lurking to the public API). So still, I would like to have (recursive) sum types.
Update 2 (@Dmitriy_Ignatyev and to all): With the proposed design changes / respecting SRP it might be OK if XContentLike could be conformed to for other types outside the library, as the author is then responsible for correctly implementing addToSampler, so I think the proposed changes are good without the InternalUsageLimiter and the “ceremony” is gone. So maybe I do not “need” sum types here. I would “need” sum types (to avoid the additional ceremony and the publicly lurking __internal_usage_limiter) if I would like to avoid conformances to XContentLike as was the situation with my design. So this is maybe not a good example for needing sum types, but includes a hint when sum types might be helpful (outside the case of typed throws).
Got you. Then perhaps the further two sugar could help:
var v: E = E.a
// this we can have today:
v = E(42) // E.b(42)
v = E("Hello") // E.f("Hello")
v = E(S()) // E.c(S())
// this would require a compiler change:
v = 42 // E.b(42)
v = "Hello" // E.f("Hello")
v = S() // E.c(S())
If there's an ambiguity that'd be an error like:
v = E(42) // 🛑 Ambiguous usage, use explicit "discriminator" form
v = 42 // 🛑 Ambiguous usage, use explicit "discriminator" form
v.b = 42 // ✅
This does not help me building a nice API.
Well, you said before:
Which makes me wonder: what if they did, will there still be something missing between union types (like they are in other languages) and Swift's enums if they taught some new tricks to be easy syntax wise.
enum { case string(String), int(Int), tableContent(TableContent) }
XElement("caption") {
"Table: " // a shorthand for .string("Table: ")
42 // a shorthand for .int(42)
table.children(...).content // a shorthand for .tableContent(....)
}
I don't think result builders make a great motivating example. There's no need for a type union as the input since you can take advantage of overloading. For instance:
enum Element { case string(String), int(Int), tableContent(TableContent) }
@resultBuilder
struct Things {
static func buildExpression(_ expression: String) -> Element {
return .string(expression)
}
static func buildExpression(_ expression: Int) -> Element {
return .int(expression)
}
static func buildExpression(_ expression: TableContent) -> Element {
return .tableContent(expression)
}
// ...
}
Compare your code with a result builder for a single type that has been defined by a definition like from the above TypeScript code type Result = number | string | Result[] and using an exhaustive switch statement (without default case) in the result builder. In your code, by using several buildExpression functions, you are “virtually” creating a type (everything that can be processed by a buildExpression function) without explicitely stating that type. For expressiveness it might be better to instead define that type.
Having such an explicitly defined type also gives you the possibility to refer to that type elsewhere in your code, where you might want to allow anything that can go into your result builder (without actually using the result builder at this place).
BTW for the XML library the enum cases would actually be something like case content(XContent); …; case contentSequence(XContentSequence); … (there would be no tableContent case as this is a general XML library and not an HTML library). But I would also like to accept according sequences of sequences etc., compare the recursive definition in TypeScript, I do not think you can have such a recursive definition with enums. (Compare the current implementation in the XML library.)
Is such example cover the case?
enum AnyDecodableValue: Codable, CustomStringConvertible {
case dict([String: Self])
case array([Self])
case primitive(AnyDecodablePrimitiveValueStub)
}
// dictionary of dictionaries of arrays of dictionaries and so on... can be expressed
enum AnyDecodablePrimitiveValue: Codable {
case string(String)
case int(Int64)
case uint(UInt64)
case bool(Bool)
case double(Double)
case null
}
It is made for decoding any possible json but similar approach can be done for xml I suppose.
update:
May be something like protocol XContentRepresentable can be done.
protocol XContentRepresentable {
var contentRepresentableValue: XContentRepresentableValue { get }
}
enum XContentRepresentableValue {
case content(XContent)
case contentArray([XContent])
case array([Self])
}
Fine, I see the recursiveness. So the question about “recursive enums” should be clarified, thanks.
But enums would only be a choice if they could be used as described in the above comment by @tera. I already said above, sum/union types for me is something like enums, but with a nicer syntax (see the same comment by @tera).
In conclusion, I see strong arguments for (recursive) sum/union types with the nice “|” syntax, I have the strong feeling that things like the buildExpression functions for state builders discussed above or enums where all cases have accociated values are a unsatisfactory replacement for a tool that seems very natural (and is part of other languages), and concerning questions of efficiency, my naïve suspicion is, if enums are efficient than why should union types be less efficient.
I would say if language is expressive enough for tasks to be doable then there are no reasons to add new feature.
Along that line some recent changes for Swift which make the language much “nicer” would not have happened.