1. 程式人生 > >ComPiler200003:Story-Oriented Programming

ComPiler200003:Story-Oriented Programming

without ret int problem raw efi 類比 lan 人類

Story-Oriented Programming

A World of Gremlins

Handle is a gremlin. Handle can make other gremlins. Handle tends to make gremlins in the likeness of what he imagines, and what he imagines always comes in pairs. Today Handle has borne two children: Reqla and Resnak. But he does not know the purpose of his children. He just exists to create them. Polys, the muse god, inspires Handle and Handle transcribes the inspiration with his creative ability. Upon creation, Handle sends them to a gremlin school, where the gremlin teacher Routin helps them find their way in the world.

Routin immediately understands why Resnak is important. He sees that Resnak’s whole purpose in life is to communicate Reqla’s purpose back to Polys. Whenever anyone tells Resnak something, he and Reqla dissolve into the aether. It’s very important that whoever talks to Resnak say the best possible thing to him, because Resnak is capable of talking to Polys directly and upon discerning Reqla’s value, will tell Polys so that Polys may experience the joy of understanding of his creation. But to him, the purpose of Reqla is less clear. He converses with Reqla to attempt to figure out how she can best serve Polys.

In general, when Routin is dealing with gremlin children, if he can determine their purpose, he will send them out to fulfill that purpose. Otherwise, he would tell the child who can speak to Polys, Resnak in this case, that he does not know the other child’s importance.

Today Routin discerned that Polys is playing a language game with himself, and, growing bored of the language in which Polys typically thinks, wanted to hear something in a different language for the joy of it. However, Routin doesn’t know anything about language, but he knows someone who does. He sends Reqla and Resnak to the one and only gremlin translation company.

When they arrive, they are met by the company foreman Logon, who knows just enough about language to know which member of the company would be the most suited. After a short conversation with Reqla, he knows just who Reqla should talk to get her message translated.

Let’s pause here and get a little more interactive.

Reqla and Resnak are currently with Logon. A translator who is capable of translating Reqla’s message is known to exist by Logon. What happens next? Should Reqla and Resnak both go to the translator? If they do, does the translator greet them, or does Reqla petition the translator? Upon translation, who tells Resnak the translation, Reqla or the translator? Should Resnak stay with Logon? If he does, who brings the translation back? Does the translator stay sequestered away (maybe he is very fat, and cannot easily move)? Upon return, does the message carrier tell Resnak directly? Or does he tell Logon who tells Resnak? Is there any value to telling the foreman first? Does the foreman need to negotiate something internally (or maybe externally, with his accounting buddy) before following through to tell Resnak? Unwinding back, maybe it’s more economical for the translator to be in the presence of all three.

No matter which question you choose to answer, they all beg the question of what line of reasoning is logically consistent with the contrived world. If you find out that translators are all kept in ivory towers and you can only shout up to them, and they back to you, then that’s going to preclude you from calling them into the foreman’s office.

If you have a background in web API development, you may realize this is a very thinly veiled anthropomorphization of a translation web service. In the jargon of what is typical of the domain: a request and response pair are generated by the underlying http server. A handler dispatches the request/response to a router which determines which sub handler to call. If it’s a translation request, the translation sub handler dispatches, based on language detection heuristic, to a translation service, which may not be colocated, but remotely accessed through sub request. Upon completion of the translation, the translation is sent back to the requesting entity through the response body.

Clearly the description of the service using jargon is much shorter than the story. And it’s tantalizing to think that the shortness is necessarily an asset.

Is it possible that the brevity is actually cryptic or opaque to the requirements of implementation?

The latter might be how I would actually describe the service to another developer if they asked about it. I certainly usually wouldn’t concoct a story like I did above (which took me like at least 30 minutes to ensure narrative consistency, and even the most basic palatability, which I am still not sure it contains, lol.)

In going through the exercise of actually making the description more verbose, and forcing my components be broken up as active sub processes (the gremlins) behaving more or less as humans might, I found it causes lateral considerations that tend to be just left out of typical “formal” systems “design.”

Narrative Reasoning

Human intuitive reasoning seems to be very closely linked to the concept of narrative, specifically narratives involving other human actors. While it is possible for humans to reason about things that seem to bear no clear resemblence to another human, such as a perfect geometric object like an isosceles triangle, this seems to be mostly secluded to the right side of the bell curve.

All more or less functional humans have the ability to reason about narrative. It’s quite literally the human way. Hidden in these myriad reasonings may be an emergent solution to a problem that can be leveraged from an otherwise average minded individual. This is similar to the difference between a strong classifier like a support vector machine (an analogy to a very smart person), and the adaptive boosted weak classifiers like chained decisions stumps (a crowd of average people). In the machine learning metaphor, the average person is not contributing a miracle solution despite his averageness, he is being leveraged to exert just the right pressure on the problem to yield a solution. This analogy in turn reminds me of the classical conceptualization of the wisdom of crowds. Some people will be smarter than the entire crowd. But the reality is that these individuals are incredibly rare, and anecdotally they seem to work well neither with others who are equally as intelligent nor with less intelligent individuals.

If we wanted to democratize reasoning about complex things, like systems, and specifically reasoning about interactions between things that give rise to this complexity, it would seem prudent to treat components directly as human agents, or carriers of intentionality, because this is something even your average person can reason about. Too often we attribute human intentionality to components of a system anyway when reasoning about them, so instead of mixing technical jargon with behavioral analogy, what if we just treated the components as little humans, or little gremlins, or lemmings or whatever. What if instead of couching speech about systems development in highly technical jargon, we told stories instead?

The Morality Argument

