Debouncing and mapping Effect<Never, Never>

I have an Effect that makes an API call that returns nothing. I have it modelled as Effect<Never, Never>

I'd like to debounce this effect – it's attached to changes of UISlider so I just want it to fire when the user is done interacting with the slider. When it actually does get executed I want to map it to a different action. I have something like this:

case let .controlAction(id, action: .levelChanged(level)):
  struct ChangeLevelDebounceId: Hashable {}
  guard let control = state.controls[id: id] else { return .none }

  return environment.apiClient.setLevel(level, id)
    .debounce(id: ChangeLevelDebounceId(), for: .milliseconds(500), scheduler: environment.mainQueue)
    .map { _ in

Where setLevel is:
var setLevel: (Int, String) -> Effect<Never, Never>

I get a warning in the map that says "Will never be executed" probably because Effect<Never, Never> emits no values.

So how do I make this work then? Should I change my setLevel to return Effect<Void, Never>?

There's a difference between returning nothing, like all of these examples:

  • func f() { }
  • func f() { return }
  • func f() { return () }

And not returning, e.g. func f() { while true {} }

What you want is Effect<Void, Never>.

1 Like

Yeah, one way to accomplish what you want is to make your client return an Effect<Void, Never>, but that may also make the client seem a little weird to return a value just to signify it did its work.

Another way would be to concatenate an effect after setLevel so that you can know the moment it finishes. Unfortunately you have to do a bit of a dance first in order to coerce the Never into an Action before you can concatenate:

func absurd<A>(_: Never) -> A { }

return .concatenate(
  environment.apiClient.setLevel(level, id).map(absurd),
  Effect(value: .someOtherAction)
  .debounce(id: ChangeLevelDebounceId(), for: .milliseconds(500), scheduler: environment.mainQueue)

Alternatively you can bake that dance into a little Combine helper like we do in isowords:


Also, now that I look at that code snippet from isowords I don't really know why we erase it to AnyPublisher. You could maintain the type information there if you wanted.

Thanks for the help! Got it working