Multiple Types

I think we're talking about different things.

The original quote of mine was:

Here, the "nice little emergent feature" I'm referring to is the ability to Optionals to compose into nested optionals (Optional.some(Optional.some(something))), and its utility in modelling the distinction between "missing" and "present but nil".

From what I understood of your original response, you're suggesting that type unions (or "non-nominal enums") could also do that, but I just don't see how.

You would have to forbid associativity of | in order to achieve that result. Mathematically (T | Nil) | Nil is equivalent to T | Nil | Nil which is equivalent to T | Nil, but what you're really trying to express with (T | Nil) | Nil is that (T | Nil) is the .0 element and Nil is the .1 element of this anonymous thing.

3 Likes

A use case for union types: I am using a result builder which accepts (for good reason) very different types as the content of an XML element:

let element = XElement("paragraph") {
    "Hello "
    XElement("bold") { "world!" }
    " "
    otherParagraph.content
}

I am using a protocol to specify what is allowed as content, but then I have to use a switch statement where I have to handle all cases and be careful not forget a case.

Enums won‘t help because they do not allow the easy syntax above.

Update: I actually think I would like to have enums with some syntax trick.

No problems:

((String | none) | none)

I'm suggesting that this is enums, but without a name. A suggested spelling:

(first | second)
// enum AnonymousEnumWithoutAssoicatedValues {
//    case first, second
// }

(first: Type | second)
// enum AnonymousEnumWithAssociatedValues {
//    case first(Type)
//    case second
// }

(Type | AnotherType)
// enum AnonymousEnumWithoutLabels {
//    case 0(Type)
//    case 1(AnotherType)
// }

(Type | (AnotherType | none))
// enum NestedAnonymousEnum {
//    case 0(Type)
//    case 1(Nested)
//    
//    enum Nested {
//        case 0(AnotherType)
//        case none
//    }
// }

But I can also envision other spellings.

However, the feature set it the same as for enums, except that they cannot be extended or have non-case members — just like unnamed tuples correlate to nominal structs without extensions, etc.

3 Likes

Is there a reason you can’t have a custom type that’s ExpressibleByStringLiteral?

How would compiler know if second is a case and AnotherType is a type? Do you suggest to do a lookup and if the type is found then it's a type? I don't think we have precedents like that, so I guess we'd like to have some different spelling to distinguish these two cases, bike shedding:

(:Type | :AnotherType)
(_:Type | _:AnotherType)
(0:Type | 1:AnotherType)

or the other way around:

(case first, second)
1 Like

Sure, the spelling probably has shortcomings. I just wanted to explain how a non-nominal enum is both useful, would fill a gap, and could work as a kind of union type. But I'm sure the spelling has room for improvements.

1 Like

I'd just like to share my bike-shedding;

Since this is anonymous sum type in contrast to anonymous product types (tuples), we can use brackets like syntax, so that we can have analogous syntax. If you use | as bracket, then you can write something like this.

// equal to enum {case 0(T), 1(U), 2(V)}
//   like (T, U, V) is struct {var 0: T, 1: U, 2: V}
|T, U, V|  
// equal to enum {case first(T), second(U)}
//   like (first: T, second: U) is struct {var first: T, second: U}
|first: T, second: U|
// enum {} and can be alias for `Never`
//   like () is `Void`
||

// unanalogous but equal to enum {case none, some(T)}
|none:, some: T|

And this can be naturally extended for variadic generics;

|repeat each T|
1 Like

Another bikeshedding attempt:

(nope() | opacity(Float) | Color | rgb(Int, Int, Int))

or something else for outer brackets and inner separator.
The idea is to use empty parens to denote cases without associated types (like nope() above) and use bare name to denote types (like Color above).

use site usage:

func foo(_ v: (nope() | opacity(Float) | Color | rgb(Int, Int, Int))) {
    switch v {
        case .nope: // or case .nope():
        case let .opacity(value): // or case .opacity(let value):
        case let value as Color:
        case let .rgb(r, g, b): // or case .rgb(let r, let g, let b):
    }
}