There exists a moral imperative concerning software and our world as well. Our world today is driven by software. Most of it is terribly written and really hard to reason about because of that, and by some miracle still works a good 80% of the time. But bugs are rampant even in solved problem domains. A lot of time these bugs come from the difficulty not in reasoning about how a single component operates, but in understanding the relationships and communication patterns between components. Breaking software into components is a good first step in eliminating bugs, and there are probably thousands of articles on why this is the case.

We need to write reliable software. People’s lives rely on it often, including my own. In order to fulfill this basic moral criterion we have to come to the realization that our current ways of reasoning about systems are inadequate.

A History of Decomposition

There are several historically prominent methods by which systems were composed to try to improve the ability to reason about them, a few examples being structured programming, 90s object-oriented programming, and the recent uptick in functional programming. Finally, cutting across all of these methods was the usage of static typing to attempt to automate some aspects of reasoning so humans did not have to do all the heavy lifting.

Previous to the 90s, the predominant way this was achieved was through the usage of structured-programming which broke down procedures into sub-procedures that followed a few heuristics in order to stay well-behaved. But sub-procedures could literally do anything, and because of this they could manipulate state that other procedures relied on in a way that was unpredictable, etc. From Wikipedia:

Structured programming is a programming paradigm aimed at improving the clarity, quality, and development time of a computer program by making extensive use of the structured control flow constructs of selection and repetition, block structures, and subroutines in contrast to using simple tests and jumps such as the go to statement, which can lead to “spaghetti code” that is potentially difficult to follow and maintain.1

In the 90s, the manner by which problems were popularly broken down was what is still called “object-oriented programming.” Unfortunately, the person (Dr. Alan Kay) who originally envisioned this concept much earlier (70s) lost it to the morass of popular opinion of something else entirely. Endemic in this approach is the anthropomorphization of things often leading to convoluted design that sounds something like “the elevator object talks to the floor object to know if it needs to stop.” Wut..? Floors don’t talk. Neither do elevators. But we were told that if we look at the requirements document and find the nouns, those can be candidates for our domain objects. There are numerous criticism of what OOP eventually became.2

This became such a travesty in design that many people opted to leave it behind entirely for the repopularized holy grail of mathematical or functional programming. Instead of floors and elevators, we had vectors whose positions represent elevators and whose values represent floors, and we pass these vectors into analysis algorithms also informed by call button state vectors. These combine to produce a description of an action that will alter which elevator will stop at which floor.

Talking about math will always make you sound smart to those who don’t know any better. Actually knowing what you’re talking about can make you admirable. If you honestly are amongst the relative few people who know what they are talking about when the now aphoristic “a monad is just a monoid in the category of endofunctors” quote gets bandied about, you know we are few.

In all of these forms of programming, types have also been included to allow for differing levels of automated reasoning about a program’s correctness. It turns out that writing types that abstract object creation (classes) or types that describe abstract mathematical properties across data structures (haskell typeclasses) is actually somewhat of a hard problem if you are looking for true simplicity. Typed languages that are used by well meaning, but under educated, people tend to end up in type declaration explosion. Type A and type B may do 50% of the same things, because they actually are ad-hoc realization of some underlying type C, but discovering the underlying abstraction C requires more than just copying and pasting the 50% of the code that is shared. This type of ad-hoc inheritance then leads to extremely brittle code when A and B are coupled in ways they should not be.

One of Alan Perlis’ famous epigrams states the following, “It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures.” This pithy statement is generally backed up by concepts that can be borrowed from abstract algebra. But finding this data structure that gives rise to 100 useful functions is sometimes very obvious (lists come to mind), and sometimes much more difficult (program I/O).

The Proposal

There is utilitarian, aesthetic, and performative value for making systems design crowd-sourceable.

There is moral value as well.

Components of a system should be structured in such a way as to be as human like as possible. By this I mean components should have a purpose or a direct responsibility they manage, their interactions should reflect or caricaturize what would be a meaningful interaction between two or more people. Breaking up a system in this manner allows any person familiar with the system to recount the systems behavior as the behaviors of individuals. Inherent in this is the ability for people outside of the technical space to reflect and reason about the system. This necessarily allows recruitment of more people to more realistically leverage something they have spent their entire lives doing: thinking about human relationships, as a way to scale solutions to system design problems. This is the democratization of systems design. This is story-oriented programming.

This is not an original idea. This is a reframing of a previous idea that allowed the idea to click in my own mind. James Coplein gave a talk3 at the GOTO 2017 conference where he asserted that the original conceptualization of OOP was that the ontology in the user’s mind was directly reflected in the ontology of systems design. If I see a dog on the screen, I should be manipulating a dog in code (to some degree). This immediately sheds light on the absurdity of some sorts of OO designs (the character walks into a wall object, and it’s the wall that tells the character it collided.) This idea started me down this train of thought.

An additional novetly of this particular reframing is its internal/external homoiconicity. In many of Alan Kays talks that have been put on YouTube, he talks about how his original conceptualization of scale was based on a model of something that has already scaled: the cellular model of the human body. The human body and brain are a stunning example of singular components in a deeply woven network working very well at very large scale. But no cell is really “smarter” than another cell in the sense we might talk about a human being smarter than another human.

This begs the question to me, if the human body is essentially a sort of distribution and democratization of life to the constituent cells, then other forms of democratization may be able to be leveraged to produce artifacts at scale. Dr. Kay proposed that democratizing tasks to components similar to cells in a computer system would give rise to scalable systems. This was bastardized into 90s OOP which has failed spectacularly in some cases. Rewinding back to that original concept, and reframing it slightly; if instead of cells, we conceptualized the system as a series of actors, gremlins, humans, whatever, engaged in a story, we leverage a reasoning technology for interaction that almost every human can contribute to. We scale out the problem of systems design in the very same way that we scale out the system itself.

Objections

