Generic type constraint by enum case

I'm writing a small library that would allow me to handle Sockets in a generic fashion.

I basically want to define a Socket such as it would define Emitting & Receiving types which would be the events we want it emit and receive, respectively.

Now, I like using enums for this sort of functionality, where I could define the Emitting type like such:

struct MySocket {
    enum Emitting {
        case send(message: String)
        case join(room: String)

        var responseType: <?> {
            switch self {
                case .send: return nil
                case .join: return Room.self
            }
        }
        
        struct Room {
            let id: String
            let participants: Int
        }
    }
}

You'll notice that I also defined a Room type, which is data that I know the socket should return when I emit .join(room: "room_uuid"). There could also be other such types I could define for any future event.

The idea is that I'd then have my generic Socket implementation which would have a convenience emit(_:) function that would be called as such:

let socket = MySocket()
socket.emit(.join(room: "room_uuid") { data in
    // type(of: data) == Emitting.Room
}

Is this at all feasible ? I know I can define a single associatedtype which would allow me to do

protocol Event {
    associatedtype Response
}

struct Socket<Emitting: Event, Receiving: Event> {

    func emit(_ event: Emitting, handler: @escaping (Emitting.Response) -> Void) {
        // magic
    }
}

but that then prevents me from easily defining multiple events with multiple response types.

I don't know if this actually makes sense? Maybe all I need to do is define functions for each event rather than types :thinking: then I have control over what response type I send back in the handler...

I think the closest you can get is another enum with the same cases but response objects as associated types instead:

enum EmittingEvent {
    case send,
    case join(Emitting.Room)
}

socket.emit(.join(room: "room_uuid") { event in
    guard case let .join(data) = event else { return /* or throw */ }
    // type(of: data) == Emitting.Room
}

Alternatively drop enum for the types of messages and use structs sharing a common protocol that provides an associated type for the response value.

I was thinking of doing something like that, but that kind of defeats the point altogether, since I'm trying to achieve type safety :confused:

"type safety" is not all powerful. If you want emit signature to look something like this

func emit(_ request: Emitting, respond: (XX) -> ())

Then the response XX can only be based on the fact that request is Emitting, not that it is specifically send or join.

You could make Emitting a protocol instead, which would let you push some information up into the type-system level

protocol Emitting {
  associatedtype Response
}

struct Send {
  typealias Response = Never
}

struct Join {
  typealias Response = Room
}

PS

Your emit call has unbalanced ()

1 Like