Another possible solution in your case is to add a buildExpression method to your resultBuilder for each acceptable type.

4 Likes

right now in some code that deals with the swift @available mixin, i have three “union” enums that represent the range of versions that an API is deprecated for:

extension Availability
{
    @frozen public
    enum EternalRange
    {
        case unconditionally
    }
}

extension Availability
{
    @frozen public
    enum AnyRange:Equatable, Hashable, Sendable
    {
        case unconditionally
        case since(SemanticVersionMask?)
    }
}

extension Availability
{
    @frozen public
    enum VersionRange:Equatable, Hashable, Sendable
    {
        case since(SemanticVersionMask?)
    }
}

this is because only some deprecation ranges make sense for various availability domains:

extension Availability
{
    @frozen public
    enum AgnosticDomain:String, CaseIterable, Hashable, Equatable, Sendable
    {
        case swift = "Swift"
        case swiftPM = "SwiftPM"
    }
}
extension Availability.AgnosticDomain:AvailabilityDomain
{
    public
    typealias Deprecation = Availability.VersionRange
    public
    typealias Unavailability = Never
}
extension Availability
{
    @frozen public
    enum PlatformDomain:String, CaseIterable, Hashable, Sendable
    {
        case iOS
        case macOS
        ...
    }
}
extension Availability.PlatformDomain:AvailabilityDomain
{
    public
    typealias Deprecation = Availability.AnyRange
    public
    typealias Unavailability = Availability.EternalRange
}

and so forth. i feel like all these {Eternal, Any, Version}Range types are just a workaround for a lack of anonymous union types.

This would probably solve the issue for String, but this can only be part of a good solution. But thank you for the hint.

Thank you for the hint, I will evaluate this, maybe this is the solution I need. [UPDATE: This cannot cope with the recursive nature of some of the things I would like to use via my result builder, but may help with some edge cases.]