In internalizing and fleshing out this argument, I came across a number of objections that I addressed internally, and would prefer to address point by point. These objections came in two varieties: aesthetic or pragramtic. Aesthetic objections are those that argue for preferential prejudice based on an arbitrary factor that can slide around (an aesthetic). Pragmatic objections are those that argue that the actual enactment of the suggestion is not possible due to social or psychological constraints.

Aesthetic

Reductio Ad Absurdem, the Slippery Slope, if we do it in one place we have to do it everywhere.

This is the argument that everything, including a value as simple as a number, should be an actor. I don’t know that I agree or disagree with this. I am presenting the aesthetic I use for discrimination.

The extant universe does seem to justify that communicative decomposition can go all the way down. We can (and do) talk about quarks exchanging color-charged gluons. Charged particles like protons and electrons exchange photons. These examples attribute human-like communication or intention onto what are otherwise extraordinarily simple phenomena.

There does exist a realm of phenomena that have no physical, semi or otherwise, referents. Products of formal thought systems, such as mathematics, do not exist in the physical world. This is the perfect line in the sand that can be drawn to figure out whether or not something should be modeled as an actor or a solid process.

Thus we would probably not say that the number 5 talks to another number 5 saying “plus.” Instead we would just use the lexical constructs of that formal realm. Much the same way that set theory has its own set of combinatorics, category theory theirs, and high school algebra theirs, these systems of thought form closed, self-consistent systems of reasoning in which no negotiation or other communication needs to happen.

A caveat to this is that care must be taken to understand that purity is easily violated. While you can talk about numbers and strings as if they are mathematical constructs (and thus do not require decomposition through communicative processes), should they come to violate their own purity (a mutable string, or a sufficiently large number such that decisions must be made for memory allocation), we once again enter the world of a mathematical notion with a physical referent, and the case can again be made for narrative decomposition.

More relevant to real-world scenarios is the example of an HTTP web server. The protocol specifies that bytes coming in conform to a specific shape. This shape is a string with further constraints on what is in the string. However, part of the specification allows for some aspects of this shape to be unbounded, such as the request body. In process, you can model a request as a string, and then treat it as if it’s a pure mathematical entity. That works until you run into the real world constraint of large request bodies that may consume all available memory, or for performance reasons, you don’t want to locate the request entirely in memory. This breaks the string abstraction and shows the request for what it really is, an interpretation of electrical signals over a wire. Treating it under this lower level view also reveals the temporal-spatial nature of a physical process. Thus, eschewing the string abstraction and instead looking at the request as an actor capable of negotiating its available bytes honors the underlying process, and more accurately allows navigation of the optimization concerns previously stated.

A fascinating union of these concepts is to first design a sub-system using narrative design, and then, if it’s internal communication concerns can be safely abstracted over, a library that treats the flows as data objects themselves can be created that populates the sytem and changes it in otherwise pure ways. This allows the programmer to flow back and forth between the option to use pure manipulations and narrative manipulations depending on the use case. Additionally, as is the case with business to business transactions: collections of subprocesses can treat other collections of subprocesses as a macro process for sake of abstraction over an agreed communication protocol.

Math is Simpler than Natural Language. Mathematical models simplify, natural discourse obfuscates.

There is an aesthetic consideration that communication should be accomplished in the simplest possible manner. Simplicity is often hard. When you discover something simple, it tends to be universal. The language of mathematics is simple, and because of its simplicity, it tends to be confusing. Yet there is room for a philosophical critique that the simplest language is the most universalizable in potential. This runs afoul of pragmatic concerns.

Humans are not born into this world understanding math. The first linguistic tool all humans learn is messy, full of exceptions, and irregular. It’s natural language: full of tone, nuance, and context. Above and beyond that, a young human must learn to strip away context and talk in terms of formal abstractions. Many simple never learn this skill above and beyond a certain level. Pragmatically speaking then, while mathematics, abstract or concrete, has the potential as a universal language, it also requires a level of commitment or intelligence to reach fluency in, and therefore precludes its candidacy for a democratic language.

A prime example of math establishing a generic language is in regards to communication and side effects in programming languages. Most of these sorts of effects, such as time, space, divergence, can be encapsulated using a notion from applied category theory called a monad. If you read anything about programming in aggregators like Hacker News, chances are you have run across this word. And maybe you’ve also run across functor, applicative, typeclass, composition, identity, densities, ends, cones, isomorphism, anamorphisms, catamorphisms, yoneda lemma, kan extensions… (where is that Kmett generator when you need it).

And here is the crux. It takes a long time for most people to be comfortable even hearing the language of category theory, let alone internalize it to the level required to make proper use of it or to communicate it to others. For a lot of people, the math is simply intimidating, language, symbols and all. They will never learn it.

But — almost morally speaking — why should they? Monads as they are applied in computer programming, are a formal mathematical construction that captures the idea of composing functions whose values are annotated with additional data. That last part is an opaque way of saying communication. Any function whose output is a monad is communicating something in addition to the result. That communication could be failure, it could be a log, it could be the description for communication with an external system. These communications can then be sequenced meaningfully using the monadic combinators. This is all to say that monads capture communication and treat it both abstractly, generically, and mathematically.

I have to explain that. I have to explain monads. Let that sink in. The majority of programmers know how to talk. They, more or less, understand that humans have relationships, and that relationships are navigated using communication. This is wired deep into us, and you don’t have to even be cognizant of it to leverage it. Why am I going out of my way to tell people, “hey, you already have a powerful technology at your disposal, let me show you how to talk about talking in a way that you can’t understand?” I don’t have an answer to this question. There is an aesthetic beauty to the abstractness of monads that appeal to analytical minded people like me, so I think I erroneously assume everyone else has the same aesthetics.

