Renaming "Reducer" to "ActionHandler" for clarity

When I learned TCA, I had a hard time understanding why Reducer is called Reducer. Just recently I watched the Point-Free episode where they explain their reasoning and I find it kind of far-fetched or at least not very intuitive. They basically state they named it Reducer because of it's similarity to the reduce method in Swift. To name things in a consistent manner compared to Swift features seems to be a good approach overall, but I feel the ergonomics of the Reducer isn't similar enough to the reduce method or at least it's not apparent intuitively.

Let me elaborate more why I don't see enough similarity. Here's the reduce functions API:

func reduce<Result>(_ initialResult: Result, _ nextPartialResult: (Result, Element) throws -> Result) rethrows -> Result

In words: The reduce function is defined on collections (e.g. Array), takes the initial value to build the result from and a closure that incrementally alters the initial value into the final value. It returns the final value once all alterations are completed.

Now let's have a look at the Reducer types API:

public init(_ reducer: @escaping (inout State, Action, Environment) -> Effect<Action, Never>)

In words: The Reducer type takes one argument, a reducer closure. This closures takes the current state, action and environment as arguments and returns a "side effect" object which will basically cause a new call to the same closure if not .none.

Here's all the differences between reduce and a Reducer:

  1. The reduce function is defined on collections, for a Reducer no collection is involved anywhere.
  2. The reduce function takes the initial value for its result as the first argument, the Reducer takes the current value (state) though, this is an entirely different concept (more in line with map). The idea of the "initial state" lies in an entirely different place, in the Store to be exact, but not in the Reducer.
  3. The reduce function completes and returns the final result, whereas the Reducer closure or type never returns with a final result, only applies "the next result". There's no concept of "completion" involved.

These are the reasons why I think the similarity to reduce is far fetched and what makes new developers also not make the connection. While the State and Action, also for the most part Store and Environment are intuitive names, the Reducer feels more like the place where we handle what needs to happen when actions are triggered to me. Hence I‘m thinking that ActionHandler would be a more intuitive and clear name. I think it might help mitigate some of the confusion for new developers learning TCA.

Looking forward to your thoughts and opinions. Feel free to suggest other names as well.

2 Likes

ActionHandler is definitely more self explanatory. I think there's a lot of people comfortable with the term Reducer though, especially those familiar with functional programming and frameworks like Redux.

The reduce function also has the following overload:

func reduce<Result>(into initialResult: Result, _ updateAccumulatingResult: (inout Result, Element) throws -> ()) rethrows -> Result

If you ignore Environment for a moment, reducer looks very similar to updateAccumulatingResult.

Using an example we can see the similarities as well

enum Action {
  case press(Character)
  case backspace
}

let reducer: (inout String, Action) -> Void = { state, action in
  switch action {
  case let .press(character):
    state.append(character)
  case .backspace:
    state.popLast()
  }
}

let actions: [Action] = [.press("B"), .press("l"), .press("o"), .press("p"), .backspace, .press("b")]

let state = actions.reduce(into: "", reducer)

// "" -> "B" -> "Bl" -> "Blo" -> "Blop" -> "Blo -> "Blob"

I think it's important to make things easy to understand and I'm keen to hear how others feel about the name, but I imagine Reducer is pretty much a standard naming convention.

Thanks @Jeehut for starting the discussion and providing some great points

2 Likes

I think there's a lot of people comfortable with the term Reducer though

Maybe in your bubble, in mine, every single one of the ~25 developers I've discussed TCA with didn't know the term reducer from anywhere else.

especially those familiar with functional programming

I think you mean "reactive programming", not "functional programming". Cause I used functional programming a lot, both in Swift and Haskell, but never heard or used "reducers".

and frameworks like Redux.

Isn't that a minority of Swift developers? I mean, again, leading two separate iOS teams for 5 years now, I have never heard anyone talk about "Redux" or "reducers" in my teams. I also couldn't find "Redux" mentioned in The iOS Developer Community Survey, which is the biggest survey in our community and isn't the very lack of questions about this topic a sign this isn't as known as you might think? What do you mean with "Redux" even, do you mean the JavaScript library? Or do you mean the likes of ReSwift? (BTW, quote from ReSwift: "Reducers will handle the actions by implementing a different state change for each action." :wink: )