…So maybe I get this resolved, but I have the feeling that something “more easy”, “more straightforward” is missing here and that some kind of union types (as an enhancement to enums?) is missing here for a more natural way to cope with such cases (although I have to confess I didn't think it through in any systematic way).

The crucial point here is that the idea for my problem (and I suppose for many similar problems) is “those are the types I want to let people put in here” (without the syntactical “ornaments” needed for enums), and I would like to express this in a clear and easy way, but what I need is knowledge about the special structure (result builders) used which is not really a clear expression of intent.

I've recently worked on a ts project, and I really like ts unions!

I think we can extend the current Type Parameter Packs to implement Type Union, we can actually write code that almost works now, but it's not elegant enough:

struct Union<each T: Equatable>: ExpressibleByValue {
	// ExpressibleByValue is something we are missing today

	let value: (repeat (each T)?)
	init(value: repeat (each T)? = nil) { // = nil is missing today
		self.value = (repeat each value)
	}

	static func ~=(pattern: (repeat (each T)?), value: Self) -> Bool {
		for (l, r) in repeat (each pattern, each value.value) { // It's not possible today.
			guard l == r else { return false }
		}
		return true
	}
}

* Note that adding default values for optional Type Parameter Packs is not a worthwhile exercise since there is a String | String case, but for Union, String | String is meaningless so it can be done.

And the usage is similar to:

let union: String | Int = 1
// `String | Int` is the syntactic sugar for union
// `= 1` is provided by ExpressibleByValue, equivalent to (nil, 1)

switch union {
case let value as String:
	print(value)
case let value as Int:
	print(value)
}

enum NetworkError {
	case deviceNotFound
	case wifiNotConnect
}

enum ParserError {
	case rootIsArray
	case typeIsUndefined
}

let error: NetworkError | ParserError = .typeIsUndefined

switch error {
case .deviceNotFound:
	print("deviceNotFound")
case .wifiNotConnect:
	print("wifiNotConnect")
case .rootIsArray:
	print("rootIsArray")
case .typeIsUndefined:
	print("typeIsUndefined")
}

If I remove the ExpressibleByValue and the syntactic sugar, the above code works.


struct Union<each T: Equatable> {
	let value: (repeat (each T)?)
	init(value: repeat (each T)?) {
		self.value = (repeat each value)
	}
}

let union: Union<String, Int> = .init(value: nil, 1)

switch union.value {
case let (.some(value), nil):
	print(value)
case let (nil, .some(value)):
	print(value)
default:
	break
}

enum NetworkError {
	case deviceNotFound
	case wifiNotConnect
}

enum ParserError {
	case rootIsArray
	case typeIsUndefined
}

let error: Union<NetworkError, ParserError> = .init(value: nil, .typeIsUndefined)

switch error.value {
case let (.some(value), nil):
	switch value {
	case .deviceNotFound:
		print("deviceNotFound")
	case .wifiNotConnect:
		print("wifiNotConnect")
	}
case let (nil, .some(value)):
	switch value {
	case .rootIsArray:
		print("rootIsArray")
	case .typeIsUndefined:
		print("typeIsUndefined")
	}
default:
	break
}

Perhaps a better approach would be for the compiler to rewrite union as enum or tuple. If it's tuple, it's perfectly fine with the current compiler.

1 Like

Sometimes it is required by business logic. I can provide example with delivery date: imagine we have a list of products. Each of them have two properties: deliveryDate: Date and deliveryText: String?.
Both Date& String are about delivery day / time in this case. Which one is chosen depends on several rules. Different formats, styles and colors.

Another one example is when the same kind of objects are get from server with different ID kinds: UUID and UInt64 for example.

But in my own opinion type union is the worst choice here. We have several alternatives:

  • Either type
  • convert Date string and and pass it as a tuple of String and some sort of flag
  • use generic function like func foo(bar: some CustomStringConvertible)

Personally I believe that using union types people in general will do "incorrect things" instead of making better api design.

Seems that feature is requested by people with background in other languages, but not deeply familiar with Swift.

2 Likes

Can you please provide some real world tasks where type unions are needed and the solution can not be expressed with current syntax or language constructs.

Typed throws.

This is the wrong question, the question should be if there is a “simple enough” or “good enough” solution.

1 Like

Each of them have two properties: deliveryDate: Date and deliveryText: String?.

Why would the delivery date be a string?

100%.

You're right, in fact most of the scenarios where Union can be used can be implemented manually in Swift, essentially Union can be understood as a compiler's way of simplifying the code for us, first of all, as I said earlier, the compiler can rewrite Union to deal with different types of Enums/Tuples, so in turn Union can be implemented as a much simpler alternative to dealing.
Other examples are:

  1. Typed throws need to throw more than one type of error, I can use a throw from the Error inheritance of the Protocol, and then manually check which type of error at runtime, with Union can be simplified to
throw(Error1|Error2)

and

catch error as Error1 { }
catch error as Error2 { }
  1. If a method accepts String or Int or Double or Data or any other type that can be converted to String according to a specific rule, I can create a new protocol StringConvertible, and implement StringConvertible for String, Int, Double, Data, and then use StringConvertible, and then return the corresponding string, and then use StringConvertible as a parameter, or I can create four methods with the same name to handle each of the four different types, or use Enum.
    With Union, I can simplify it to (String|Int|Double|Data) as a parameter, and then switch the union in the method to get the target string(like the Enum approach), without creating a new type.
  2. If several types have common methods (such as the text of TextView, Label and TextField), and I need to deal with these types at the same time, and only use these common methods, the original approach is usually to add a Protocol and add the common methods to the Protocol and then deal with the Protocol can be, with Union is (view: TextView|Label|TextField), and then directly use view.text.

When it is of Date type it is formatted locally on device as "today", "tomorrow", "January 16th"...
When it is of type String no formatting is needed as text is prepared on server, something like "in 1 hour', "super fast", "soon"...

A Swift Date represents a point in time, irrespective of locale or anything us silly humans have come up with like "calendars" or "formatting".


If one wants to have types that "can be in X or Y form, depending on the context" they're only asking for trouble.