Finally, I think the true death-blow to this aesthetic desire is the argument that a mathematical representation of a problem is necessarily a premature optimization. It is an optimization away from the most accurate encoding to one that ignores what are considered irrelevant details. Murphy’s law puts a time limit on how long those details stay irrelevant though. If you get lucky the intersection of when the abstraction breaks, and when you have to further maintain the code does not exist. More often though, when you aggressively pursue casting solutions into abstractions that ignore too much, you will be bitten more quickly.

The function f(x, y) = x + y is perfect until x and y are not providable at the same time. Turns out there is a monad for that (future/stream). And are not locatable from the same source. Turns out there is a monad for that too (environment/reader). The number of people who understand monads really well are few. The number who understand monad transformers (composing these effects) are fewer. The implementation of monad transformers in languages without sophisticated type checkers is a nightmare. So when we picked this beautiful mathematical abstraction that we attempted to force into a new requirement domain suddenly it got a lot more complicated.

Pragmatic

In addition to these aesthetic considerations are a handful of pragmatic objections.

Silliness and Ego

One possible reaction to the concept of story oriented programming, that I experienced myself, was how humbling it was (I just felt silly when I relayed the gremlin story to a coworker out loud) to talk about components of a system as if I was telling a story to a child. In reflection, this is not unexpected. Components of a system at this time will not be a deep and nuanced human, so talking about them as anything more than something like “see spot run” will simply not happen unless aspects are attributed to the actors that they do not possess. But no matter how expected this seems, it is almost shocking in a way to experience a reframing of a problem that makes it sound like any child could solve it. That is exactly the point. To democratize a process requires that it be rendered into parts consumable by the lowest common denominator. A 5 year old mind makes a great exemplar.

Avoiding humility or feeling silly or stroking a big ego are not valid (moral) reasons to preclude something that may otherwise lead to progress in an area that is difficult. If you are a very intelligent person, and you find that you have a hard time letting go of the need to midas-touch everything you interact with, you may be contributing to the problem of difficult to maintain and difficult to reason about software.

Condescension

A second objection I considered is the pragmatic issue of transition. Due the democratizing nature of this decomposition technique, systems problems can be more easily explained to non technical people so they may offer their own narrative reasoning. However, it seems conceivable that in some organizations, if you were to hear someone who normally talks in jargonese suddenly tell you a story about something that sounds like it’s for children, it could come across as extremely demeaning or condescending.

This seems relatively easy to mitigate by either only practicing it with a subset of the culture who understands the value it brings, while educating outside of the culture, or just starting by espousing the benefits to non technical people (project managers, etc) and that it sounds the same no matter who is talking about the problem space. The language does not dumb down the exposition to the listener, it lowers the barrier of entry across the board.

Identity Dynamics

The final pragmatic consideration that came to mind is chained off a consideration about purity and impurity. Sometimes people defer to mathematics so they can wash their hands of intentionally human affairs. An equation may say nothing about time, but neither does it say anything about gender or race.

Prominent code bases have had contentious conversations around the usage of identifiers or concepts like “master/slave” as being exclusive of Black Americans, or people inappropriately referring to sexual concepts in code (0x8008135). It is a valid concern that departure from the extremely formal world of mathematics necessarily can encode language people take issue with into discussions about an otherwise unsentient system. In my own story above I only had one actor of female pronoun.

My response to this criticism is that these issues transcend the solution I am presenting. You are no less racist or sexist just because you practice colorblind or genderless programming. Microsoft’s racist Tay twitter bot is an indication that the systems we build extend and encode the memes of the systems that produce them. Precluding your system from these concerns implies that you preclude the concerns from the system producing it, that is, your team.

Conclusion

That is Story-Oriented Programming in a box and why you may want to try it. I will be frank though. The idea seems wildly radical to me, as I fall aesthetically in the camp of people who want something like math to be the lingua franca of computers. I have used this post to espouse a deeply counter-advocated position that indicts my own position as elitist and inherently unscalable in both the artifacts it produces, and the systems surrounding artifact production.

Maybe it resonates with you. If so, I encourage you to try it today. I will for sure be attempting to put these ideas into practice soon in my own personal projects. Then maybe I will be brave enough to bring these concepts into larger discussions at my company, or in productions systems. The implications of this approach are extremely far reaching, and so the change they would bring would be sweeping.

The human condition is narrative. It’s time to directly weave narrative into our software approach. It’s time for some story-oriented programming.

  1. https://en.wikipedia.org/wiki/Structured_programming ?

  2. https://en.wikipedia.org/wiki/Object-oriented_programming#Criticism ?

  3. www.youtube.com/watch?v=ZrBQmIDdls4 ?

===

【CSDN編者按】其實,本文並不是說讓你將系統設計成故事一樣,而是利用故事的心理技巧來發布設計決策和討論。像講故事一樣設計系統的好處在於,可以讓開發者將設計決策分發給那些能修辭敘述的人,而不必非得局限於某個領域,如高階數學。

以下為譯文:

精靈的世界

Handle 是一個小精靈。Handle 可以創造其他精靈。Handle 會按照其喜好的樣子創造精靈,而他的想象總是成對出現。今天 Handle 創造了兩個孩子 Reqla 和 Resnak。但他不知道為什麽要創造這些孩子。只是為了存在而創造他們。繆斯神 Polys 啟發了 Handle,於是 Handle 用自己的創造力詮釋了靈感。創造完孩子以後,Handle 將他們送到了精靈學校,學校的精靈老師 Routin 負責幫助他們尋找存在的意義。

