Stelpolva Write


Read the latest posts from Stelpolva Write.

from senioria

Well... Since the title was in English...

Let's skip all the common preambles illustrating the importance, etc. of partial evaluation, and just say we want to write a partial evaluator for a procedural language, which Senioria didn't find any articles introducing many details. Note that this is only a personal note, and the algorithm here may not be the optimal :)

The language has the following characteristics:

  • Functions don't take closures, or, in other words, closures are passed to functions as an implicit argument;
  • Instructions are grouped in basic blocks, thus all branch instructions may only occur at the end of a basic block, and each block is terminated with a branch instruction.

Concepts and basic partial evaluation

We all know how to write an interpreter, right? And yes, this is how a partial evaluator works: just evaluate the instructions, record all variables (here, the results of each instruction), and generate corresponding instructions when the result of the instruction may only be known at runtime, i.e. the instruction must be generated. Other optimizations may also apply, like pattern match the instruction or a sequence of instructions and replace them with a more efficient version, or postpone the instruction generation, and only write the values used by the control flow and side effect-generating instructions into the result (specialized) function to do dead code elimination.

Here, we call the dictionary recording all variables “poly”, which is an abbreviation of polyvariant, which... means that multiple versions of the function is generated, or more precisely, specialized. Not a good name, frankly.

Let's see an example of our partial evaluator at this point, for the following function:

int f(int n) {
  int i = 5;
  goto if_cond
  bool _0 = i < n;
  if _0 goto if_then else goto if_else;
  int i1 = i + 1;
  goto if_end;
  int i1 = i + 2;
  goto if_end;
  int i2 = i1 + n;
  return i2;

With n = 1, our partial evaluator would be working like this:

int f_1() {  // Specializing the function, poly: { n = 1 }
  int i = 5;  // poly: { n = 1, i = 5 }
  goto if_cond;  // poly unchanged, emit the jump, see our introduction below
  bool _0 = false;  // poly: { n = 1, i = 5, _0 = false }
  goto if_else;  // poly unchanged, since the result of the branch is known, just jump to the desired block
  println("ge");  // (Same as above, idem for the followings (x)
  int i1 = 7;
  goto if_end;
  int i1 = 8;
  return 8;

If this is not good enough, by not emitting the constant assignments and a pass that we would introduce later, the resulting function would be like:

int f_1() {
  return 8;

And with n unknown, the result would be like:

int f(int n) {  // poly: { n = (arg) }
  int i = 5;  // poly: { i = 5, n = (arg) }
  goto if_cond
  bool _0 = i < n;
  if _0 goto if_then else goto if_else;  // Unable to determine, generate both branches
  int i1 = i + n;
  goto if_end_1;
  int i1 = i + 2;
  goto if_end_2;
  int i2 = i1 + n;
  return i2;
  int i2 = i1 + n;
  return i2;

Here we choose to keep the unconditional jumps, since they can be eliminated easily by adding another pass pasting the target block to the position of the jump and eliminating all blocks not referenced by any jumps after pasting —– a pass requiring almost no analysis but translating natural language. And by keeping the blocks, we may eliminate redundant specializations by only emitting the jump and not performing the evaluation when we found ourselves jumping to a same block with the same poly, or we may record the source block in the poly, thus we can directly say “when we found we are jumping to the same poly”.

In the example with unknown n, we generated two different blocks for the same source block if_end. This can simplify some analysis since all variables have only one possible value. Some implements would be in favor of keeping the resulting blocks one-to-one to the source blocks as possible, and only record the variables as dynamic, or record all possible values of the variables. We don't like this because this would reduce the optimization potential by losing information by only record variables as dynamic, or be complex by requiring to order the blocks to evaluate all predecessors of a block before evaluating the block. If we want to reduce the generated code size, there are some more analysis we can add, e.g. collect the values that may be used by each block and their successors, and remove all unused variables from the poly, thus we can avoid emitting the same block twice just because an unrelated value in the control flow, or scan up from the bottom of each block and collect some or all reoccurring instruction sequences into one block, depending on how compact we want the code to be.


At the point, our strategy works well: constant values are propagated to eliminate unused branches and generate appropriate specialized instructions; what's better, many loops are automatically unrolled completely[^1], some loops have dynamic conditions and can't be unrolled completely, but after one iteration, the body finds itself jumping to an evaluated poly, and the evaluator continues happily. But a simple program would drive our evaluator into infinite loop:

[^1]: Though may be overly aggressive, that we may need to add one more pass to reroll the repeating instructions back into a loop, or add some analysis to process big loops differently

int *gv;
int f(int n) {
  for (int i = 0; i < n; ++i)
    gv[i] = 0;

As we can see, i changes every iteration, so our evaluator tries to evaluate all the iterations, but the condition is dynamic, which means on the static perspective, there are infinite possible iteration to be generated, Oops.

We may come up with some simple solution: store both the constant value and the instruction returning the value, and ignore the constant value when deduplicating blocks, or only record the instructions. Great, now we are generating gv[0] = 0 for every iteration, or give up unrolling constant loops.

Well... Other partial evaluators seem not to be bothered by the problem, because one of these applies:

  • We just don't know them;
  • They are partial evaluating some language without loops, so they have other problems to face :);
  • They are evaluating some higher-level, structural IR, where they can directly access the “header” of the loop, and determine ad-hoc whether the loop can be unrolled, and use the correct strategy.

After all, in our IR, we should process non-unrollable loops specially.

First we should know whether we are in an non-unrollable loop. Our solution is performing an analysis to obtain all loops and their containing blocks first, then when we encounter a conditional jump, check if we are at the head block of a loop, and if so, pollute the poly to make every variable that is read in the loop and written in both the loop body and before the head block dynamic, and reevaluate the head block with the polluted poly. This way the loop header is adjusted not specialized with values in the first iteration of variables varying in the loop. And since this only applies to conditional jumps, unrollable loops are not affected because their jumps in the header can be specialized to an unconditional jump.

This is enough for code-generation of the loop, since all variables both changed in and read by the loop is correctly set dynamic, and we would only need to repeat the story on the loop once, because when we encounter the header and the body the second time, we are with a different poly, in which there are variables in the loop body, and then after the second time the body is evaluated, the evaluator would recognize the fact that the incoming header block is evaluated in the second iteration, and end the evaluation to the loop.

Forcefully pollute all read and written variables may be rough, and we can do better. There is an instruction called phi, which yields a value depending on from which block the control flow gets to the phi instruction. With this instruction, we can transform the program to SSA form, i.e. all variables are statically written once, by replacing all reassignments with a new variable, and when there are different possible values of a variable, inserting a phi instruction, since they must be from the different block, one block a different value respectively. With SSA form, we can keep the phi instructions, which normally would be eliminated since in both actual evaluation and partial evaluation, the actual control flow can only go along one path, determining the value of the phi instruction, in the reevaluation of the header block, and mark the value as dynamic; since all variables in the SSA form must be assigned before, this can ensure all variables both changed in the loop and used by the loop are set to the correct source —– the phi instruction. This transformation can be more accurate, and may be more universal.

With this strategy, we must memorize the arms of every phi instruction in the header block of the loop, and fill in the correct value when the control flow reenters the loop. And then we found another problem: the poly would be different after evaluating the loop body, so without special process, we would be attempting to create a new header block, and repeat the iteration once; this is OK when we are free to assign, but the first phi instruction would be left with only one value, which is not allowed in some architectures like LLVM. To reuse the header block we just generated, we can record in the poly a path of dynamic loops, push a loop into it when we found ourselves encounter a dynamic loop at the conditional branch, and pop the loops we are leaving at other jumps; when we are going to jump, check if we are jumping to the header of current loop, and if so, fill in the phi arms and reuse the generated block.

That's all of our strategy for partial evaluating non-unrollable loops. This is Senioriae source code handling branches and phis:

void PeInstVisitor::visitPHINode(llvm::PHINode& inst)
  if (poly.inloop) {
    auto val = inst.clone();
    auto phiv = llvm::dyn_cast<llvm::PHINode>(val);
    val->insertInto(poly.insbb, poly.insbb->end());
    PhiPatch patch{ phiv, {} };
    for (size_t i = 0; i < phiv->getNumIncomingValues(); ++i) {
      patch.arms.emplace(phiv->getIncomingBlock(i), phiv->getIncomingValue(i));
    while (phiv->getNumIncomingValues())
      phiv->removeIncomingValue(unsigned(0), false);
    auto curv = inst.getIncomingValueForBlock(poly.from);
    phiv->addIncoming(poly.get(curv), poly.genfrom);
    poly.set(&inst, val);
  } else
    poly.set(poly.ip, inst.getIncomingValueForBlock(poly.from));

void PeInstVisitor::visitBranchInst(llvm::BranchInst& inst)
  auto brto = [&](PePoly& poly, llvm::BasicBlock* dst, bool lastjmp = true) {
    auto path = lastjmp
                  ? poly.path
                  : std::make_shared<std::vector<LoopPathItem>>(*poly.path);
    auto src = inst.getParent();
    PePoly res{
      poly.env, poly.base, &*dst->begin(), src, nullptr, poly.insbb, dst, path,
    // Pop the loops we are leaving (or in other words, not jumping into)
    while (!path->empty() && !loops.getLoopFor(path->back().src)->contains(dst))
    // Br to the cond block of the loop
    auto reloop =
      !path->empty() && dst == path->back().src ? path->back().gen : nullptr;
    if (reloop) {
      for (auto& patch : bbphi[reloop]) {
    } else {
      res.env = std::make_shared<llvm::ValueToValueMap>(*poly.env);
      res.base = std::make_shared<llvm::ValueToValueMap>(*poly.env);
    // If new block: create the block
    auto blkit = polybb.find(res);
    if (!reloop && blkit == polybb.end()) {
      res.insbb = llvm::BasicBlock::Create(ctx, dst->getName());
      polybb.emplace(res, res.insbb);
      bbpoly.emplace(dst, res);
    return std::make_tuple(res,
                           reloop                  ? reloop
                           : blkit != polybb.end() ? blkit->second
                                                   : res.insbb);
  // Get the target bbs
  llvm::BasicBlock* thenbb = inst.getSuccessor(0);
  llvm::BasicBlock* elsebb = nullptr;
  auto cond = inst.isConditional() ? poly.get(inst.getCondition()) : nullptr;
  if (inst.isConditional()) {
    auto cc = cond ? llvm::dyn_cast<llvm::ConstantInt>(cond) : nullptr;
    if (cc && cc->isZero())
      thenbb = nullptr;
    if (!cc || cc->isZero())
      elsebb = inst.getSuccessor(1);
  // Unconditional
  if (thenbb == nullptr || elsebb == nullptr) {
    auto dst = (thenbb != nullptr ? thenbb : elsebb);
    auto [sub, dstbb] = brto(poly, dst);
    llvm::BranchInst::Create(dstbb)->insertInto(poly.insbb, poly.insbb->end());
    if (sub.insbb)
  // In loop: switch to loop mode and regenerate the block
  if (loops.isLoopHeader(inst.getParent()) && !poly.inloop) {
    auto next = poly;
    next.env = std::make_shared<llvm::ValueToValueMap>(*poly.env);
    next.ip = &*inst.getParent()->begin();
    next.inloop = true;
    next.path->push_back({ inst.getParent(), next.insbb });
    // Require an rerun
    polybb.emplace(next, next.insbb);
    next.insbb->erase(next.insbb->begin(), next.insbb->end());
  // Conditional: copy the inst and insert
  auto val = insinst(&inst);
  auto brval = llvm::dyn_cast<llvm::BranchInst>(val);
  auto ping = [&](int idx) {
    auto [sub, dstbb] = brto(poly, inst.getSuccessor(idx), idx == 1);
    brval->setSuccessor(idx, dstbb);
    return sub;
  auto lsub = ping(0);
  auto rsub = ping(1);
  if (lsub.insbb) {
  if (rsub.insbb) {

There are some problems left, like how to obtain the loops and how to transform the program to SSA. Senioria didn't really implement them, since they are already done by llvm, but the algorithms seem to work well.

For loop detection, we can first make the domination graph, a directed graph of blocks with blocks as nodes and jumps in the blocks as edges. Then we can DFS on the graph, maintaining a stack of visited nodes, and when we visit a visited node N, add all nodes in the stack above N into the loop with header N. We can safely assume that a block an only be the header of one node here, since we can't do any better without better specialization technique.

For SSA transformation, we can iterate the blocks in topological order, ignoring edges from a loop body to its header when entering the header, replacing every reassignment with a new variable, and replace succeeding accesses to the assigned variable with the new variable, and create a phi instruction at the beginning of a block for potentially used variables in the block.

Further steps

We haven't discussed how to process functions yet, which is also crucial, and is at least the same complex as processing loops. Well, Senioria am lazy :)


from Nanako's Thoughts







我奶奶则对我要去留学特别高兴,虽然她也不知道国外是哪里、到底有多远,但是她觉得我要去读研究生这件事特别光宗耀祖(p.s.我家太穷了,我是我们家族里literally第一个学历读到研究生的人),她就把她所有的积蓄都拿出来给我了TAT 鄙人第一次被这么明显的偏爱,完全手足无措,哭了好几回。这个时候我还思考了一下,难道这就是男宝的普通待遇吗,就是在你想干什么的时候,家里倾尽所有地帮你,不考虑你以后到底还不还得上来……而对我来说,我外公外婆家的态度才是正常态度,根本没指望能得到他们的支持,反正老娘有钱,我要走谁拦得住我呵呵





  1. 中介赚的就是信息差的钱,自己多花点时间找资料也能弥补
  2. 自己的申请自己最上心,中介才不会关心
  3. 我没那么钱浪费(流泪



谷歌搜xx国家的大学列表 $\rightarrow$ 去官网收集资料 $\rightarrow$ 按照资料准备 $\rightarrow$ 去官网申请















此外德国的房租也不是很贵,毕竟德国很大,具体要看住在哪个地方,但总体来说就是选择非常多,总有便宜的选法;德国的物价在欧洲内也算比较ok的。根据资料,在德国生活,一个月平均需要842欧(引自Costs of education and living)。









这两放在一起写吧,主要是真的没什么选择。像我这样的肯定是只能考虑物美价廉的公立大学了,比如说我曾经考虑的佛罗里达州立大学(FSU)的国际学生学费是一年18,746刀,那么我的预算大概是够两年的学费加一年的生活费(同时还得缩衣节食),不过它的奖学金机会还算比较多,这里有一个官方提供的表格(引自Graduate Student Financial Support Policy and Statistics),可见这所大学的计算机系的助学金获得率是比较高的,能有76.7%,但是数据比较久了,不知道现在是什么情况。













  1. 毕竟是小语种国家,很多工作都需要荷兰语。
  2. 最近几年的政策逐渐排外,非欧盟身份很不方便;比如找兼职的时候有很多家都因为我的非欧盟身份拒绝我。
  3. 物价高,比德国美国都高。我跟美国的朋友聊天,她说她那里(波士顿)的越南河粉一份12刀,我这里的越南河粉一份18欧,呵呵🚬
  4. 交通费贵。想想德国学生免交通费,可恶,好嫉妒!
  5. 国际生有严格的工作时长限制。一周16个小时/暑假三个月全职工作,二选一。
  6. 奖学金少。










from 锦心


本文是 Michał “rysiek” Woźniak 的博客 Mastodon monoculture problem 的翻译。 source:


  • 联邦宇宙:Fediverse,是使用一些协议彼此链接的一系列网站/服务(被称为“实例”)的总称。在联邦宇宙,你在任何一个实例上拥有账号,你就能访问几乎整个联邦宇宙的所有内容——实例有特殊规则的时候除外。
  • 实例:联邦宇宙上的单独一个网站/服务。
  • 实例软件:运行实例使用的软件,比如 Mastodon。用一种实例软件可以搭建很多个实例。
  • ActivityPub协议:一种旨在让不同的实例之间可以互相连接,共通账号的协议。联邦宇宙实例很多使用的就是ActivityPub协议。
  • Mastodon:连接到联邦宇宙的一个微博类社交媒体软件,有类似Twitter的设计。

依照 Michał “rysiek” Woźniak 的原文 (,本文以 CC-BY-SA 4.0 协议发布。您可以自由地共享、演绎本作品,但是必须署名、以相同方式共享 – 如果您再混合、转换或者基于本作品进行创作,您必须基于与原先许可协议相同的许可协议分发您贡献的作品。

Mastodon 非营利组织^[译者注:Mastodon-the-non-profit]的首席执行官兼 Mastodon 软件的首席开发者 Eugen Rochko(在联邦宇宙^[译者注:原文是Fedi, 即Fediverse的简称, 指联邦宇宙]上被称为 Gargron)最近的举动让一些人担心 Mastodon(即是该软件项目,也是该非盈利组织)对联邦宇宙的其余部分造成的巨大影响


到目前为止,联邦宇宙上大多数人都在使用 Mastodon 软件。 截至撰写本文时,最大的实例 拥有超过 200,000 个活跃帐户。 这意味着这单独一个实例上涵括了整个联邦宇宙的大约 1/10。 更糟糕的是,Mastodon 软件经常被认为是整个社交网络,这掩盖了一个真相: Fediverse 是一个由更多样化的软件们组成的更广泛的系统

现在它就已经产生了糟糕的后果,而且以后可能会更糟。 让我困扰的还有,我以前也见过这样的情况:


几年前,我在联邦宇宙的前身有一个账户。 它主要基于 StatusNet 软件(后来更名为 GNU Social)和 OStatus协议。 迄今为止最大的例子是 ——我在那里拥有自己的账户。 还有很多其他实例、其他软件项目也实现了 OStatus协议——例如 Friendica


与今天的联邦宇宙相比,OStatus宇宙是微不足道的。 我没有具体的数字,但我的粗略估计是,在最好的时候,也只有大约100,000到200,000个活跃帐户(如果你有实际数字,请告诉我,我将很乐意更新这篇博客)。 我也没有 上用户数目的确切数字,但我粗略估计它有 10,000 到 20,000 个活跃帐户。

对,刚好也是整个社交网络的 1/10。

OStatus宇宙虽小但很活跃。在上面我们有讨论、threads、hashtags。 它的组织早于 Mastodon 软件项目实施组织的十年。 它有桌面应用程序——我仍然怀念 Choqok 的可用性! 经过一番唠叨后,我甚至说服波兰政府部门在那里设立正式代表。 据我所知,这是政府级机构在自由软件运行的去中心化社交网络上拥有官方账户的最早例子。

Identipocalypse (identi的末日)

然后有一天, 的管理员(也是 StatusNet 软件的原始创建者)Evan Prodromou 决定将其重新部署为一项新服务,即。 他们希望新软件更好、更精简。 由于 OStatus 有非常实际的限制,因此他们创建了一个新协议。

只有一个障碍:新协议与OStatus宇宙的其余部分不兼容。 它把这个社交网络撕碎了。

拥有 帐户的用户与所有其他 OStatus 实例失去了连接。 在其他实例上拥有帐户的人与 上的人失去了联系,其中一些人在OStatus宇宙中非常受欢迎(听起来很熟悉?..)。

事实证明,如果一个实例占据了整个社交网络的 1/10,那么它就会承载太多的社交联系。尽管的确存在其他实例,但突然间大量活跃用户消失了。一些群组瞬间就变得安安静静。 即使有人在不同的实例上有一个帐户,并且在其他实例上有联系人,很多熟悉的面孔也会消失。 于是此后不久我就停止使用它了。

从我的角度来看,这仅仅一个操作就使我们在推广去中心化社交媒体方面至少倒退了五年甚至十年。 的重新部署不仅在社交关系意义上,而且在协议和开发者社区意义上都破坏了 OStatus 宇宙。 正如 Pettter,一位 OStatus 资深人士所说:

我认为,这个巨大打击带来了巨大的影响,它不仅会通过切断社交联系产生影响,而且还会影响协议的开发,以及使开发人员一次又一次地重建联合社交网络的基本块的努力付诸东流。 也许这是他们重新聚在一起设计 AP^[译者注:ActivityPub] 的必要步骤,但我个人不这么认为。

当然,埃文完全有权利这样做。 这是他用自己的钱、按照自己的条件无偿经营的一项服务。 但这并不能改变它削弱了OStatus宇宙的事实。

我认为我们需要从这段历史中吸取教训。我们应该担心 的庞大规模。 我们应该为联邦宇宙上 Mastodon 软件明显的单一文化感到担忧。 我们还应该担心将整个联邦宇宙与“Mastodon”等同起来。


发展到像 这样的规模,需要付出实际成本和风险。 这些成本,尤其是那些风险,既针对该实例本身,也针对更广泛的 Fediverse。

Fediverse 上的审核很大程度上以实例为中心。 单个巨大的实例很难有效地管理,特别是如果它开放注册(就像 目前所做的那样)。 作为直接在官方移动应用程序中推广的旗舰实例,它吸引了大量新注册,其中包括不少不太好的用户

同时,这也使得其他实例的管理员和版主更难以做出有关 的审核决定。

如果不同实例的管理员认为 出于某种原因缺乏管理,他们是否应该对其保持沉默,甚至与之抗争(显然,有些人已经这样做了),从而拒绝其实例的成员访问许多在那里有账户的受欢迎的人吗? 或者他们应该冒着让自己的社区面临潜在有害行为的风险,保留这种访问权限吗? 的庞大规模使得其他实例的任何此类决定立即成为一件大事。 这是一种权力形式:“当然,如果您不喜欢我们的管理方式,您可以与我们断开连接,但如果您实例上的人无法访问整个联邦的 1/10,那将是一种耻辱!” 正如 GoToSocial 网站所说


请注意,我并不是说这种权力动态是有意识地、有目的地利用的! 但不可否认,它是存在的。

作为一个巨大的旗舰实例也意味着 更有可能成为恶意行为的目标。 例如,在过去几个月中,它多次受到 DDoS 攻击,并且好几次都因为这个而无法访问。 联邦系统的抗性依赖于消除大的故障点,而 现在已经是一个巨大的故障点了。

该实例的规模及其它成为了一个诱人的攻击目标也意味着需要做出某些艰难的选择。 例如,由于经常成为 DDoS 的目标,它现在由 Fastly 保护。 从隐私角度和互联网基础设施中心化的角度,这是一个问题。 这也是较小的实例完全避免的一个问题,因为它们只是更小,因此任何人都不会通过 DDoS 攻击来攻击目标。^[译者注:这方面我非常存疑,事实上小实例也经常受到随机的DDoS攻击。]


虽然联邦宇宙并不完全是单一文化,但它太接近单一文化了,令人感到不舒服。 Mastodon 非营利组织对整个联邦机构有着巨大的影响力。 这让使用社交网络的人、Mastodon 软件和其他实例软件项目的开发人员以及实例管理员感到紧张。

Mastodon 既不是联邦宇宙上唯一的实例软件项目^[译者注:即运营一个实例的软件。],也不是第一个。 例如,Friendica 已经存在了十五年了,它出生于 Mastodon 软件获得第一次 git 提交之前。 今天联邦宇宙中运行着的 Friendica 实例(例如在十年前曾是 OStatus 宇宙的一部分!

但是很多人在将整个联邦宇宙称为“Mastodon”,说的跟 Fediverse 上只存在 Mastodon 软件一样。 这导致人们要求 Mastodon 实现新的功能,有时这些功能已经由其他实例软件实现了。 Calckey 已经有引用(quote)功能了。 Friendica 也早就有线程对话和富文本


将 Mastodon 与整个联邦宇宙等同起来对于 Mastodon 软件开发人员来说也是不利的。 他们将面临被要求实现可能不完全适合 Mastodon 软件的功能的压力。 或者,他们有时候不得不和两群意见相左的用户打交道,一组要求某种功能,另一组坚持认为这种功能会带来太大的改变,必须拒绝。通过清楚地划出一条界限,并引导人们使用可能更适合他们的用例的其他实例软件,许多此类情况可能会更容易处理。

最后,Mastodon 是目前为止(按活跃用户和实例数量衡量)最流行的 ActivityPub 协议实现。 每个实现都有其自己的特性。 随着时间的推移和新功能的实现,Mastodon 的实现可能会进一步偏离严格的规范。 毕竟,这很诱人:如果你怎么来都是龙头老大,为什么要艰难的去实现标准协议呢?

如果这真的发生了,其他所有实现是否都必须跟随它,从而变得随波逐流,但没有实际的代理机构来把这些更改变成规范? 这是否会在 Mastodon 软件开发人员和其他实例软件项目的开发人员之间造成更多紧张关系?

“Mastodon 错过了 XX 功能”的最优解并不总是“Mastodon 应该实现 XX 功能”。 通常,最好使用更适合特定任务或社区的不同实例软件。 或者开发一个扩展协议,允许尽可能多的实例可靠地实现特别流行的功能。

但这只有在每个人都清楚 Mastodon 只是更大的社交网络:联邦宇宙 的一部分的情况下才有效。 而且,对实例软件、单个实例以及移动应用程序而言,我们现在本来就有很多选择。

遗憾的是,这似乎与 Eugen 最近的决定背道而驰:该决定将会导向自上而下(不是完全垂直整合,但倾向于垂直整合)的官方 Mastodon 移动应用程序模型,以推广他们最大的 实例。 在我看来,这很值得担忧。


我想澄清的是,我在这里并不是主张停止 Mastodon 的开发并且从不实现任何新功能。 我也同意注册流程需要比以前更好、更简化,我也同意需要实施大量 UI/UX 更改。 但所有这一切都可以并且理应以提高联邦宇宙弹性的方式进行,而不是破坏它。


我觉得 Mastodon 和联邦宇宙必须要更改的地方是:

  1. 现在关闭 上的注册 对于联邦宇宙的其他部分来说,这个实例已经太大了,所带来的风险也太大了。
  2. 使配置文件迁移更加容易,甚至可以跨不同实例类型 在 Mastodon 上,个人资料迁移目前仅移动关注者。 您关注的人、收藏夹、屏蔽和隐藏列表都可以手动移动。 帖子和列表无法移动——这对很多人来说都是一个很大的问题,因此他们就被和他们注册的第一个实例绑在了一起。 这并不是无法克服的——我已经迁移了两次个人账户,感觉也很不错。但这种阻力太大了。 值得庆幸的是,其他一些实例软件项目也正在努力允许后期迁移。 但这不会是一个快速而简单的解决方案,因为 ActivityPub 的设计使得在实例之间移动帖子变得非常困难。
  3. 默认情况下,官方应用程序应该从一小部分经过验证的实例中为新用户提供随机实例 至少其中一些推广实例不应由 Mastodon 非营利组织控制。 理想情况下,某些实例应该运行不同的实例软件,只要它使用兼容的客户端 API。



  1. 如果您在 上有帐户,请考虑迁移走 的确这是有点艰难的一大步,但也是你可以做的最直接有助于解决问题的事情。 几年前,我从 迁移过来,再也没回去过。
  2. 考虑使用基于不同软件项目的实例 越多的人迁移到使用 Mastodon 软件以外的其他实例软件的实例,我们的联邦宇宙就越平衡越有弹性。 例如,我听说很多人都觉得 Calckey 不错。 GoToSocial 看起来也很有趣。
  3. 请记住,联邦宇宙不仅仅是 Mastodon 语言很重要。 当谈论联邦宇宙时,称其为“Mastodon”只会让我上面提到的问题更难处理。
  4. 如果可以的话,支持 Mastodon 官方项目以外的项目 至此,Mastodon软件项目已经拥有众多的贡献者、稳定的开发团队以及足够雄厚的资金,可以安全地持续很长一段时间。这那太棒了!但是,对于其他与联邦宇宙相关的项目,包括独立的移动应用程序或实例软件,它们没有那么受到关注。为了拥有一个多元化、有弹性的联邦宇宙,我们需要确保这些项目也在各个方面得到支持,比如金钱上。


首先,联邦宇宙是一个比任何中心化的围墙花园更有弹性、更长期可行、更安全、更民主化的社交网络。 即使存在 Mastodon 单一文化题,它仍然不是(并且不可能 )由任何单一公司或个人拥有或控制。 我也觉得它比只是在 cosplay 去中心化的社交网络,比如 BlueSky 是一个更好、更安全的选择。

从某种意义上来说,OStatus宇宙可以说是联邦宇宙的早期版本; 如前所述,当时属于其中的一些实例仍在运行,并且今天已成为联邦宇宙的一部分。 换句话说,联邦宇宙已经存在了十五年了,尽管它受到了严重的伤害,但它仍然在独裁式灾难中“幸存下来”,同时见证了Google+的诞生和过早的消逝

我确实相信如今的联邦宇宙比 重新部署之前的OStatus宇宙更具弹性。 就用户群而言,联邦宇宙至少要多十倍,有数十个不同的实例软件项目和数以万计的活跃实例。还有一些认真的机构对其未来进行了投资。 我们不应该对我上面写的一切感到恐慌。 但我确实认为我们应该防患于未来。

我不会将恶意归因于 Eugen 最近的行为(比如让官方 Mastodon 应用程序将新人引向,也不会归因于 Evan 过去的行为(在 上重新部署 )。 我认为任何人都不应该这样做。 做好这件事很难,我们都在边走边学,并努力利用有限的时间和有限的资源去做到最好。

Evan 随后成为 ActivityPub(联邦宇宙运行的协议)的主要创建者之一。 Eugen 发起了 Mastodon 软件项目,我坚信这个项目让联邦宇宙蓬勃发展到了今天的样子。 我真的很欣赏他们的工作,并认识到如果没有人发表意见,在社交媒体空间中做任何事情都是不可能的。


更新:我^[原作者]犯了一个傻傻的错误, 由 Fastly 提供保护,我以为是 CloudFlare。修复了,感谢指出这个错误的人!

更新2:衷心感谢 Jorge Maldonado Ventura 提供了这篇博文的西班牙语翻译,并在 CC BY-SA 4.0 下发布。 谢谢!