Having that said, there's a reason Apple didn't call Combine a "reactive" framework and opted for alternative (new) names for many of the key concepts (like Publisher or Subscriber) rather than the ones which were already introduced (e.g. Observable, Observer) in the reactive programming community: The names were not very intuitive or misleading. While I agree we shouldn't introduce new terminology where there's already good and well-established terminology – if the existing terminology is misleading or only established amongst some developers: As long as we use new terminology (to not overload existing terms), I think it's always good to review established names before just reusing them.

The reduce function also has the following overload:

func reduce<Result>(into initialResult: Result, _ updateAccumulatingResult: (inout Result, Element) > throws -> ()) rethrows -> Result

If you ignore Environment for a moment, reducer looks very similar to updateAccumulatingResult .

Even looking at the overloaded version of reduce, all my points still hold true: 1. it is still defined on collections (which Reducer isn't), 2. it still has an initial value (which Reducer doesn't) and 3. it still returns the final result after completing (which Reducer doesn't).

If anything, your argument shows that that Reducer is similar to the updateAccumulatingResult closure which is only a part of the reduce function. But then the Reducer should be called something like AccumulatingStateUpdater if you really wanted to be inspired by the reduce function. Even this would be a better name than Reducer, but maybe a bit too bulky compared to ActionHandler.

The shorter StateUpdater could also be considered and is a valid name – it just shifts the focus from what the main input is (the action) to what the outcome is (an updated state). As a Reducer in many cases also just starts side effects that are totally unrelated to the state, I feel like "handling the action" is more correct than "updating the state" though.

1 Like

That’s fair enough. I was just giving my input :)

4 Likes

reduce is more general than the definition on Collection, and the Combine framework even comes with a reduce on the Publisher protocol that has a similar signature: https://developer.apple.com/documentation/combine/fail/reduce(::).

AsyncSequence ships with a reduce as well.

If you think of the "reducer" as the trailing closure and not the full signature of reduce, then you can think of the Store initializer as similar to the kicking-off point of the reduce function, where initial state is needed.

The same can be said for a Combine publisher or async sequence that uses reduce and never completes.

We thought a lot about the naming of concepts in the Composable Architecture, and we believed that taking well-established terms of art from Redux would create the least amount of friction, since we can lean on a large body of literature online that should come up if a person searches "reducer." If a user is not aware of Redux (or React hooks, or ReSwift, or any other number of unidirectional frameworks that adopt the same terms), hopefully any initial confusion wears off after a quick search.

16 Likes

+1 for the Reducer naming. It's an established term, there's already reduce declared in the standard library for all collections, and in Combine for publishers. This name makes perfect sense, and is especially obvious given that SwiftUI follows patterns established previously in React, and TCA does the same for Redux.

Renaming Reducer to ActionHandler sounds to me the same as renaming String to CharacterSequence, or Integer to NumberWithoutFractionalComponent. Not only it's more verbose, it also breaks the established convention.

15 Likes

reduce is more general than the definition on Collection , and the Combine framework even comes with a reduce on the Publisher protocol that has a similar signature: https://developer.apple.com/documentation/combine/fail/reduce( : :) .

@stephencelis Thank you for the link and the additional examples for the reduce function (and for TCA of course, I love it!). Let me quote how Apple explains what Combines reduce function does in that link:

Applies a closure that collects each element of a stream and publishes a final result upon completion.

The collection involved here is the collection of elements in the stream, so a "collection over time", same is true for the reduce function in AsyncSequence. The job done by the reduce function is to collect the values (and return them). This is (both) not done by the Reducer in TCA, it doesn't collect anything. It is just the "closure" (mentioned at the beginning of that sentence) that a real reducer would apply.

If you think of the "reducer" as the trailing closure and not the full signature of reduce , then you can think of the Store initializer as similar to the kicking-off point of the reduce function, where initial state is needed.

This is exactly how I think of the Store and the Reducer and why I think it's wrong to call the type for the action handling closure a Reducer, because it's not one. The Store and the Reducer combined could be called a reducer, that I agree on. But it's confusing then to name only a part the "Reducer".

The same (refers to "There's no concept of "completion" involved.") can be said for a Combine publisher or async sequence that uses reduce and never completes.

Are you sure about that? I have never used Combines or AsyncSequences reduce function, but from the documentation example and also the documentation of the method (I quoted above), it seems to actually do return with a final result:

let numbers = (0...10)
cancellable = numbers.publisher
    .reduce(0, { accum, next in accum + next })
    .sink { print("\($0)") } // << here, the `$0` is the final result

// Prints: "55"

Am I missing something here regarding not returning the final result? Of course it doesn't directly return the result (so it's result type is not an Int but another publisher), but the sink will receive the final result and this is the closest to the concept of "returning" in the Combine world. I don't see where the final result is received anywhere with the Reducer though. Yes, the Reducer returns another Effect, but I don't get how an Effect ever emits something like a "final result". If anything, then an accumulated result is received in the view which automatically subscribes to changes in state through the Store. Without Store though, the Reducer is not a full reducer, at least to me.

@Max_Desiatov Comparing the naming of Reducer to that of String or Integer isn't a fair one, I think. Because String and Integer are entirely new names (maybe inspired by things outside of programming) whereas Reducer is inspired by the reduce function and therefore I expect it to behave similar to it for the sake of consistency, which is exactly what I think is not the case here. If there was a stringify method before naming a CharacterSequence and String was chosen due to its similarity to stringify, then the naming situation would be comparable, but I assume we had String before anything like stringify.

I don't know why it was named Reducer in other languages, maybe they don't have (the same) notion of a reduce function, but having the reduce function in Swift I feel like this specific name creates more confusion and inconsistency than it needs to.

Ha, naming! There's a scan operation with a similar signature but emits an output per accumulation. Both operations take a "reducer"/accumulation function, though.

Redux chose this name specifically because of the reduce function. From the original documentation:

It’s called a reducer because it’s the type of function you would pass to Array.prototype.reduce(reducer, ?initialValue)

5 Likes

That makes sense now. So a „Reducer“ is by definition „the closure you pass to a reduce function“. Ahh, why didn‘t I get it before. :sweat_smile: I was thinking of it as „a wrapper type for a reduce function“. :roll_eyes:

Then I understand the naming choice here. I will try to explain it like this then, I hope it will help. But since it‘s not obvious which reduce function it‘s for (cause that‘s an implementation detail of the TCA library), I might still need to create a typealias „ActionHandler“ to make it easier to understand for developers who don‘t care about the technical details.

1 Like

I went and looked at Redux to understand what they are doing, and despite the weird Array.prototype.reduce thing, they appear to be implementing a finite state machine. Their use of "Reducer" makes a sort of sense in this plumbing minded frame, but not in the frame of telling the story of the system.

Their Reducer function takes a state and action, and returns a new state. In the language of FSMs, it is a transition function.

Perhaps "Transitioner" would be a good name?

Or maybe "Evolver", since: "A function that describes how to evolve the current state of the app to the next state given an action." (From the README.md)

-1 for the proposal.

The name IMHO fits perfectly with the functional programming principles the library was built upon & library authors did well to lean on prior art in other libraries as well as functional programming concepts. I don't think personal experience or "bubbles" are good arguments to provide when arguing for the name change.

4 Likes

@eimantas As we don’t have any objective data that proves that „Reducer“ is not a confusing name to many, personal experience actually is the best argument I can have and I wish you had some more respect for peoples experiences. If I struggled with something not only myself, but also saw several others struggle and even others create a thread about confusing terminology in TCA in this very forum, then it‘s basically proven to me that there‘s something wrong here. Either the concept is too complicated to find an intuitive name, or the APIs should be adjusted in some way to be more clear. At least if you care about user experience more than about academic consistency.

By the way, I believe that „others made the same mistake, so we can make it, too“ is the worst of all arguments.

1 Like

Didn't mean to sound condescending or show any disrespect. I apologize if taken otherwise :bowing_man:

My main point was that familiarity with ecosystem of similarly named concepts help you understand why that name was chosen over something else. You can find other functions that go together with reduce in different languages/libraries and they are all used for the very same purpose:

  • map function with transform;
  • filter function with predicate;

Nothing comes to mind (personal experience) that would have a changer function with ChangeHandler as argument instead of map and transform.

Not pointing to functions and their arguments having different names but that they persist across different languages and libraries.

Update: as per "tu quoque" you mention: I highly doubt this is a mistake that could not have been fixed in other libraries/languages even through a few major version releases which allow for breaking changes.

3 Likes