Routin 馬上明白了為什麽 Resnak 很重要。他發現,Resnak 存在的意義就是將 Reqla 的意義傳回給 Polys。每當有人告訴 Resnak 一些事情,他和 Reqla 就會融入以太。很重要的是,與 Resnak 交談的人應該用最佳方式與他交流,因為 Resnak 可以直接與 Polys 交談,而且他能夠辨別 Reqla 的價值,並告訴 Polys,從而讓 Polys 感受到自己的創作為人理解的喜悅。但他並不了解 Reqla 的目的。他與 Reqla 交談,試圖找出她如何才能最好地為 Polys 服務。

一般來說,當 Routin 在與精靈孩子打交道的時候,如果他能確定他們的目的,那麽他會派出這些孩子來完成目的。否則,他會讓能與 Polys 交談的孩子(這裏是 Resnak)去告訴 Polys,他不知道另一個孩子是來幹什麽的。

今天,Routin 發現 Polys 正在自己玩一種語言遊戲。由於 Polys 對自己每天用的語言感到了厭倦,所以他希望聽到其他語言。然而,Routin 不懂語言,不過他知道有人懂。於是他把 Reqla 和 Resnak 送到了唯一的一家精靈翻譯公司。

當他們到那裏的時候,他們遇到了公司的領班 Logon。Logon 掌握的語言程度剛好可以讓他找出公司裏最合適的人選。與 Reqla 進行了簡短的交談後,Logon 知道 Reqla 應該與誰交談,才能翻譯她帶來的消息。

讓我們暫停一下,多一點互動。

現在 Reqla 和 Resnak 與 Logon 在一起。Logon 知道有一個翻譯員能夠翻譯 Reqla 的消息。接下來會發生什麽?Reqla 和 Resnak 都應該去找那個翻譯員嗎?如果都去了,是應該由翻譯員跟他們打招呼,還是 Reqla 應該跟翻譯員打招呼呢?翻譯完了以後,是應該由翻譯員告訴 Resnak 翻譯的結果,還是應該讓 Reqla 告訴他呢?Resnak 是不是應該留在 Logon 那裏?如果他留下,那麽誰把翻譯結果帶回來呢?翻譯員應該留在原地不動嗎(也許他很胖,無法輕易挪動)?回來以後,捎信的人是應該直接告訴 Resnak,還是先告訴 Logon,然後由 Logon 告訴 Resnak?先告訴領班有意義嗎?領班需要現在內部(或者與他外部的會計同事)交涉一下,再告訴 Resnak 嗎?退一步說,或許讓翻譯人員直接面對另外三個人可能更加有效率。

無論你選擇回答哪個問題,最終都會導向同一個問題:什麽樣的推理在邏輯上與人為的世界一致。如果你發現翻譯人員都被關在象牙塔裏,而你只能沖他們大喊,他們才會回答你,那麽你就無法把他們叫到領班的辦公室。

如果你有 web API 開發的背景,你可能會意識到上面我們講述的是一個 Web 翻譯服務稍稍擬人化的故事。用典型的行業術語來說就是:請求和響應由底層的 http 服務器成對生成。處理程序將請求/響應分發到路由,由它來決定調用哪個子處理程序。如果是翻譯請求,那麽語言子處理程序則根據語言檢測的啟發,將請求分發到翻譯服務上,該服務可能不在同一個服務器上,但是可以通過子請求遠程訪問。翻譯完成以後,通過響應體將翻譯結果返回給請求者。

顯然,用專業術語描述該服務要比故事簡短得多。而且,短小精悍是一種非常誘人的優點。

但對於實現需求而言,這種簡短是否會造成模棱兩可或晦澀難懂呢?

當我向另一個開發人員描述該服務的時候(如果他們問及),肯定會采用第二種專業術語的方式。我當然不會像上面那樣編造一個故事。(我花了至少 30 分鐘才確保了故事敘述的一致性,且合乎大眾讀者的口味,雖然我仍然不確定是否合乎大眾的口味,哈哈。)

但是,在我試圖將上面的故事變得更詳盡,同時強迫自己將各個組件分解成活躍的子進程(即精靈)並讓他們的行為更像人類的過程中,我發現這裏引發的橫向思考往往被「正規」系統「設計」所忽略。

敘事推理

人類的直覺推理似乎與敘事的概念緊密相關,特別是涉及其他人類角色的敘事。盡管人類能夠推理一些看似與其他人類沒有明確關系的事物,如等邊三角形等正幾何體,但這種行為貌似僅限於鐘型曲線的右側。(註:這裏鐘型曲線指人類的智商分布呈正太分布,「鐘型曲線的右側」指高智商的那些人。見《鐘型曲線:美國社會中的智力與階層結構》,理查德?赫恩斯坦、查爾斯?默裏,1994 年 9 月)

正常人類都有敘事推理的能力。這是最普通的人類的方式。在這無數的推理中,可能隱藏著一個突發問題的解決方案,即便是一個平常的人也可以解決。這類似於支持向量機(相當於一個非常聰明的人)等非常強力的分類器與鏈式決策樹樁(相當於一群普通人)等可調節的增強版的弱分類器之間的區別,在機器學習中,水平一般的人由於其平庸無法貢獻驚人的解決方案,但他可以向問題施加適當的壓力從而得到解決方案。這反而讓我想起了人民的智慧是無限的。有些人比所有人都聰明。但是現實中這樣的人非常罕見,而且有趣的是聰明人無法與其他聰明人或不聰明的人很好地相處。

如果我們想對復雜事物(例如系統等)的推理進行民主化,特別是在推理引發這種復雜性的事物間相互作用時,一個似乎很明智的做法是將這些組件當作人類代理或意向載體,因為使用這種方法,普通人也可以進行推理。太多的情況下,我們會在推理過程中認為,人的意圖是由於系統的組成部分而產生的。那麽,不要將專業術語和行為類比混在一起使用,而是將組件當成小人,或小精靈,小旅鼠等等,會怎麽樣呢?如果我們不用高科技術語來發表關於系統開發的演講,我們只是講故事,會怎麽樣呢?

