Why Reactive(Cocoa)?-时间线、输入、输出、复杂性、异步、状态、聚合
To put it another way, the output at any one time is the result of combining all inputs. The output is a function of all inputs up to that time.
摘要:
1、It can remove a significant amount of complexity from our programs and make our code easier to reason about.
2、The problem is that we rarely (read: never) are updating our output based on just one input/event. I can’t put it any better than the way Josh stated it:
“To put it another way, the output at any one time is the result of combining all inputs. The output is a function of all inputs up to that time.”
3、In addition, these inputs are littered up and down our source file, all with slightly different interfaces for the developer to remember. We now have many related inputs looking very unrelated in our code, making the work of mentally mapping out their relationships difficult. In other words, it makes it hard to reason about our code.
4、Basically, we program in linear fashion;Our asynchronous callbacks and other events are still occurring in a linear fashion.
5、Whenever one of these events happens, we likely need to generate output. To do that, we need to combine the new information from this event with all the information from previous events relevant to this particular output.
6、We want to define the relationship between events (inputs), define their subsequent outputs, and then let the computer react appropriately whenever those events occur. State still exists somewhere in this event system, but it’s largely abstracted away from our day to day work. That’s a big win. The paper tape still exists, but it’s all under the hood.
Why
Anyone who builds things for a living should always be striving to improve their skillset. This means investigating new ways of doing and thinking whenever possible. As programmers one of the hardest things we have to do on a regular basis is to be able to reason about our code1; to have a mental map of the implementation used to solve a complex requirement. To me, this is the biggest gain provided by the reactive model of programming. It can remove a significant amount of complexity from our programs and make our code easier to reason about.
For a more authoratative read about reactive programming, you should go read the article “Inputs and Outputs” by Josh Abernathy, one of the creators of ReactiveCocoa. It’s an extremely well written article that I can only hope to supplement with a slightly different explanation. (I’ll be rehashing a bit of what he’s covered there.)
Inputs and Outputs
So what does Josh mean when he says it’s all Inputs and Outputs? The entirety of our job, the meat and potatoes of what we are doing when we build an app, is waiting for events to happen that provide some sort of information (inputs), and then acting on some combination of those inputs and generating some kind of output. Inputs come in all kinds. They provide us with varying levels and types of information:
This is just a sampling of the different kinds of inputs we deal with daily. The information they provide can be as simple as “This event happened”, or be accompanied by a lot of detail about the event such as “The user entered these new characters in the text field”. There are certainly reasons for all the different patterns above, not the least of which is the evolution of our craft and tools; but when it comes down to it they are ALL just telling us some event happened, and sometimes providing extra information about that event.
Outputs can be anything, but a few typical examples include updating a value on a server, storing something in core data; or most importantly, and most typically, updating the UI.
The problem is that we rarely (read: never) are updating our output based on just one input/event. I can’t put it any better than the way Josh stated it:
“To put it another way, the output at any one time is the result of combining all inputs. The output is a function of all inputs up to that time.”
I will add one point pertinent to this article. We often don’t really care what order these inputs came in from an application design perspective, but from an implementation perspective it’s something we are constantly dealing with. In addition, these inputs are littered up and down our source file, all with slightly different interfaces for the developer to remember. We now have many related inputs looking very unrelated in our code, making the work of mentally mapping out their relationships difficult. In other words, it makes it hard to reason about our code.
Paper Tape Computing (Linear Programming)
The issue here is one of time, or more accurately, the timeline of execution. Basically, we program in linear fashion, never wandering all that far from the way things were done on paper tape computers or punch cards. We have an understanding that there is a run loop, and that our code is going to be placed on a timeline and executed in order (ignoring multiple processes for the sake of argument.) Even our awesome block callbacks and delegates are just giving us another snippet of time on the timeline where we can execute code. In fact, all our code is driven by inputs (events) just giving us another chance to insert some paper tape, as shown in this beautiful (and super simplified) diagram.
Our asynchronous callbacks and other events are still occurring in a linear fashion. The period of time in which the data from these events is available to us is limited to a small spot on our linear execution timeline (scope).2 Even if all these events occurred at the same exact millisecond, the CPU would still have to handle one at a time. Our code is stuck executing in single file, with no knowledge of what happened before it.
Whenever one of these events happens, we likely need to generate output. To do that, we need to combine the new information from this event with all the information from previous events relevant to this particular output. Our blocks and delegate calls may be asynchronous, but we still have to deal with the consequence of when they occured on our timeline.
But how do we do that? The information from those previous events isn’t available in the same scope as the current event we’re dealing with.3 There’s no elegant mechanism provided for accessing past events, or combining the information from all relevent events. Since these inputs are essentially isolated from each other in this linear style of programming, we have to imperatively (directly) go and check the status of any dynamic information that other inputs could have changed beforehand. That is to say any time there is an event (input), and we’re given a chance to run some more paper tape, we do a lot of this type of thinking in code:
“Ok lets check what we’ve got here. User tapped a button eh? Well we’re going to want to show the menu then, but a couple things first: Is this user logged in? Good they are. And has the data for this menu loaded in or are we still pulling that from the server? Ahh good we already have that. Finally, the menu isn’t already showing is it? Ok it isn’t, in which case I’m going to go ahead and show it. Let’s write down that it’s showing now, so I know in the future.”
Basically anytime we get the opportunity to run a bit more code via some event, we are doing so with the memory of a goldfish.4 Each time we start with no idea what’s happened previously and have to go check a bunch of statuses to build enough context to correctly generate the output we need. The system is “dumb” and doesn’t know what information we might need for that particular output, so it can’t track it for us. We are left to our own devices to come up with a solution, and end up tracking any possibly relevant inputs ourselves using state.
STATE
State is what makes our job as programmers VERY hard sometimes. To be honest, I didn’t recognize this. That is to say, I knew when I had lots of different events and variables affecting my UI that it became really hard to manage; but I didn’t recognize it in a philosophical, definable way. It’s so ingrained in me that I just thought it was part of the deal with programming, and that maybe I wasn’t the best programmer for constantly struggling with it.
This is not the case. Everyone sucks at managing state. It is the source of an endless amount of bugs and blank stares. “I understand WHY it crashed. I just don’t understand how the user could have possibly gotten it in that state.” ← Programmer adds another state check.
So what is state? Are we just talking about enums and state machines like TCQuestionDetailViewControllerState
? Nope. This is all state:
That should have made you cringe. I’m SURE you’ve seen (and written) similar code. If not you are a WAY better programmer than I am. UI’s are often very complex in code even when they appear simple to the user. In fact, the very reason a user may LOVE your app is the amount of power it delivers in an extremely simple interface.
Value is created by swallowing complexity for the user. - CEO of my first startup
The problem here is that every time you add another state, you are increasing the possible number of combinations in an EXPONENTIAL manner. And that’s assuming your states are just BOOLs. I had an “aha!” moment when I saw a slide in a talk by Justin Spahr-Summers.
I looked at my code, like the interface above, and realized I was giving myself an unbelievably difficult task, requiring super-geek like abilities to pull off (to the tune of 4000+ different combinations I needed to handle if they were all relevant to the output). Obviously this is HIGHLY error prone.
To make matters worse, this type of code will often produce UI only bugs that are incredibly hard, if not impossible, to identify in any automated way. After a few times through this wringer, we’ve probably all ended up writing centralized update methods that check a bunch of properties and update things accordingly. We end up with methods like this littered throughout our code: updateViewCommentStatus
updateChangeAnswerButtonText
. Any time we add another event (input), we make sure it’s updating all the relevant states (e.g. properties) and call the appropriate centralized update methods.
We are expending an awful lot of mental energy dealing with the consequences of this linear code execution thing, this modern version of the paper tape computer. Despite the power of computers today, we are still doing a heck of a lot of work on their behalf. We’re programming to the way the computer hardware works; to the way that the run loop is creating a timeline and the cpu is processing bits in a single file. We are architecting our code around low level implementation details of computing. What if we were to harness the power of the computer, let it do more of the work, and allow ourselves to think and design our apps in a more sane manner?
What if we abstract away the whole pesky notion of linear code execution, explain to the computer what combinations of inputs we are interested in for a particular output, and let the computer track state over time for us? Seems like something the computer would be good at; and we’re certainly awful at it.
Compositional Event System (Non-Linear Programming)
I’m not a classically trained, CS Degree wielding programmer. I did a lot of multimedia before ending up programming for my day job. In that time I’ve seen a lot of tools make the jump from linear to a non-linear style of thinking and working. I’ll talk about tools in another post, but even auto-layout is a great example of moving towards a non-linear (and reactive) approach. Instead of waiting for certain events to occur, then checking the status of a bunch of views and imperatively updating frames, you just describe what the relationships between all the views are ahead of time and let the computer do the work.
In the discussion about the correct terminology to describe RAC and similar styles of programming, the term “Compositional Event System” has been brought up. I think it fits really well. Much like the shift in the multimedia tools I mentioned, what we really want to do is remove (for the most part) the need to account for the timeline of when events (inputs) happen, and therefor the need to manually keep track of them with state. We want to make the computer do that for us.
We want to define the relationship between events (inputs), define their subsequent outputs, and then let the computer react appropriately whenever those events occur. State still exists somewhere in this event system, but it’s largely abstracted away from our day to day work. That’s a big win. The paper tape still exists, but it’s all under the hood.
When you think about it, this concept is actually very simple, and not entirely foreign. Inputs and outputs 5:
ReactiveCocoa
So how does ReactiveCocoa abstract away the timeline for us and step into the world of Non-Linear Programming? It does this by wrapping all those types of input from above (delegate, KVO, completion blocks, etc) in one unified interface called a Signal (RACSignal
).
Think of a signal as a representation of future values from your input. It’s similar to a promise/future which you may have used, but more robust in that it can continually send values (it isn’t limited to just one). While this is similar to a KVO block repeatedly being executed as it observes changes, the difference here is that we now have an object representing those future values. We can take that representation and combine it with other signals to build relationships among our events, and describe how data will flow through our app. This is done with powerful operators that can transform and filter our data into new signals, eventually arriving at the data we need for our output.
http://www.sprynthesis.com/2014/06/15/why-reactivecocoa/