@Jeehut I don't think it's ideal to imply the library authors have made a "mistake". It looks like they've put a lot of thought and effort into TCA; the term Reducer would not have been chosen impulsively.

I appreciate you're trying to make TCA more accessible for developers, it just seems this conversation is unnecessarily argumentative.

Naming is a pretty interesting topic... my only fear is...

func discussNamingConventions() {
  discussNamingConventions()
}
2 Likes

The reason why I bring this up is that I‘m preparing an opinionated iOS development course for beginners where I want to make a point for TCA and explain it. And currently, I‘m thinking about introducing a typealias named „ActionHandler“ for reducers due to the beginner-unfriendly naming of reducers in my course. So I‘m not just asking this question for the sake of discussion, I‘m trying to contribute to the library directly rather than suggesting a workaround from the outside into the community first. This is an approach I would want others to also follow when they want to improve something in my own libraries.

Also, through the discussion here I already learned a few things about the rationale for this name, this will definitely help me explain it in the videos if needed and answer my viewers questions when defending TCA if they have any. Wouldn‘t be that great if I said I believe this is a great architecture but couldn‘t even answer basic questions about it. :smile:

Additionally, I wanted to see if anyone has good reasons why „ActionHandler“ maybe is not a fitting name. But so far no one seemed to have arguments against it.

So thank you everyone involved here for your honest opinions and your patience with me.

I wanted to write my thoughts on the name in my pervious post, but didn't want to mix in my personal opinion :slight_smile:

My only association is that Handler comes from imperative programming world of callbacks and delegation (if we're talking about Cocoa frameworks). You won't see handlers anywhere in FP (again, not in my experience).

One could also probably argue on ActionHandler accepting only action (based purely on the name), while Reducer has a similar signature to standard reduce function (which, ironically, could be known only having familiarity with other FP languages/libraries :).

So I could probably understand why you would introduce the Reducer as ActionHandler to beginners that (most likely) are coming from imperative programming world of UIKit or other platforms. Because in imperative programming world it does look like action handler. Though once you start digging deeper — subtle differences will be discovered.

P.S. TCA has also on more than one occasion has been described as opinionated library that tells you how to structure the features of the application. I guess this goes to the naming too :wink:

2 Likes

@Jeehut If you're preparing a course, I think this is great that you have an alternative name for Reducer to propose to your students. You can explain to them why ActionHandler would be a better name in your opinion, and why Reducer is used nonetheless. That's two ways to reach your goal. As a side effect, you can digress on the "reduce" functions in Swift (whereas it's on Collection, in Combine, or the new AsyncSequence), or other architectures in other domains (like Redux, which is far from anecdotal with its 55k GitHub stars). As a student, I feel it is always a win when you learn more than what you came for.

There is nothing absolutely wrong with ActionHandler, as there is nothing absolutely wrong with Reducer. Both are coming from different fields, each one with their own strengths.

I personally prefer Reducer because it has meaningful connections with prior art, whereas ActionHandler feels too generic and ad-hoc. This doesn't prevent me to picture Reducers as things that are handling actions, but also mutating the state and/or producing effects.

6 Likes

@Jeehut That makes sense. Echoing what @tgrapperon suggested. It seems reasonable to introduce the concept to your students in the way you think is best. Possibly even get them get them comfortable using

typealias ActionHandler<State, Action> = (State, Action) -> State

before introducing inout, Environment and Effect, similar to how Point-Free did it.

I think at some point it's worth getting them familiar with Reducer as well, highlighting how using typealias ActionHandler = Reducer is always an option.

I had used JS and Redux before learning about TCA so Reducer worked for me. ActionHandler looks like it would work well too.

Keep us updated on the course :)

2 Likes

Please keep Reducer. I think it's important to maintain links to the wider concept. As mentioned, reduce is a much larger concept than the function on arrays.

e.g. You can define reduce for any enum.

enum Either<L, R> {
  case left(L)
  case right(R)

  func reduce<T>(left: (L) -> T, right: (R) -> T) -> T {
    switch self {
      case let .left(value): return left(value)
      case let .right(value): return right(value)
  }
}

I would argue that using an "easier" name is a disservice to students and beginners. It would be like calling things with map Mappable. It seems easier on the surface, but it's conflating "familiar" with "easy", and depriving someone of the ability to learn and dig deeper on their own by removing the link to the larger concepts.

My 2c, in any event. :smile:

1 Like