道德論點

我們的世界中存在必要的道德,軟件也一樣。當今世界受到軟件的驅動。其中大部分都寫得很糟糕,很難對其進行推理,但是很神奇的是 80% 的時間裏它們依然能夠正常工作。而 bug 肆意猖獗,即便在已解決的問題領域中,bug 也是層出不窮。很多時候,這些 bug 並不是由於推理單個組件運作的困難性,而是由於推理組件之間的關系和溝通模式的困難性。將軟件分解成組件是消滅 bug 的良好開端,成千上萬的文章都揭示了其中的緣由。

我們需要編寫可靠的軟件。人們的生活常常依賴軟件,包括我自己。為了履行這最基本的道德標準,我們必須認識到目前關於系統的推理方法是不充分的。

解構歷史

歷史上出現過好幾種構建系統的重要方法,試圖增強系統的可推理性。一些例子如結構化編程,九十年代的面向對象編程,以及最近又興起的函數式編程。然後,橫跨所有這些方法的還有靜態類型,試圖在某些方面進行自動推理,以減輕人類的繁重工作。

九十年代之前被廣泛采用的是結構化編程,即依照一些啟發式原則,將過程分解成子過程,以保證過程的正確行為。但子過程可以做任何事,它們可以通過不可預測的方式操作其他過程依賴的狀態,等等。維基百科說:

結構化編程是一種編程模式,它廣泛使用結構化控制流程,如選擇、重復、塊解構、子例程,試圖增強計算機程序的清晰程度、質量並減少開發時間。結構化編程避免使用簡單條件測試和 goto 等跳轉語句,因為這些會導致「面條式代碼」,增加閱讀和維護的難度。

九十年代流行的分解問題的方式,現在依然被稱為「面向對象編程」。不幸的是,Alan Kay 博士早在七十年代發明了面向對象的概念,其實與當下流行的面向對象並不相同,卻完全被人遺忘了。這種面向對象編程的主要問題是,事物擬人化經常會造成扭曲的設計,如「電梯對象詢問樓層對象以得知是否需要停止運行」。什麽意思?樓層又不會說話,電梯也不會說話。但人們都說,只要看看需求文檔,裏面的名詞就是對象的候選者。對於後來成為 OOP 的概念也出現過許多批評。

由於面向對象的這種荒謬的模仿,許多人選擇了完全拋棄面向對象,轉而重新擁抱數學上的聖杯,即函數式編程。函數式編程不會作擬人式的地板和電梯,而是用向量表示電梯的位置和樓層的位置,將這些向量傳遞給某個分析算法,同時傳遞呼叫按鈕的狀態向量。這些向量的組合就會產生一個動作描述,控制哪部電梯應該停在哪一層。

談論數學的人總是看上去比那些不會數學的人聰明些。而真正理解你所談論的數學會令人欽佩。當人們說「單子不過是自函子範疇上的一個幺半群而已」(a monad is just a monoid in the category of endofunctors)這句名言時,如果你確實能理解這句話,那麽恭喜你,你是為數不多的聰明人之一。

所有這些編程的形式中都包含了類型,以針對程序的正確性做出某種程度的自動推理。事實證明,如果你真的追求簡單,那麽編寫用於抽象對象創建的類型(class),或者用於抽象數據結構之間的數學屬性的類型(Haskell 的 typeclass)實際上是很困難的。讓那些想把事情做好卻受教育程度不高的人使用有類型的語言,通常會導致類型爆炸。類型 A 和類型 B 可能有 50% 的功能是一樣的,因為它們都是某個底層類型 C 的特殊實現(ad-hoc realization),但發現底層抽象 C 可不是簡單復制粘貼功能相同的那 50% 代碼就能做到的事情。當 A 和 B 以某種不應該出現的方式耦合在一起時,這種特殊繼承就會導致極其脆弱的代碼。

Alan Perlis 的一句著名的格言說:「100 個函數操作同一個數據結構,要好過 10 個函數操作 10 個數據結構。」這個簡介的斷言通常可以由抽象代數中的概念論證。但是,找到一個能支持 100 個有用的函數的數據結構有時很簡單(第一個想到的就是列表),有時卻十分困難(程序的輸入輸出)。

提議

讓系統設計能夠被眾包,從實用性、美學和性能方面都很有價值。

還有道德價值。

系統的組成結構應該盡量模仿人類。我的意思是,組件應該擁有目的,或者說擁有直接的責任,它們的交互應該可以類比為兩個或多個人類之間有意義的交互。用這種方式分解系統,任何熟悉系統的人就能像敘述人類行為一樣描述系統的行為。同時這種方式還能讓非技術的人推理這個系統。這就保證了更多的人能夠更現實地使用他們用了一輩子的辦法:考慮人與人之間的關系,借此來找到系統設計問題的解決方案。這就是系統設計的民主。這就是面向故事編程。

這種想法並不是我的原創,只不過是前面的思想在我腦海裏的轉述而已。James Coplein 在 GOTO 2017 大會上有一次演講,他說原始的概念化 OOP,就是用戶思維的本體直接反映到系統設計的本體。如果在屏幕上看到一只狗,那麽就應該能在代碼中操縱(以某種形式)一只狗。這直接反映出某些面向對象設計的荒謬(如角色走向墻的對象,然後墻對象告訴角色兩者碰撞了)。這個思想引發了我的一連串思考。

