ComPiler200003:Story-Oriented Programming

Posted 0924

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ComPiler200003: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的主要内容,如果未能解决你的问题,请参考以下文章

Compiler讲解

“@angular/compiler-cli”包没有正确安装。错误:找不到模块'@angular/compiler-cli'

哈希模板

maven maven.compiler.source和maven.compiler.target的坑

哈希表(开放寻址法)

哈希表(开放寻址法)