RunLoop.main as default Scheduler compliant function parameter

I use class as below (simplified version).

class SomeClass<Value> {
    var subject = PassthroughSubject<Value, Never>()

    private var c: AnyCancellable?

    convenience init(closure: @escaping (Value) -> Void) {
        self.init(on: RunLoop.main, closure: closure)
    }

    init<S>(on scheduler: S, closure: @escaping (Value) -> Void) where S: Scheduler {
        c = subject
            .receive(on: scheduler)
            .sink(receiveValue: closure)
    }
}

On the first sight convenience init seems to be reduntant and easy replaceable with default value in init function...

init<S>(on scheduler: S = RunLoop.main, closure: @escaping (Value) -> Void) where S: Scheduler {

... but complier rejects such solution suggesting me that

Default argument value of type 'RunLoop' cannot be converted to type 'S'

Could somebody explain what's going on here? Code with default value of on parameter seems to be formally almost identical with initial code, which compiles properly.

Scheduler has associated types, so you must further constrain the generic:

init<S>(
  on scheduler: S = RunLoop.main,
  closure: @escaping (Value) -> Void
)
where
  S: Scheduler,
  // These below:
  S.SchedulerTimeType == RunLoop.SchedulerTimeType,
  S.SchedulerOptions == RunLoop.SchedulerOptions
{

This of course means you will only be able to pass run loops to this helper, so you'd need another init without a default if you want to use other schedulers, or you could use a type-erased AnyScheduler (one is defined in GitHub - pointfreeco/combine-schedulers: ⏰ A few schedulers that make working with Combine more testable and more versatile.).

Hi, thx a lot for the answer (almost lost hope for any feedback). As you write, you solution actually doesn't solve the problem.

Actually, the problem seems to concern all protocols - even the simplest. I'm using Hashable as an example (couldn't find simpler).

func fooNoDefault<V>(x : V) where V: Hashable { }
fooNoDefault(x: 3)
fooNoDefault(x: "3") 

This code compiles. Compiler infers type of x as Int/String, which conforms to Hashable so it accepts it as function parameter.

The code below is formally identical. It's just assigns 3 to x as default value instead of explicitly providing it in function call (if parameter is not provided explicitly). Compiler should use Int as V (exactly like it does in explicit function call), shouldn't it? Isn't it sort of compiler constraint?

func fooDefault<V>(x : V = 3) where V: Hashable { }

Default value can't provide generic constraint, it consumes generic constraint. If you want to add default value for generic parameter, just remove generic constraint and use concrete type.

init(on scheduler: RunLoop = .main, closure: @escaping (Value) -> Void) {

V could be used elsewhere in the function signature, and inferred as another type.

func fooDefault<V>(x: V = 3) -> V where V: Hashable { return x }

let foo: String = fooDefault() // ???

Yes, the compiler could theoretically report an error in this case, instead of rejecting generic default value entirely. But that's not how it currently works.

I'm definitely not an expert, but from discussion so far I conclude (correctly?) that the problem is just compiler constraint, not some low-level language definition stuff, and formally could be supported. From end-user point of view it would be nice (just nice) to have it implemented.

Wouldn't be a good idea to report it as low-priority feature request?

1 Like

Again, the code below seems to be formally exactly the same as your example (i.e. some pre-parser could create code like below from your example).

func fooNoDefault<V>(x : V) -> V where V: Hashable { return x}
//let foo: String = fooNoDefault(x: 3) //NOK
let fooInt: Int = fooNoDefault(x: 3) //OK

Actually default value could be implemented like sort of pre-compiler stuff (I have no idea how it works in reality), e.g.

func foo(x: SomeType = defaultValue, ...) 
let bar = foo(...)

could be changed to

func foo(x: SomeType, ...)
let bar = foo(x: defaultValue, ...)

before real compilation. Should the latter compiler, the same should happen for former.

This is the same issue as SR-4273, which is closed with the “Won't Do” resolution for the following reason:

[...] this wouldn't be valid because generic parameters can be controlled by context:

let x: Float = foo()

We'd have to have the notion of a default argument value that only kicks in under certain conditions, which gets complicated. You might as well just use overloads for that.

Ah, now I at least am sure that I didn't make any programming error but just faced some compiler limits. Bad luck I faced two constraints almost at the same time, writing my property wrapper class - 2nd problem being error during implicit nil initialization of wrapped value. I reported it SR-14411 as advised by another Rob (I thought it was you at first).
Fortunately both are addressable with some additional lines of code / tricks.

Ah right :) my mistake. I'm so used to using type-erased schedulers that I forgot Swift's limitations when it comes to this sort of thing. @ddddxxx's solution is a good one.

With all respect, it's not quite the solution, as it doesn't accept other schedulers which initial SomeClass does.

let someClassdefault = SomeClass(closure: {(value: Int) in }) //OK
let someClassRunLoop = SomeClass(on: RunLoop.current, closure: {(value: Int) in }) //OK
//let someClassDispatchQueue = SomeClass(on: DispatchQueue.main, closure: {(value: Int) in }) //NOK

I have no choice but accepting current limitations, but am still a bit surprised that they exist. And definitely have to learn how to use type-erased schedulers.
Thx for your help.

You can definitely work around the limitations with a type-erased scheduler (like the library with the AnyScheduler I linked to above). Its implementation is simple and short enough that you could copy and paste that file into your project if you want to avoid incurring a dependency. With an AnyScheduler around you can then do:

init(
  on scheduler: AnySchedulerOf<RunLoop> = .main,
  // OR: = AnyScheduler(RunLoop.main)
  // OR: = RunLoop.main.eraseToAnyScheduler()
  closure: @escaping (Value) -> Void
) {

However this AnyScheduler only erases the type of the scheduler, not its associated options. This is so you don't lose access to the scheduler options and time type should you need them, but it also means you can't freely swap dispatch queues and run loops in.

It is possible to erase the associated types, though, as this other library does: Add AnyScheduler by ddddxxx · Pull Request #9 · cx-org/CXExtensions · GitHub

This would allow you to swap dispatch queues for run loops, but comes with a few caveats:

  • You lose access to the options and time type, should you need them
  • If you were to advance an AnyScheduler's time type with the wrong strideable value I believe it's a crash at runtime

Edit: Also, isn't the solution that takes an overload for the default suitable? You could have a generic function that takes a scheduler and then have a RunLoop-specific overload. Might be able to avoid type erasure entirely then.

The solution with overload (as presented in my first post) is definitely suitable. One of the reasons I asked the question was for somebody to help me understand why such overloads are necessary at all instead of just using defaults.
I still don't understand why implementing default parameters for generic types is "whole new language feature", like one of wise guys in SR-4237 discussion wrote, but I have no choice but to believe that it really is.

It's unfortunate, and I definitely hope for this to be implemented in the future, but if you're interested in diving deeper, this feature is known as "existentials," and is described in Swift's generics manifesto: swift/GenericsManifesto.md at main · apple/swift · GitHub

This may help you understanding how default value currently works:

protocol DefaultValueProviding {
    static var defaultValue: Self { get }
}

func foo<V: DefaultValueProviding>(x: V = V.defaultValue) {} // OK

The default value is determined after generic type V is determined. So you can't use fixed type here.

This also can't be done with "pre-compiler stuff" because you need to type check first to determine which overloaded function to use.

1 Like

I just noticed this may be a breaking change due to SE-0299

protocol DefaultValueProviding {
    static var defaultValue: Self { get }
}

struct DefaultType: DefaultValueProviding {}

extension DefaultValueProviding where Self == DefaultType {
    static var defaultValue: DefaultType { DefaultType() }
}

let v = DefaultValueProviding.defaultValue // v is inferred as DefaultType since SE-0299

func foo<V: DefaultValueProviding>(x: V = V.defaultValue) {}
// generic `DefaultValueProviding.defaultValue` or concrete `DefaultType.defaultValue`?

SE-0299 is indeed nice improvement, even without this discussion context (no more looking up documentation for possible options for .buttonStyle(_:) modifier).

My idea, which I called "pre-compiler stuff" was sort of (let's call it more precisely) syntactic sugar, e.g.

func foo<V>(_ v : V = "1") -> V where V: Hashable { return v }
//currently doesn't complile

would be treated like

func foo() -> String { foo("1") }
func foo<V>(_ v : V) -> V where V: Hashable { return v }
//compiles 

with all the consequences (e.g. compile failure in case "sugar extraction" results in failed code). I think I understand current constraints (code analysis steps/phases you write about).
Even though my proposal seems indeed sort of "dirty", overcomplicated and (seems like) against "philosophy" of Swift I still cannot find one example (which I try to find solely for educational purposes) it wouldn't work.

Aside from being untenable in the face of multiple defaulted arguments (for a function with n defaulted arguments you'd potentially need the compiler to generate and later on disambiguate between 2n different overloads), that scheme would break the current treatment of "magic" constants (#file, #line) that are evaluated in the context of the caller, rather than the callee.

The issue raised in this thread was only one of a couple I faced when writing my property wrapper. I'm trying to summarize my findings below. It's mainly for me to remember, but maybe somebody will find it interesting.

This is the initial property wrapper (unnecessary code removed), that I expected to work properly for every use case. I'm using Scheduler in this example (I'm using it in my property wrapper) but it could by any protocol, like Hashable.

@propertyWrapper
class WrapperOfDreams<Value> {

    var wrappedValue: Value

    init<S: Scheduler>(wrappedValue: Value, loop: S = RunLoop.main) {
        self.wrappedValue = wrappedValue
        // receive published values on loop
    }
}

Use cases that I expected to work with comment why they don't work (unless they do).

class Test_WrapperfDreams {
    // NOK: default parameter for generic types not allowed
    @WrapperOfDreams var v1 : Double

    // NOK: default parameter for generic types not allowed
    @WrapperOfDreams var v2 : Double = 1.0

    // NOK: default parameter for generic types not allowed
    @WrapperOfDreams var v3 : Double?

    // NOK: default parameter for generic types not allowed
    @WrapperOfDreams var v4 : Double? = nil

    // Cases below assume that default parameter for generic type IS allowed

    // NOK: deffered initialization of multi-argument property wrappers not possible
    //      see link below 
    @WrapperOfDreams(loop: RunLoop.main) var v5 : Double

    // NOK: implicit nil doesn't work for wrappedValue (should compile like v8)
    //      see SR-14411 link below
    @WrapperOfDreams(loop: RunLoop.main) var v6 : Double?

    // OK
    @WrapperOfDreams(loop: RunLoop.main) var v7 : Double = 1.0

    // OK
    @WrapperOfDreams(loop: RunLoop.main) var v8 : Double? = nil

    init() {
        v1 = 1.0
        v5 = 1.0
    }
}

Deffered initialization of multi-argument property wrappers not possible

SR-14411

Extended version of property wrapper (created before I was made aware of existence of AnyScheduler solution).

@propertyWrapper
class Wrapper<Value> {

    var wrappedValue: Value

    // workaround for "default value for generic types not possible" problem
    init<V> (wrappedValue: Value = nil) where Value == V? {
        self.wrappedValue = wrappedValue
        // don't use receive(on:)
    }

    // workaround for "SR-14411" problem
    init<V, S>(wrappedValue: Value = nil, loop: S) where Value == V?, S: Scheduler {
        self.wrappedValue = wrappedValue
       // receive published values on loop
    }

    // workaround for "default value for generic types not possible" problem
    init (wrappedValue: Value) {
        self.wrappedValue = wrappedValue
        // don't use receive(on:)
    }

    init<S>(wrappedValue: Value, loop: S) where S: Scheduler {
        self.wrappedValue = wrappedValue
        // receive published values on loop
    }
}

Use cases for this wrapper

class Test {
    @Wrapper var v1 : Double
    @Wrapper var v2 : Double?
    @Wrapper var v3 : Double = 1.0
    @Wrapper var v4 : Double? = nil

    //NOK - deffered initialization for multiparameter property wrappers not possible
    // @Wrapper(loop: RunLoop.main) var v5 : Double

    @Wrapper(loop: RunLoop.main) var v6 : Double?
    @Wrapper(loop: RunLoop.main) var v7 : Double = 1.0
    @Wrapper(loop: RunLoop.main) var v8 : Double? = nil

    init() {
        v1 = 1.0
       // v5 = 1.0
    }
}