這次思考還產生了些新的東西,就是它內在和外在的同像性(homoiconicity)。YouTube 上有許多 Alan Kay 的談話,他談到了他最初關於規模擴展的想法來源於已經支持擴展的東西:人體的細胞模型。人體和大腦是個單例組件(singular component)在巨大規模的深度網絡中能很好工作的典型例子。但是,並沒有哪個細胞比其他細胞更「聰明」,這很像是我們談論某個人比其他人聰明一樣。

這給我帶來了一個問題:如果人體本質上是生命對組成細胞的某種分布和民主,那麽其他形式的民主也許可以用來創造大規模的人造物。Alan Kay 博士提議,在計算機系統中像細胞那樣將任務民主化成組件,就能催生可擴展的系統。這無疑是給九十年代的 OOP 一記響亮的耳光,後者在某些情況下顯然並不成功。回到 OOP 的原始概念來,重新思考下。這次不想象成細胞,而是將系統換成一系列角色、精靈、人類或者隨便什麽東西,讓他們參與到故事中,我們就創造了一種推理系統,幾乎任何人都可以參與這種推理。用擴展系統的同樣方式,我們擴展了系統設計的過程。

反對意見

在構思並充實這篇文章的過程中,我收到了許多內部的反對意見,在這裏我想一一介紹下。這些意見分為兩種:美學的,和實用的。美學上的反對意見,指任何可變的因素(美學)造成的偏見。實用的反對意見,指那些認為某些提議由於社會限制或心理限制從而無法實現的觀點。

美學

歸謬法,滑坡謬誤,如果在一個地方做了,那麽每個地方都要做。

這種觀點認為,一切事物,甚至包括最簡單的一個數字,都應該是個角色。我也不知道是否應該同意這種觀點,我只是展示下這種觀點而已。

現實的宇宙似乎已經證實了,交流性的解構可以一直進行下去。我們可以說(實際上確實在說)誇克互相交換帶有顏色的膠子。質子、電子等帶電粒子交換光子。這些例子用人類的交流或意圖來比喻極其簡單的現象。

有個現象領域與物理沒有任何關系。正規思維體系的產物,比如數學,是不存在於物理世界的。這可以作為一條明確的分界線,判斷某個事物是否應該作為角色,或者作為實體過程。

因此,我們不會說數字 5 跟另一個數字 5 說「相加」。相反,我們會直接用那個正規領域的語法結構。類似地,集合論有它自己的組合數學,範疇論也是,高中代數也是,這些思維體系都構成了封閉且自我約束的推理系統,不需要任何其他交流方式。

關於這個問題的一點警告是,必須謹慎地理解,純凈性很容易被破壞。你可以把數字和字符串當做純粹的數學結構來處理(因此不需要通過交流性的結構去分解它們),但一旦它們的純粹性被打破(如一個可改變的字符串,或者很大的數字導致必須考慮內存分配的問題),那麽就又進入了與物理有關的世界,可以再次進行敘述性解構了。

一個與現實世界關系更緊密的場景就是 HTTP Web 服務器的例子。HTTP 協議規定了字節必須以某種規定的格式傳輸。這種格式由字符串組成,進一步又規定什麽才能組成字符串。但是,協議的其中一部分可以不遵循這種格式,比如請求體。實際過程中,你可以將請求看作一個字符串,並當做純粹的數學實體來處理。這種方式一般沒有問題,但當你遇到真實世界的限制,如很大的請求體導致耗盡所有內存,或導致性能問題的時候。這種限制會打破字符串的抽象,顯露出請求的本質:對網線上傳輸的電信號的一種解釋而已。在低層次上對其進行處理,還反映了現實世界的物理過程的本質。因此,應該避開字符串抽象,將請求看作一個角色,它能與底層進程交流其字節,這樣就能更精確地討論前面提到的優化過程。

這些概念的一種迷人的組合形式就是,首先用敘述性方式設計一個子系統,然後,如果它的內部交流可以被安全地抽象,就可以創建一個庫,把流程當做數據對象來處理,用流程填充整個系統,並以純粹的方式交換這些流程。這使得程序員可以根據實際場景,在純粹的操作和敘述性操作之間做出選擇。另外,可以像企業之間的交易那樣,一組子處理的集合可以根據某個獲得共識的通信協議,以抽象的方式把其他子處理的集合當做一個宏處理。

數學比自然語言更簡單。數學模型讓事物簡化,自然語言讓事物晦澀。

有一種美學的觀點是,交流應該以盡可能簡潔的方式完成。然而簡潔通常很難達到。簡潔的事物通常會傾向於通用。數學語言很簡潔,而正由於其簡潔,它變得很難理解。當然,哲學評論家會說最簡潔的語言是最通用的語言。但這與實用性是矛盾的。

人類並不是一生下來就懂得數學。人類學習的第一種語言工具是完全混亂的,充斥了各種例外和不規則。這就是自然語言:各種語調,語氣,還有語境。在這之上,年輕人必須學會剝離語境,用正式的抽象語言進行交流。這種技能是許多人根本學不會的。那麽從實用角度來說,不論是抽象數學還是具體數學,盡管它有潛力成為通用語言,但它需要一定水平的能力或智力來達到流利的水平,這就阻礙了它成為民主的語言。

一個利用數學作為通用語言的基本例子,就是編程語言的交流和副作用。這方面的許多事物,如時間、空間、分歧,都可以用某種記號來表示,在應用範疇論中稱為單子(monad)。如果你用過某種新聞閱讀器(如 Hacker News)閱讀編程的文章,那麽你對這個詞應該不陌生。你還可能遇到過函子(functor)、加強函子(applicative)、類型類(typeclass)、組合(composition)、恒等(identity)、密度(density)、端(end)、錐(cone)、同構(isomorphism)、anamorphism、catamorphism、米田引理(yoneda lemma)、kan 擴展(kan extensions)……

這就是最棘手的部分。絕大多數人連順利地閱讀範疇論中的語言都需要花費很長時間,更不用說讓他們具有足夠的水平去順利地應用這些語言,或者用這些語言與他人交流了。對於大多數人來說,數學就是個怪物,完全是看不懂的符號語言。他們絕不會學的。

但是——這就涉及到道德了——為什麽他們會這樣?在計算機語言中,單子是概括帶有額外數據的函數組合的正式數學結構了。其中的「額外數據」正是交流中晦澀的部分。任何輸出為單子的函數都在正常的結果之外交流了某些東西。交流的東西可能是失敗,可能是日誌,也可能是為某個外部系統進行針對交流的描述。這些交流可以用單子組合有意義地連在一起。也就是說,單子能用抽象、通用、數學的方式概括所有交流。

我必須解釋一下。我得解釋下單子。大部分程序員都知道怎麽說話。他們或多或少都明白人類之間有關系,這些關系通過交流進行。這些都是我們的本能,使用這些能力都是下意識的。所以為什麽我要用另一種方式跟別人說,「嘿,你有一種強大的能力,我來告訴你怎樣才能用你理解不了的方式來交流」?我不知道該怎麽回答。單子的抽象對於我這樣擁有分析思維的人來說的確很美,所以我錯誤地假設所有人都有同樣的美學觀點。

最後,我認為對於這種美學欲望的最後一擊就是關於對問題的數學表現形式是否屬於過早優化的爭論。對於不想關心無關細節的人來說,數學表現形式遠遠不是最準確的優化。盡管墨菲定律對於這些無關細節能保持無關多久給出了時間限制。如果你很幸運,那麽你能遇上當抽象被破壞時,你要維護的代碼恰好不存在了。不過更多的情況是,你越是激進地抽象,得到報應就越快。

函數 f(x, y) = x + y 完全沒有問題,除非 x 和 y 不同時存在。這種情況有個單子可以描述(future/stream)。或者無法從同一個來源獲得。也有個單子可以描述(environment/reader)。能理解單子的人真的很少。能理解單子變換(monad transformer,組合單子的效果)的人就更少了。在沒有類型檢查的語言中實現單子變換就是個災難。所以,一旦將美麗的數學抽象強行引入到新的需求域,事情就一下子變得復雜了。

務實

除了這些美學方面的考慮之外,還有一些實用的反對意見。

愚蠢和自大

對於面向故事編程概念的一種可能的反應是:像我給孩子講故事一樣討論系統的組件太難為情了,我自己也有過這樣的經歷,當我大聲地向同事講述小精靈的故事的時候,覺得自己好傻。仔細想想,這也並不意外。這種情況下,系統的組件不是深刻而又細致入微的人類,所以討論它們不像討論《警犬追殺令》那麽簡單,除非我們討論一些演員欠缺的方面。但是,無論預期結果怎樣,要想重新構建問題讓任何孩子都能解決,那都是一件駭人聽聞的事情。解構的過程需要將門檻降到最低,比如 5 歲的孩子都可以做到。

害怕難為情,犯傻,或自大都不是好的理由,我們要排除萬難。如果你是個非常聰明的人,而且你發現自己很難放下做好一切事情的想法,那麽你可能會導致已然艱難的軟件推理雪上加霜。

屈尊

我所想到的第二個反對意見是過渡的實際問題。由於分解技術的解構特性,我們很容易給非技術人員解釋系統問題,因此他們可以提供自己的敘述推理。然而,可以想象到在一些組織中,如果你聽到通常使用行業術語的人突然用給孩子講故事的口氣講話,會感覺有點自降身份或屈尊。

如果只對一小撮明白這種練習帶來的價值的人進行練習,那麽相對會容易一點,同事要培養外部的人,或只向非技術人員(項目經理等)解釋其中的好處,至於由誰來討論問題並不重要。語言並不會減弱對聽眾的闡述,它可以降低門檻。

身份動態

最後一個我想到的顧慮是純粹與不純粹的考慮。有時人們會依靠數學來解決人事問題。方程不會涉及時間,也不會論及性別或種族。

優秀的代碼庫曾引起過爭議,主要是圍繞身份,美國黑人之外的「主人/仆從」等概念上,或人們在代碼中(0x8008135)不恰當地引用性別概念。這是一個值得關註的問題,除了非常正規的數學世界以外,其他非科學系統的討論中也必然會摻雜人們所關註語言編碼問題。在文本上述的故事中我只有一位女性代詞的角色。

我對於這種批評的回應是:這些問題超出了我提出的解決方案。僅通過參與色盲或無性別差異編程,無法改變你的種族歧視或性別歧視。微軟的種族主義者 Tay twitter 聊天機器人就是一個例子,我們構建的系統擴展並將網絡用語編碼到系統中,因而產生了這種機器人。為你的系統排除這些憂患意味著排除系統產生的問題,也就是你的團隊。

結論

本文介紹了面向故事的程序設計,以及值得嘗試的原因。坦白來說,這個想法對我來說似乎非常激進,因為面對那些希望數學成為計算機通用語的人群時我會華麗麗的倒下。鑒於與通用語相關的產品以及這種產品周圍的系統的優點以及其與生俱來的無法改變規模的特性,我想用這篇文章來表達自己完全站在對立面的立場。

也許這可以引起你的共鳴。如果真是這樣,我鼓勵你大膽嘗試。我肯定會在自己的個人項目中將這些想法付諸實踐。之後,我會勇敢地在公司和產品系統內開展關於這些概念的更大的討論。這種方法的意義非常深遠,所以他們帶來的改變將席卷全球。

人類善於敘述。我們可以將敘述直接編入我們的軟件方法中。讓我們進入面向故事的編程時代。

ComPiler200003:Story-Oriented Programming