<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
        <title><![CDATA[一叶斋]]></title>
        <description><![CDATA[一叶斋]]></description>
        <link>https://xieguanglei.github.io</link>
        <generator>RSS for Node</generator>
        <lastBuildDate>Thu, 21 Aug 2025 12:45:41 GMT</lastBuildDate>
        <atom:link href="https://xieguanglei.github.io/blog/feed.xml" rel="self" type="application/rss+xml"/>
        <pubDate>Thu, 21 Aug 2025 12:45:41 GMT</pubDate>
        <copyright><![CDATA[All rights reserved]]></copyright>
        <language><![CDATA[zh-cn]]></language>
        <item>
            <title><![CDATA[写在《小丑牌》全卡组金注之后]]></title>
            <description><![CDATA[<p>经过半年多断断续续的努力，终于为 <a href="https://en.wikipedia.org/wiki/Balatro">《小丑牌》</a> 画上句号。获得了「完美主义者+」称号：使用所有卡组在金注难度下获得胜利。</p>
<p><img style="width:440px; height:100px;" src="https://xieguanglei.github.io/blog/2025-08-21/xie-zai-xiao-chou-pai-quan-ka-zu-jin-zhu-zhi-hou/wan-mei-zhu-yi-zhe-jia.png" alt="|440x100"></p>
<p>一共有 15 个卡组，每个卡组，从白注到金注共 8 个难度，一共赢了 120 局。</p>
<p>赢下第一个白注花了一周，赢下第一个金注很激动，赢下最后一个金注如释重负。</p>
<p>游戏的随机性很强，即使是白注，也不一定能保证通关。紫注以上，基本需要反反复复地尝试才可能成功。</p>
<p>高难度下，筹码、倍率、倍数三者都要合格，如果有某一项拖后腿，就会比较难。</p>
<p>筹码小丑：</p>
<ul>
<li>小小丑：最强筹码小丑，配合未断选票简直强无敌，但因为是稀有，比较难遇到。</li>
<li>跑步选手：很实用，也比较稳定，如果有四指或者捷径，叠起来也很快。</li>
<li>城堡：还不错，蓝注以上弃牌少一次，成长稍慢。</li>
<li>特技演员：后期已经成长起来而且对牌型没要求可以拿一张，前期别贪，拿了影响成长。</li>
</ul>
<p>倍率小丑：</p>
<ul>
<li>备用裤子：最喜欢的小丑之一，非常稳定，因为叠倍率是前期最重要的任务，每次拿到它，感觉这盘就稳了。</li>
<li>侠盗：依赖与其他小丑（鸡蛋或礼品卡）的配合，但如何真能凑出来，就很无敌。</li>
<li>超新星、占卜师、搭乘巴士、仪式匕首：前三者成长太慢，后两者副作用比较大，但还算能用；搭乘巴士在废弃卡组里表现很好。</li>
<li>斐波那契：其实除了备用裤子，并没有特别好用的倍率小丑，这张虽然不是成长型，但数值比较高，也能用到后期。</li>
</ul>
<p>倍数小丑：</p>
<ul>
<li>全息影像：稳定又强的倍数小丑，只为金注通关，不玩无尽流的话，可以无脑拿。</li>
<li>卡文迪什：俗称大香蕉，小香蕉炸了才可能出。千分之一的概率炸，被我遇到过，竟然没有成就。</li>
<li>老千：打小牌型的话很不错，也经常拿。</li>
<li>积分卡：非常不稳定，但我最后一个金注就是靠它赢下的。</li>
<li>疯狂：黑注以下难度完全废柴，带着极大副作用的高收益，我竟然有一个金注是带着疯狂过的，我太疯狂了……</li>
<li>驾驶执照：成长曲线太陡峭，后期符合条件又刷到了可以拿。</li>
</ul>
<p>经济小丑：</p>
<ul>
<li>冲向月球：感受复利的力量，前期少花点，后期花不完。拿到这个可以考虑金牛和提靴带，同时解决筹码和倍率的问题，主打省心。</li>
<li>火箭：同样很强的经济小丑，前两轮必拿。</li>
<li>乌合之众：前期必拿，生成的小丑不合意，就算卖钱也是一笔不小的收益。</li>
</ul>
<p>功能小丑：</p>
<ul>
<li>蓝图：公认全游戏最强小丑，虽然是稀有，但我觉得比五个传奇小丑还有牌面。</li>
<li>未断选票：最喜欢的小丑之一，和几乎三分之一的小丑都能配合，极大提高牌型质量，几乎总能带来质变，这么强的卡，品质竟然只是普通。</li>
<li>骷髅先生：呃，也算一种过关方式吧。</li>
</ul>
<p>传奇小丑：</p>
<ul>
<li>卡尼奥：成长很高，但条件苛刻。</li>
<li>特立布莱：还不错，最好能与未断选票或者喜与悲配合。</li>
<li>约里克：又强又稳定的倍数小丑，对得起传奇的品质。</li>
<li>帕奇欧：强！这是谁想出来的效果，满分设计。</li>
<li>希科：平时没有半用用，但当你满心欢喜觉得即将过关，突然被 boss 击毙时，你会想念它的。</li>
</ul>
<p>最后罗列一下所有被我贴了金注标签的小丑：</p>
<p>奸诈小丑、仪式匕首、神秘之峰、积分卡、致胜之拳、斐波那契、混沌小丑、抽象小丑、搭乘巴士、窃贼、黑板、跑步选手、冰淇淋、星座、老千、疯狂、全息影响、九霄云外、邮件回扣、冲上月球、占卜师、金牛、闪示卡、备用裤子、城堡、微笑表情、骷髅先生、喜与悲、吟游诗人、证书、回溯、未断选票、箭头、蓝图、小小丑、三重奏、特技演员、头脑风暴、驾驶执照、提靴带、卡尼奥、特里布莱、帕奇欧。</p>
<p>值得一提的是，这款横扫了众多奖项，并获得了 TGA 2024 最佳独立游戏的游戏，是一个人设计和开发出来的，而他开发这款游戏的初衷，竟然只是为了「使简历上更好看一些」。真是程序员的楷模。</p>
]]></description>
            <link>https://xieguanglei.github.io/blog/2025-08-21/xie-zai-xiao-chou-pai-quan-ka-zu-jin-zhu-zhi-hou</link>
            <guid isPermaLink="true">https://xieguanglei.github.io/blog/2025-08-21/xie-zai-xiao-chou-pai-quan-ka-zu-jin-zhu-zhi-hou</guid>
            <pubDate>Thu, 21 Aug 2025 00:00:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[东京独自旅行游记]]></title>
            <description><![CDATA[<p>在两段工作经历之间的假期，我安排了一周的独自旅行。理论上，已婚有娃人士很难有独自旅行的机会。但是，此次从钉钉辞职，虽然早有预感，但真正决意也就在骤然之间 —— 说辞就要辞了。自己名下的二十多天假期须尽快休掉，晓辰最近又忙，一时无法脱身，这才有了我这次的独自旅行。人生啊，就是计划赶不上变化，我几乎没做攻略，只定了酒店，就登上了飞往东京的航班。</p>
<h2>第一天</h2>
<p>降落成田机场已是傍晚。我的住宿订在秋叶原，本想搭乘京成本线到上野，但我却误上了一列先走京成线，然后接入总武线的列车，往千叶方向去了。我在千叶换乘了中央线，到达秋叶原已经夜里十点多。</p>
<p>路上行人渐少，大多店铺也已经打烊。虽然下着绵绵的细雨，但我还是套上外套，下楼在附近的几个街区兴奋地溜达了一圈。</p>
<p>酒店就在秋叶原站上方，我的房间位于较高的楼层，视野很棒。我喜欢视野好的房间。</p>
<p><figure><img src="https://xieguanglei.github.io/blog/2025-08-10/dong-jing-du-zi-lv-xing-you-ji/fang-jian-shi-ye.jpg" alt="秋叶原，酒店房间视野，之后拍摄"><figurecaption>秋叶原，酒店房间视野，之后拍摄</figcaption></figure></p>
<h2>第二天</h2>
<h3>上野公园</h3>
<p>我在东京造访的第一个目的地是上野公园。这座公园历史悠久，又是赏樱圣地，名气很大，连鲁迅的《藤野先生》都是这样开头：「东京也无非是这样，上野的樱花烂漫的时节……」。公园里有几处古迹，有不忍池，但除此之外，这座公园似乎也没有什么大不同之处。孔子说「近则不恭」，看来也适用于人对物的情形。大概因为是早晨，游客不多，很是幽静，但是公园中心的长条喷泉池四周却很热闹，穿着宽大校服的小学生或列队，或席地而坐，颇像国内小学生春游的样子。</p>
<h3>东京国立博物馆</h3>
<p>喷水池广场北侧就是东京国立博物馆。我买一张票，走进去，偌大的一个庭院。主馆建筑门前杵着一株茂盛的银杏，弯曲粗壮的树干顶着张牙舞爪的树冠，在行道上投下斑驳的影子。树木上了年纪，和人上了年纪一样，总能现出一些不修边幅的气质来。忽然「哇——哇——」两声，两只乌鸦从树冠上飞起，藏到主馆建筑屋顶的缝隙里去了。东京的乌鸦很多，后来我在银座和涩谷都有听到乌鸦的叫声。</p>
<p><figure><img src="https://xieguanglei.github.io/blog/2025-08-10/dong-jing-du-zi-lv-xing-you-ji/dong-jing-guo-li-bo-wu-guan-yin-xing.jpg" alt="东京国立博物馆"><figurecaption>东京国立博物馆</figcaption></figure></p>
<p>博物馆的文物，印象最深刻的有两件：一件是遮光器土偶，日本绳纹时代陶偶，特点是巨大的宛如遮光器（即飞行员护目镜）的眼睛；我注意到这件文物，是因为《文明 7》游戏中就有一个名为「遮光器土偶」的领袖属性点 —— 原来是出自这里。另一件是日本武士铠甲中的一具，这套铠甲使用黑色的皮毛装饰，头盔上还配备了一个威武的金属面具，使我想起《只狼》中的苇名一心。</p>
<h3>北斋美术馆</h3>
<p>北斋美术馆位于墨田区，从两国站出来，步行一公里左右到达。美术馆拥有银灰色金属质感的外墙和现代风格几何形状的造型，坐落在一片安静的住宅区中。那幅最著名的<strong>神奈川冲浪里</strong>，虽然馆藏确有一幅真迹（浮世绘是近代的印刷品，所谓「真迹」的存世数量并不特别稀少），但不常驻展出，能见到的只是复制品。</p>
<p><figure><img src="https://xieguanglei.github.io/blog/2025-08-10/dong-jing-du-zi-lv-xing-you-ji/shen-nai-chuan-chong-lang-li.jpg" alt="神奈川冲浪里"><figurecaption>神奈川冲浪里</figcaption></figure></p>
<p>美术馆北侧是一片空地，用栏杆围起，空地上立着有秋千，球门，还有一个小型的旋转木马。这种小型社区公园在日本似乎很常见，设施虽不很新，但维护得极好，几乎没有坏的。去年关西旅行的时候，我也多次注意到这样的小公园，傍晚时分，有很多七八九岁的孩子聚集在公园里呼喊着玩耍，而且不太见到在旁看护的家长。</p>
<p>这种社区氛围使我想起，自己七八九岁时，夏日傍晚，在公房前的泥土空地上，和玩伴们打玻璃弹珠的日子。必须太阳西沉，天色已昏暗到难以看清地面上的弹珠时，才肯依依不舍地回家。如今国内，已经很难想象家长会放心让七八九岁的孩子在外独立玩耍了。</p>
<h3>友都八喜秋叶原</h3>
<p>晚上，好好逛了一下秋叶原最大的电器城友都八喜。顶楼的游戏机区域很是吸引我，可惜的是 Switch 2 国际版全部缺货；我很是想买些什么有趣的主机配件之类，但没有看到特别亮眼的，又不想空手而归，最后买了一个任天堂闹钟。</p>
<p>电器城这种业态，在国内已完全被电商取代了 —— 标准化产品的价格过于透明，消费者现场体验线上下单的问题，几乎是无解的。想不明白，日本的电器城为什么还能继续存活。</p>
<h2>第三天</h2>
<h3>浅草寺</h3>
<p>第三天早晨，出发去浅草寺。浅草寺周围主要是招待游客的商店街，我到得早，店铺大多还没开门，街道上空荡荡的。浅草寺占地不大，建筑也不多 —— 除了主殿和五重塔，就是影向堂（御朱印在里面），不似京都清水寺或杭州灵隐寺那般层层叠叠。洗过手，走进主殿。抽了一签，是吉签。签语是：「盘中黑白子，一着要先机。天龙降甘霖，洗出旧根基。」</p>
<p><figure><img src="https://xieguanglei.github.io/blog/2025-08-10/dong-jing-du-zi-lv-xing-you-ji/qian-cao-si-chou-qian.jpg" alt="浅草寺抽签"><figurecaption>浅草寺抽签</figcaption></figure></p>
<p>当我沿着仲见世商店街离开时，店铺才陆陆续续开始营业，有游客聚集在雷门拍照。我前往隅田川方向，晴空塔矗立在远处。</p>
<h3>银座</h3>
<p>银座大概是我到过的最豪华、最昂贵的商圈了。这里路网密集，一个街区只有一两栋建筑宽。我走马观花地穿过三越百货、银座松屋，打卡了百年老店和光百货。和光百货实在是高档，宽阔而一尘不染的走廊，老派而奢华的玻璃柜，衣着精美的工作人员，令我走进去的时候不由地屏住呼吸。</p>
<p><figure><img src="https://xieguanglei.github.io/blog/2025-08-10/dong-jing-du-zi-lv-xing-you-ji/he-guang-bai-huo.jpg" alt="和光百货钟楼"><figurecaption>和光百货钟楼</figcaption></figure></p>
<p>银座的街道给我的感受是「安静」，一种与其繁荣不匹配的安静：没有混杂着各种语调语气的高频人声，只能偶尔听到被刻意压低的窃窃私语。我能清晰地听到汽车驶过，引擎的突突声，轮胎摩擦地面的沙沙声，能听到皮鞋和高跟鞋踩在地面上的哒哒声，风吹动树叶的哗哗声，吹动广告旗帜的呼呼声，甚至能隐约听到远处，火车压过铁轨的咚咚声，车厢晃动的哐哐声。印象最深刻的是和光百货钟楼的整点报时：响亮的「当——当——」的钟声在沉默的人流上空盘旋，那一刻，这种安静的感受尤为强烈，我大概对日本社会中某些礼貌和压抑的特质也有了更生动的体会。</p>
<p>逛得最久的店铺，是银座的 MUJI 无印良品旗舰店。我很难掩饰对 MUJI 品牌的喜好，它的极简留白的「空容器」风格真是打在我的心头上。我大概有超过一半的衣物和各种日用品来自 MUJI，所以造访这家世界上最大的 MUJI，也算我到东京必做的事情之一。</p>
<p><figure><img src="https://xieguanglei.github.io/blog/2025-08-10/dong-jing-du-zi-lv-xing-you-ji/muji.jpg" alt="银座 MUJI 无印良品商店"><figurecaption>银座 MUJI 无印良品商店</figcaption></figure></p>
<p>这家 MUJI 的一楼是一整层食物，有新鲜面包、新鲜蔬菜（规格外野菜）、便当速食、零食饮料、甚至还有各种各样的调味料。其实我一直不理解国内 MUJI 门店怎么会有软糖和咖喱这种和家居主题完全不搭的商品，原来 MUJI 是有完整的食品板块的。二楼到五楼和国内门店差不多，只是商品的款式似乎更全一些。买了些小东西，退税柜台的收银员普通话比我还标准。六楼以上是 MUJI Hotel，餐厅对非住客也开放。我喝了一杯美式，然后离开了。</p>
<h3>东京铁塔</h3>
<p>东京铁塔，因为频繁出现在《奥特曼》中，频繁被怪兽摧毁，成为儿时我对东京的第一印象。傍晚从秋叶原出发，搭日比谷线，到神谷町出站，天已黑了大半。这一带的城市风貌令我感到熟悉：宽阔的马路，高档而略冷清的玻璃外立面高楼，很像国内的一些地方。快到永井坂路口时，先是看见前方很多游客正举着手机拍照，再快速走几步，红通通的铁塔就忽然从楼宇背后跃出，高耸着矗立在我的眼前中。</p>
<p><figure><img src="https://xieguanglei.github.io/blog/2025-08-10/dong-jing-du-zi-lv-xing-you-ji/dong-jing-tie-ta.jpg" alt="东京铁塔"><figurecaption>东京铁塔</figcaption></figure></p>
<p>游客大部分是南亚、东南亚人，也有少数白人，中国人很少。我跟着人流向前，很快就到了塔底的游客中心。我便去买票，排队乘电梯上塔。</p>
<p>塔上的夜景令人震撼：几乎无遮挡的视野中，全是密密麻麻，连成一片的灯火，似乎要从所有的缝隙里都生长出来，就像丛林一样 —— 令我想起在香港太平山顶观景台所见的灯火 —— 饱含着一种健壮无序的力量。</p>
<p>观景台内部就没有什么特别之处了。主观景台上的顶层观景台，排队时间太久，而且排进去后的流程也是无趣冗长，竟然还有免费（半强制）拍照，收费取照的桥段，令人大跌眼镜。十一点多，我才回到酒店。</p>
<h2>第四天</h2>
<h3>吉祥寺</h3>
<p>从地图上看，东京的周边地区似乎是由一个个小镇组成，每个小镇都有一个火车站，火车站辐射出相当规模的繁华商圈，再往外是安静的住宅区。吉祥寺就是这样一个小镇，去吉卜力美术馆需要在这里下车，然后步行过去。这里位于东京西部，与东京核心区有一段距离，从秋叶原搭火车花了一个小时才到。再往西，就到立川了，那里是 Falcom 法老控公司的所在地，也许可以朝圣一二，我想。但我还是在吉祥寺下了车。</p>
<p>吉卜力美术馆的门票有严格入场时间，我到早了，因此在吉祥寺闲逛了一两个小时。这个理论上只服务周边地区的小镇，它的繁荣程度大大出乎我意料。这里有密集的超市、特色餐厅、咖啡厅，各种各样文具店、电子产品商店，连锁书店，还有一整栋楼的优衣库，甚至还有专门卖烟斗和卖灯油的店 —— 真是不敢相信。本地人悠闲地在精致的街道和店铺里消磨时光，就像乐高街景里的人偶与建筑一样协调。可以说，与长三角地区普通地级市 —— 比如我的家乡南通，或扬州、无锡、嘉兴、金华这类城市 —— 市中心最好的商圈相比，也完全不输。可是，这只是东京市郊的一个普通小镇，这么说，东京可真是踏踏实实的繁荣啊。</p>
<h3>吉卜力美术馆</h3>
<p>从吉祥寺前往吉卜力美术馆，需要穿过井之头恩赐公园。高大的乔木，茂密的草丛，没有铺装的泥土小径，使我联想起《龙猫》中姐妹俩居住的乡下房屋。美术馆内，有以影片中形象为素材，展示视觉暂留原理的设施；有真实的早期手稿、原型图等「文物」；还有一个龙猫主题的儿童乐园 —— 真的可以进入到龙猫巴士内部；门票还包含一场小剧场中的动画短片（似乎不会在外部渠道放映，而且会定期轮换）。美术馆唯一可以拍照的地方是屋顶的天空之城机器人塑像。</p>
<p><figure><img src="https://xieguanglei.github.io/blog/2025-08-10/dong-jing-du-zi-lv-xing-you-ji/tian-kong-zhi-cheng-ji-qi-ren.jpg" alt="吉卜力美术馆"><figurecaption>吉卜力美术馆</figcaption></figure></p>
<h3>涩谷</h3>
<p>搭乘京王井之头线，从吉祥寺到涩谷。我匆匆打卡了世界上人流量最大的涩谷十字路口。这种路口的信号灯是「行人全向通行」的。当行人绿灯亮起，数百人甚至上千人同时从四个街角像鱼群一样涌出，形成若干股巨大的人流，四个方的车流全部停下，你会产生有一种身在舞台的感觉。</p>
<p>因为没有预定，我没能上得去涩谷最有名的天空观景台。简单逛了逛，我就开始在庞大的由天桥、通道组成的迷宫中，寻找前往代官山的路。经过涩谷溪流大厦时，我有看到 Google 的指示牌 ——查询后发现，这里正是 Google 东京的两个主要办公地点之一。</p>
<h3>代官山</h3>
<p>沿着涩谷溪流大厦楼下的一条无名小路，向东南方向步行一段，然后经猿乐桥跨过铁路。街道一下子安静了，这就到了代官山区域。再向前走两公里，就到了代官山森林之门 —— 这个建筑和上海天安千树有点相似。</p>
<p><figure><img src="https://xieguanglei.github.io/blog/2025-08-10/dong-jing-du-zi-lv-xing-you-ji/seng-lin-zhi-men.jpg" alt="代官山森林之门"><figurecaption>代官山森林之门</figcaption></figure></p>
<p>在森林之门这个路口右转，折进一条文艺的小路，很快就来到了代官山茑屋书店。这家茑屋书店可比杭州天目里的那一家茑屋书店大了不少，由三栋相互连接的二层建筑组成，其中一栋建筑二层是酒吧，几乎满座了。</p>
<p>这家茑屋书店有挺大一片区域是关于汽车主题的，里面有各个汽车品牌的相关的书籍、画册、海报、模型等等，挺有意思的。另外还有一块 SHARE LOUNGE 区域 —— 也就是收费休息/办公区，即使不点任何饮食，进入休息区也要收费，而且是按时长计费。比起更为常见的把餐食和空间捆绑售卖的商业模式，以及随之而来的「消费落座」或者「最低消费」的别扭规则，这种业态倒是更对我的口味。</p>
<p>我对书店很着迷，每到一个新的城市，我几乎总要寻找当地的有名的书店游览一番 —— 即使在日本，满屋子都是日文书，我还是情不自禁。这大概和我的童年经历有关：在从小镇刚搬入市区的头两年，家里没有空调，夏天特别热。为了避暑，有两个暑假，我几乎每一天都泡在南通书城里看书，早出晚归，中午就在摊子上吃一个油饼。</p>
<p>满满四层楼的书，给当时的我带来了极大的震撼 —— 任何能想到的问题，似乎都能在这里找到答案，我产生了「和世界连接上」的感觉。那两个暑假泡在书城的日子，其实也是我第一次脱离父母老师管束，自由安排活动 —— 因此直到现在，每次我步入书店，都本能地涌起一股轻松的情绪，令我上瘾。</p>
<h3>新宿</h3>
<p>在代官山站搭乘副都心线，一会儿就到新宿三丁目。出站后，我把步行导航目的地设置为歌舞伎町一番街：怀着纯粹的好奇心，我有意探索这片东京最著名的「红灯区」。</p>
<p>新宿的街道没有明显的区域风格，也没有令人瞩目的标志性地点 —— 到处是商店、霓虹灯、人流 —— 和其他地方一样。新宿给我的感觉是巨大、沉闷、没有尽头。时不时地，脚底隐隐传来火车行驶的震动时，我觉得自己在一颗孜孜不倦地跳动着的心脏里 —— 就是这个感觉，巨兽的心脏。</p>
<p>在跨越新宿站的桥梁上，我看到有大概是乌克兰人/裔在组织谴责俄罗斯的示威活动，举着蓝黄相间的旗帜。街头政治，国内几乎不可能见到。最近日本好像正在进行的选举，有一天在秋叶原酒店楼下也看到国民民主党的街头宣传。</p>
<p>歌舞伎町一番街，其实很短，大概只有两三百米长，街道两边一楼主要是餐厅、酒吧，二楼以上，根据霓虹灯招牌判断，应该有相当比例是软色情营业场所 —— 窄窄的楼道通往地面，只在一楼开一个小门，门前摆着包括价格明细的广告牌，还有女仆或其他 cosplay 装扮的年轻女性在门前招揽顾客。之前和晓辰一起在泰国旅行时，我们也探索过芭堤雅的红灯区，歌舞伎町与之相比，更加整洁清冷 —— 还有正常营业的餐厅，也没有人主动搭讪。</p>
<p><figure><img src="https://xieguanglei.github.io/blog/2025-08-10/dong-jing-du-zi-lv-xing-you-ji/ge-wu-ji-ting-yi-fan-jie.jpg" alt="歌舞伎町一番街"><figurecaption>歌舞伎町一番街</figcaption></figure></p>
<p>出卖身体换取金钱，真是一件颇为可悲的事情 —— 但是，联想起国内普遍超长的工作时间与缺乏尊严的职场环境，我又有什么资格去评判别人呢。</p>
<h2>第五天</h2>
<p>前一天晚上，我就一直在考虑接下来是去台场还是镰仓，毕竟来东京旅行，怎么着也得见一见东京的海才行。台场可以去丰州市场看三文鱼拍卖，还有富士电视台大楼，而且台场比较近，下午回来还能去东京大学逛一逛；镰仓比较远，但可以打卡高德院大佛和镰仓高校前。后来我发现，这天是周一，丰州市场没有拍卖，于是就就选择去了镰仓。</p>
<h3>高德院大佛</h3>
<p>去镰仓的火车，坐了一个半小时。从镰仓站出来，我沿着由比滨大通步行，去往长谷和高德院方向。其实直接搭乘江之电更快，但我喜欢步行。我在由比滨大通上发现了一栋极具年代感的建筑，这里曾是镰仓银行由比滨出张所，现在是一个酒吧。</p>
<p><figure><img src="https://xieguanglei.github.io/blog/2025-08-10/dong-jing-du-zi-lv-xing-you-ji/the-bank.jpg" alt="镰仓银行由比滨出张所旧址"><figurecaption>镰仓银行由比滨出张所旧址</figcaption></figure></p>
<p>走了两三公里，到长谷大道路口，右拐上坡，游客开始多起来了 —— 不仅有国际游客，也有不少日本国内游客（甚至很多应该就来自东京）。恰巧，我和三四个中国游客擦肩而过，他们正在说南通话 —— 南通话是如此小众又难懂，以至于我和晓辰已经习惯于把南通话当做「公共场合的加密语言」来使用。当我听到这几名游客在用南通话点评一家古旧店铺内的商品时，我甚至感受到一种窥听的紧张。</p>
<p>整体上，镰仓的街道给我的感觉有点像冲绳，有一种「历史的厚重」和「度假的轻松」糅合在一起的味道：上了年龄的木质老屋和稍新的现代房屋交错在一起，沿街的建筑大多布置成餐厅或各种商店，店铺门口的冲浪用品和远处若隐若现的大海提醒着你，这里是海边。</p>
<p>高德院，其实不算太大，检票进去直接就看到了露天的大佛：青绿色的金属材质，大概三四层楼高。据说之前可以进到大佛内部，但是我没有看到。高德院大佛最早不是露天的，而是在大殿之中，但是大殿毁于海啸。后来镰仓幕府衰落，日本政治中心重新移回京都，镰仓当地也不再有资源来重建这座大殿，久而久之，露天的大佛就成了新的地标景观。《文明 6》游戏里的世界奇观高德院，建成时大佛就是露天的，并不严谨。</p>
<p><figure><img src="https://xieguanglei.github.io/blog/2025-08-10/dong-jing-du-zi-lv-xing-you-ji/lian-cang-da-fo.jpg" alt="镰仓高德院大佛"><figurecaption>镰仓高德院大佛</figcaption></figure></p>
<p>日本的景点，即使如镰仓大佛这种，极具分量的历史遗迹，其配套设施也比较简单，不占用太多土地，能够和周边环境融洽共处，就像放在纸盒中的珍珠一样。而国内的很多历史遗迹，修建大量配套设施和商铺，一道门套着一道门，甚至还需要用上接驳车，高下是不言而喻的。</p>
<h3>镰仓高校前</h3>
<p>镰仓高校前的丁字道口，因出现在《灌篮高手》片头中而成为著名的动漫圣地，以至国内很多同时拥有有铁路轨道和海岸风光的地方，在社交媒体上都被称为「小镰仓」。</p>
<p>从高德院出来，原路下坡，走到长谷电车站，搭江之电前往镰仓高校前。电车上很热闹，中国人很多，我旁边的两个男人大声谈论着上海和东京的房价，还有举着小红旗的导游扯着嗓子提醒团里的游客下车地点。我是一个不喜欢喧嚣的人，但在当时，作为一个独自在异国旅行了快一周的游客，我感到切实的亲切和轻松。列车冲到开阔地带，湛蓝的太平洋一下子映入眼帘，整个车厢的中国人一起发出了「哇哦」了呼喊，然后继续沉入喧嚣之中。</p>
<p>很快电车就到站了。镰仓高校前站特别小，简单说就是一个长条形的亭子。列车离开后，我在站里找了一个座位坐下来，直接就面朝大海了。我吹了一会儿海风，在隐约的海浪声中发了一会儿呆。</p>
<p><figure><img src="https://xieguanglei.github.io/blog/2025-08-10/dong-jing-du-zi-lv-xing-you-ji/lian-cang-gao-xiao-qian.jpg" alt="镰仓高校前站"><figurecaption>镰仓高校前站</figcaption></figure></p>
<p>然后出站，去网红道口看了一眼，很多人在排队拍照。电车很久才会有一趟，恰好能与电车合影的，都是幸运儿。</p>
<p>接下来，我沿着高校前路口上坡，本想随意探索一番，结果走了好长一段，大约三四公里路程，最后到达了腰越站。这一路都是安静的住宅，几乎没有商店，甚至没见到什么人。在腰越站穿过轨道后，又回到了海边。沿着 134 过道向西，又走了一两公里路，就到了须花通。</p>
<p>这是一条通向江之岛的小道，路边有不错的西餐厅、售卖奶油布丁的小店、海洋风的纪念品商店、颇具设计感的首饰和服装店，和刚刚走过那么远的「荒芜之地」相比，实在是太可爱了。又累又饿，还没吃午饭的我在这里吃了一个虾仁奶油三明治，喝了一大杯热拿铁。</p>
<p>太阳有点斜了，我决定返程。从湘南江之岛搭乘湘南线到大船，再换火车到东京。湘南线是吊挂式的轨道，车体悬于轨道下方，这是我第一次乘坐吊挂式列车。</p>
<h3>秋叶原</h3>
<p>晚上，重新逛了逛秋叶原。第一天重点在友都八喜这种电器卖场，其实秋叶原还有海量的中古品（二手商品）商店：中古的动漫周边店，中古手机和游戏主机店、中古相机、镜头和拍立得店，中古书籍、漫画和音像制品店等等。可以感受到，日本的中古商品零售产业相当发达，只要是批量生产的标准化产品，都能按照年份，成色，稀有度等因素，匹配出一个合理的价格 —— 就连银座 MUJI 都有一小块售卖中古家具的区域。</p>
<h2>第六天</h2>
<p>返程有一个小插曲：原计划乘坐的航班，在执飞前序航班（上海-东京）执飞过程中机械故障，备降了大阪，所以我的航班取消了。庆幸之余，改签了下午稍晚的另一个航班。</p>
<p>在秋叶原站搭山手线到上野站，换京成本线前往成田机场，虽然还是慢车，但总算没有绕到千叶去。我惊奇地发现，在华语梗圈小有名气的「我孙子市」，就在这条线上。</p>
<p>傍晚时分，降落上海浦东，搭市域铁到虹桥，搭高铁到杭州，搭 19 号线回家。公共交通是如此完备便捷，普通人的活动半径之大，在一百年前的人们看来，根本难以想象。这是时代给普通人的福利，一定要珍惜啊。</p>
]]></description>
            <link>https://xieguanglei.github.io/blog/2025-08-10/dong-jing-du-zi-lv-xing-you-ji</link>
            <guid isPermaLink="true">https://xieguanglei.github.io/blog/2025-08-10/dong-jing-du-zi-lv-xing-you-ji</guid>
            <pubDate>Sun, 10 Aug 2025 00:00:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[编程随笔]]></title>
            <description><![CDATA[<h2>命名</h2>
<ul>
<li><p>命名，无论如何强调都不过分。</p>
</li>
<li><p>命名的本质是概括，换言之，代码必须形成概念。</p>
</li>
<li><p>命名做不好，根本原因往往是，代码背后的概念本就含糊不清。不深究本因，一味追求「命名规范」，其实是颠倒了因果关系，无异于水中捞月。</p>
</li>
<li><p>代码复用，绝不是简单把重复代码抽离为公共模块。如果代码没有形成完整概念，如果你给不了它一个简短的名字，那么即使有再多地方出现这段重复的代码，也不要复用。</p>
</li>
<li><p>为使一个概念从残缺变完整，不一定要增加什么，常常也可以扔掉什么。</p>
</li>
<li><p>如果某件东西，把其任意一个部件移除，它就彻底坏了：我愿称之为「精妙」。</p>
</li>
</ul>
<h2>分治法</h2>
<ul>
<li><p>分治法：把一个问题拆解成为数不太多的若干个独立子问题。</p>
</li>
<li><p>面对复杂问题，自顶向下不断分治，将问题拆解为树，使每个叶子节点都足够简单。这是设计复杂系统最重要的方法论。</p>
</li>
<li><p>分治的结果，不应是子问题的简单加和（把一箱苹果拆分为每个苹果），而应是子问题的有机组合（把一箱苹果拆分为一些苹果和一个纸箱，苹果在纸箱内）。</p>
</li>
<li><p>一个问题的拆解，必须由架构师一人独立完成，因为概念只能源于一人的脑海。</p>
</li>
</ul>
<h2>架构师</h2>
<ul>
<li><p>架构师的职责：划分边界，规定依赖。一个项目中，谁能在这两件事上做决定，使他人遵照你的方案，谁就是架构师；如果没人能在这两件事上做决定，那就没有架构师。</p>
</li>
<li><p>为什么架构师追求形式正确？因为分治的结果（至少在未实现前）是形式。</p>
</li>
<li><p>为什么架构师关注接口甚于实现？因为正确的接口能够避免错误的实现导致的 bug 在各个子系统间传染。</p>
</li>
<li><p>举一个追求形式 / 接口正确的极端案例：考虑「根据 id 查询 item 详情」的接口，返回的详情数据中，是否应该包含 id 本身？我认为，如果不包含，接口就从形式上就杜绝了出错的可能。调用方原本就知道 id，试问如果详情数据中包含的 id 与调用时传入的不同，调用方应如何自处？如果采信了错误的 id，责任究竟在调用方还是接口提供方呢？</p>
</li>
</ul>
<h2>技术债</h2>
<ul>
<li><p>我认为「技术债」这个比喻并不恰当：首先，你可以永远不偿还技术债，因为技术债不影响软件运行，背负沉重技术债的软件甚至可以运行得非常稳定；其次，你似乎永远可以借到技术债：不管现状是多么的千疮百孔，你似乎总能找到「临时方案」。</p>
</li>
<li><p>如果把软件开发活动，视为使用「开发资源」这种货币来购买「软件需求实现」这种商品，那么我更愿意把「技术债」称为「技术贷」：一种特殊的消费贷。如果你看上某件商品，但囊中羞涩，可以选择贷款，自己只需支付 5% 的首付即可买下。这笔贷款没有任何偿还期限，但贷款存续期间，其他支付行为会增加 5% 手续费。结束贷款的方式有两种：重新按原价购买商品，或再次支付 5% 的手续费来扔掉商品（需注意，结束贷款支付的费用，也会受存续贷款的影响）。同时，银行承诺永远提供这样的贷款合约。</p>
</li>
<li><p>举债似乎是完全无成本的，但是举贷必须有 5% 的首付（再巧妙的临时方案都有开发成本）。最终压垮软件系统的，并不是无债可借，而是在大量存续贷款手续费的加持下，我们连最廉价商品的 5% 首付都无力承担。</p>
</li>
<li><p>如何评价软件的腐败程度：开发新功能时，多少精力投入在功能本身的开发上，又有多少精力投入在防止把原有功能弄坏上。</p>
</li>
</ul>
<h2>诊断</h2>
<ul>
<li><p>如何把不稳定复现的问题转化为稳定复现的问题？把复现的过程自动化，然后重复运行足够多次。</p>
</li>
<li><p>软件性能问题就像发烧。发烧不是一种病，而是一种症状，你不应指望「退烧药（性能最佳实践）」能真正治好什么大病。</p>
</li>
<li><p>「再多的药也比不上一次正确的诊断」——《霍乱时期的爱情》。</p>
</li>
<li><p>问程序员「这个 bug 什么时候可以修好」，可类比于问医生「这个病什么时候可以看好」。</p>
</li>
</ul>
<h2>程序员</h2>
<ul>
<li><p>程序员喜欢抬杠，因为赞同意见不会实质地推动讨论的进展，可以不说（只会在心里默默赞同）。</p>
</li>
<li><p>我不喜欢「打磨」的说法，它暗示了这件事是容易的、表面的、可替代的。软件开发工作中不存在容易的部分，因为容易的部分已经被优秀的工程师自动化了。</p>
</li>
<li><p>当队友说「这个盒子真好看，我要留着装东西」时，我听到的是：「房子太大了，这块空间扔了吧」。</p>
</li>
<li><p>软件开发工作中的沟通成本比任何外行估计的都高。这就是为什么单枪匹马的程序员，与传统的开发团队相比，有着巨大的成本优势。个人英雄主义在软件开发行业并未过时。</p>
</li>
</ul>
]]></description>
            <link>https://xieguanglei.github.io/blog/2023-05-03/bian-cheng-sui-bi</link>
            <guid isPermaLink="true">https://xieguanglei.github.io/blog/2023-05-03/bian-cheng-sui-bi</guid>
            <pubDate>Wed, 03 May 2023 00:00:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[集合视角下的 TypeScript 泛型开发实践]]></title>
            <description><![CDATA[<p>前段时间我钻研了 <a href="https://www.freecodecamp.org/news/typescript-curry-ramda-types-f747e99744ab/">《How to master advanced TypeScript patterns》</a> 这篇文章，这是 <a href="https://github.com/millsp/ts-toolbelt">ts-toolbelt</a> 的作者 <a href="https://github.com/millsp">Pierre-Antoine Mills</a> 的一篇早期博客文章。文章提出了一个很有挑战的题目：<strong>TS 如何为柯里化函数编写类型支持？</strong></p>
<p>我参考原文进行了一些实践，然后似乎领悟到一些关于 TS 泛型的<strong>更接近实质</strong>的知识 —— 从集合的视角。基于这些知识，我发现原文中的大部分泛型都有更严密的写法。我认为这次实践和思考的过程值得记录下来，因此有了本文。</p>
<p>和原文一样，本文不展开讨论柯里化或函数式编程的问题，柯里化只是用以讨论 TS 泛型开发的<strong>素材</strong>。本文的线索是我的这份完整实现中，每一个泛型的作用，但这不是重点，重点是背后的领悟 —— 在文章前半部分，我常常会围绕一条简单的泛型讨论较长篇幅，请不要直接跳过。</p>
<h3>命题</h3>
<p>柯里化是函数式编程领域的一个重要概念，它表示这样的过程：把一个多参数的函数转化成「一次接收一个参数」的函数，比如把 <code>f(a,b,c)</code> 转化为 <code>f(a)(b)(c)</code>。举个更详细的例子：</p>
<pre><code class="hljs language-typescript"><span class="hljs-comment">// toCurry 函数为待柯里化的普通函数</span>
<span class="hljs-keyword">declare</span> <span class="hljs-keyword">const</span> <span class="hljs-attr">toCurry</span>: <span class="hljs-function">(<span class="hljs-params"><span class="hljs-attr">a1</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">a2</span>: <span class="hljs-number">2</span>, <span class="hljs-attr">a3</span>: <span class="hljs-number">3</span>, <span class="hljs-attr">a4</span>: <span class="hljs-number">4</span></span>) =&gt;</span> <span class="hljs-number">0</span>;

<span class="hljs-comment">// curry 是柯里化转换函数，接收普通函数 toCurry，返回转换后的函数（先用 unknown 类型表示）</span>
<span class="hljs-keyword">declare</span> <span class="hljs-keyword">const</span> <span class="hljs-attr">curry</span>: <span class="hljs-function">(<span class="hljs-params"><span class="hljs-attr">toCurry</span>: <span class="hljs-title class_">Function</span></span>) =&gt;</span> <span class="hljs-built_in">unknown</span>;

<span class="hljs-comment">// curried 是柯里化转换后的函数，调用者按次序每次传入一个参数，所有参数传入后，得到最终的返回值</span>
<span class="hljs-keyword">const</span> curried = <span class="hljs-title function_">curry</span>(toCurry);
<span class="hljs-title function_">curried</span>(<span class="hljs-number">1</span>)(<span class="hljs-number">2</span>)(<span class="hljs-number">3</span>)(<span class="hljs-number">4</span>);            <span class="hljs-comment">// =&gt; 0</span>
</code></pre><p>最简单的柯里化，一次仅能接收一个参数。</p>
<p>高级的柯里化，一次可以接收不定项个参数：</p>
<pre><code class="hljs language-typescript"><span class="hljs-comment">// 调用 curried 一次传入多个参数</span>
<span class="hljs-title function_">curried</span>(<span class="hljs-number">1</span>)(<span class="hljs-number">2</span>,<span class="hljs-number">3</span>)(<span class="hljs-number">4</span>);             <span class="hljs-comment">// =&gt; 0</span>
</code></pre><p>甚至还可以支持剩余参数和占位符：</p>
<pre><code class="hljs language-typescript"><span class="hljs-comment">// toCurry 中包含剩余参数</span>
<span class="hljs-keyword">declare</span> <span class="hljs-keyword">const</span> <span class="hljs-attr">toCurry</span>: <span class="hljs-function">(<span class="hljs-params"><span class="hljs-attr">a1</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">a2</span>: <span class="hljs-number">2</span>, <span class="hljs-attr">a3</span>: <span class="hljs-number">3</span>, <span class="hljs-attr">a4</span>: <span class="hljs-number">4</span>, ...<span class="hljs-attr">args</span>: <span class="hljs-number">5</span>[]</span>) =&gt;</span> <span class="hljs-number">0</span>;
<span class="hljs-keyword">const</span> curried = <span class="hljs-title function_">curry</span>(toCurry);

<span class="hljs-comment">// 调用 curried 时也可以传入剩余参数</span>
<span class="hljs-title function_">curried</span>(<span class="hljs-number">1</span>)(<span class="hljs-number">2</span>, <span class="hljs-number">3</span>)(<span class="hljs-number">4</span>, <span class="hljs-number">5</span>, <span class="hljs-number">5</span>, <span class="hljs-number">5</span>, <span class="hljs-number">5</span>);    <span class="hljs-comment">// =&gt; 0</span>

<span class="hljs-comment">// 调用 curried 时通过传入占位符把参数 2 移到了 3 之后</span>
<span class="hljs-title function_">curried</span>(<span class="hljs-number">1</span>)(__, <span class="hljs-number">3</span>)(<span class="hljs-number">2</span>, <span class="hljs-number">4</span>, <span class="hljs-number">5</span>);     <span class="hljs-comment">// =&gt; 0</span>
</code></pre><p>柯里化转换函数 <code>curry</code> 是柯里化的核心。转换函数接收一个普通函数 <code>toCurry</code> —— 暂时用 <code>Function</code> 类型来表示；并返回柯里化转换后的函数 <code>curried</code>（后面也称<strong>柯里化的函数</strong>或<strong>柯里化函数</strong>）—— 暂时用 <code>unknown</code> 类型来表示。如何写出它的合法的类型表达，就是这篇文章的主线。</p>
<h3>CurriedV1：最简单的柯里化</h3>
<p>最简单的，一次只接收一个参数的柯里化，我的实现如下：</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Length</span>&lt;T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[]&gt; = T[<span class="hljs-string">&#x27;length&#x27;</span>];

<span class="hljs-keyword">type</span> <span class="hljs-title class_">Head</span>&lt;T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[]&gt; = T <span class="hljs-keyword">extends</span> [] ? <span class="hljs-built_in">never</span> : T[<span class="hljs-number">0</span>];

<span class="hljs-keyword">type</span> <span class="hljs-title class_">Tail</span>&lt;T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[]&gt; = T <span class="hljs-keyword">extends</span> [] ? 
  <span class="hljs-built_in">never</span> : T <span class="hljs-keyword">extends</span> [<span class="hljs-built_in">unknown</span>, ...infer R] ? R : T;

<span class="hljs-keyword">type</span> <span class="hljs-title class_">CurriedV1</span>&lt;P <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], R&gt; = P <span class="hljs-keyword">extends</span> [] ? 
  R : <span class="hljs-function">(<span class="hljs-params"><span class="hljs-attr">arg</span>: <span class="hljs-title class_">Head</span>&lt;P&gt;</span>) =&gt;</span> <span class="hljs-title class_">CurriedV1</span>&lt;<span class="hljs-title class_">Tail</span>&lt;P&gt;, R&gt;;

<span class="hljs-keyword">type</span> <span class="hljs-title class_">Curry</span> = &lt;P <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], R&gt;<span class="hljs-function">(<span class="hljs-params"><span class="hljs-attr">fn</span>: (...args: P) =&gt; R</span>) 
  =&gt;</span> <span class="hljs-title class_">CurriedV1</span>&lt;P, R&gt;;

<span class="hljs-keyword">declare</span> <span class="hljs-keyword">const</span> <span class="hljs-attr">curry</span>: <span class="hljs-title class_">Curry</span>;
<span class="hljs-keyword">declare</span> <span class="hljs-keyword">const</span> <span class="hljs-attr">toCurry</span>: <span class="hljs-function">(<span class="hljs-params"><span class="hljs-attr">a1</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">a2</span>: <span class="hljs-number">2</span>, <span class="hljs-attr">a3</span>: <span class="hljs-number">3</span>, <span class="hljs-attr">a4</span>: <span class="hljs-number">4</span></span>) =&gt;</span> <span class="hljs-number">0</span>;

<span class="hljs-keyword">const</span> curried = <span class="hljs-title function_">curry</span>(toCurry);
<span class="hljs-title function_">curried</span>(<span class="hljs-number">1</span>)(<span class="hljs-number">2</span>)(<span class="hljs-number">3</span>)(<span class="hljs-number">4</span>); <span class="hljs-comment">// =&gt; 0</span>
</code></pre><h3>泛型 <code>Length</code></h3>
<p>第一条泛型 <code>Length</code> 返回元组的长度。</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Length</span>&lt;T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[]&gt; = T[<span class="hljs-string">&#x27;length&#x27;</span>];
</code></pre><p>首先需要领悟的是，<strong>类型是对象的集合</strong>。比如，类型 <code>number</code> 是所有数字的集合，类型 <code>1</code> 表示由数值 <code>1</code> 组成的单一元素集合，类型 <code>string[]</code> 是所有「每一项都是字符串的数组」的集合。</p>
<p><strong>泛型</strong>，从形式上看，是类型的函数（把一种类型转化为另一种类型）；从集合的角度看，是<strong>集合的映射</strong>（把一个集合变为另一个集合）。</p>
<p>集合的映射，必须基于作用于<strong>集合内元素</strong>的<strong>规则</strong>。假设有集合 A 和 B，只有当 A 中的任意元素，按照<strong>某种规则</strong>，可以转化为 B 中的一个元素，我们才能说 A 和 B 之间存在映射关系。</p>
<blockquote>
<p>既然映射只能从<strong>一个</strong>集合映射到另一个集合，如何理解多个泛型参数的情况？回答：把多个泛型参数看作一个元组类型。</p>
</blockquote>
<p>以 <code>Length</code> 为例：</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Length</span>_Test1 = <span class="hljs-title class_">Length</span>&lt;[<span class="hljs-built_in">unknown</span>]&gt;;          <span class="hljs-comment">// =&gt; 1</span>
<span class="hljs-keyword">type</span> <span class="hljs-title class_">Length</span>_Test2 = <span class="hljs-title class_">Length</span>&lt;<span class="hljs-built_in">unknown</span>[]&gt;;          <span class="hljs-comment">// =&gt; number</span>
</code></pre><p>将类型 <code>[unknown]</code> 传递给 <code>Length</code> 得到类型 <code>1</code>，描述了这样的事实：属于类型 <code>[unknown]</code> 的无数个元素中任意一个，不管是 <code>[1]</code> 还是 <code>[&#39;foo&#39;]</code> 还是 <code>[{foo: true}]</code>，<strong>对它求取长度</strong>，得到的结果都是 <code>1</code>。这些元素经过 <code>Length</code> 这条规则，都被转化成了数值 <code>1</code> 这个元素；换言之，类型 <code>[unknown]</code> 所代表的集合通过 <code>Length</code> 这条规则映射为只包含一个元素（也就是 <code>1</code> 这个数值）的单元素集合，这个集合对应的类型就是类型 <code>1</code>。</p>
<p>将类型 <code>unknown[]</code> 传递给 <code>Length</code> 得到类型 <code>number</code>，这是因为：属于类型 <code>unknown[]</code> 的无数个元素中的任意一个，不管是 <code>[]</code> 还是 <code>[1]</code> 还是 <code>[&#39;foo&#39;, true]</code>，<strong>对它求取长度</strong>，得到的结果 <code>0</code>，<code>1</code> 或 <code>2</code> 等等，都是 <code>number</code> 类型。注意，<code>Length</code> 不能保证经过规则转换后的值<strong>占满</strong>映射得到的集合：因为没有什么数组的长度是 <code>0.5</code> 或 <code>-1</code>。所以，泛型运算 <code>Length&lt;unknown[]&gt;</code> 的结果 <code>number</code> 其实是真实世界中映射得到的结果集的<strong>超集</strong>。</p>
<p>泛型返回「真实结果集」的超集，时常比我们预期的集合要宽泛，这是不可避免的。从集合的角度看，编写泛型的目的，就是提供「真实结果集」的<strong>可以用类型规则描述</strong>的，同时<strong>尽可能小</strong>的超集。理解这一点很重要。</p>
<blockquote>
<p>如果 JS 支持无符号整数类型，<code>Length&lt;unknown[]&gt;</code>，似乎就可以得到完美的结果集，但这其实只是巧合。更多场合是无法得到完美结果集的：比如 <code>Length&lt;[unknown, ...unknown[]]&gt;</code> 需要返回「由大于 1 的整数」构成的集合。</p>
</blockquote>
<h3>JavaScript：先验性的知识</h3>
<p>TS 是如何知道 <code>Length&lt;unknown[]&gt;</code> 的结果是 <code>number</code> 的呢？在 <code>Length&lt;unknown[]&gt;</code> 和 <code>number</code> 之间，是否还存在某种<strong>原理</strong>可以被理解呢？我认为：已经<strong>没有</strong>什么原理性的内容了，TS <strong>仅仅</strong>是根据<strong>先验性</strong>（公理性）的知识来直接<strong>给出答案</strong>。</p>
<p>TS 的类型系统是为 JS 量身定制的：任意 JS 字面量都是 TS 的单元素类型；JS 的基础类型如 <code>number</code> 或 <code>string</code> 也构成了 TS 的基础类型；通过类似定义数组、JSON 对象、函数的语法，我们可以创建数组类型和元组类型、对象类型、函数类型，来表示包含符合条件的数组元素、对象元素或函数元素的集合。TS 当然熟悉 JS 里所有对象类型的<strong>习性</strong> —— 它们有什么成员属性，它们之间如何转化等等 —— 这些知识对 TS 来说是先验性的，所以 TS 才能轻易且正确地进行基础类型的运算。</p>
<pre><code class="hljs language-typescript"><span class="hljs-comment">// 基础类型间的运算</span>
<span class="hljs-keyword">type</span> <span class="hljs-variable constant_">T1</span> = <span class="hljs-built_in">number</span>[<span class="hljs-string">&#x27;toFix&#x27;</span>];                      <span class="hljs-comment">// =&gt; () =&gt; string</span>
<span class="hljs-keyword">type</span> <span class="hljs-variable constant_">T2</span> = [<span class="hljs-built_in">number</span>, <span class="hljs-built_in">string</span>][<span class="hljs-number">1</span>];                  <span class="hljs-comment">// =&gt; string</span>
<span class="hljs-keyword">type</span> <span class="hljs-variable constant_">T3</span> = keyof { <span class="hljs-attr">foo</span>: <span class="hljs-built_in">number</span>, <span class="hljs-attr">bar</span>: <span class="hljs-built_in">string</span> };   <span class="hljs-comment">// =&gt; &#x27;foo&#x27; | &#x27;bar&#x27;</span>
</code></pre><h3>泛型 <code>Head</code></h3>
<p>第二条泛型 <code>Head</code> 取出元组 <code>T</code> 中的第一个元素的类型。</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Head</span>&lt;T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[]&gt; = T <span class="hljs-keyword">extends</span> [] ? <span class="hljs-built_in">never</span> : T[<span class="hljs-number">0</span>];
</code></pre><p><code>Head</code> 首先判断是否满足 <code>T extends []</code>，如果满足，说明 <code>T</code> 是只包含空数组的单元素集，返回 <code>never</code>；否则，说明 <code>T</code> 不是空数组单元素集，可能有第一个元素，返回 <code>T[0]</code>。</p>
<p>为什么条件泛型里只有 <code>extends</code> 关键字，而没有 <code>equals</code> 关键字或 <code>==</code> 运算符？我的领悟是：在集合运算中，只有<strong>包含运算</strong>才能产生「是」或「否」的结果，而集合的其他运算：交集、并集、补集、映射，他们的运算结果都是另一个集合。在集合的语境下，A 包含于 B，意味着 A 是 B 的子集；切换到类型语境下，即 A 是 B 的子类，也就是 A 继承自 B。</p>
<blockquote>
<p>如何判断两个类型完全相同？只需判断它们互相包含（互相继承）。</p>
</blockquote>
<p>对 <code>Head</code> 进行一些测试：</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Head</span>_Test1 = <span class="hljs-title class_">Head</span>&lt;[<span class="hljs-number">1</span>, <span class="hljs-built_in">number</span>]&gt;;    <span class="hljs-comment">// =&gt; 1</span>
<span class="hljs-keyword">type</span> <span class="hljs-title class_">Head</span>_Test2 = <span class="hljs-title class_">Head</span>&lt;<span class="hljs-built_in">string</span>[]&gt;;       <span class="hljs-comment">// =&gt; string</span>
<span class="hljs-keyword">type</span> <span class="hljs-title class_">Head</span>_Test3 = <span class="hljs-title class_">Head</span>&lt;<span class="hljs-built_in">unknown</span>&gt;;        <span class="hljs-comment">// ts error</span>
<span class="hljs-keyword">type</span> <span class="hljs-title class_">Head</span>_Test4 = <span class="hljs-title class_">Head</span>&lt;[]&gt;;             <span class="hljs-comment">// =&gt; never</span>
</code></pre><p>前三条测试的结果是符合直觉的。第四条，当我们把<strong>包含空数组的单元素集</strong>传递给 <code>Head</code>，得到的结果是 <code>never</code> 类型，表示空集，也没有什么问题。</p>
<p>让我们再看一眼第二条测试：请问空数组是不是 <code>string[]</code> 集合的元素？当然是了。那么，在真实世界的 <code>Head</code> 映射中，空数组被映射为了什么元素？</p>
<p>集合论中，映射的前提是，映射规则对源集合内的所有元素都生效。我们应该如何理解 <code>Head</code>？</p>
<p>我的理解是：TS 中存在一个<strong>写不出来</strong>（JS 中没有）的 <strong><code>never</code> 对象</strong>，而<strong>能写出来</strong>的 <strong><code>never</code> 类型</strong>表示包含 <code>never</code> 对象的单元素集。同时，TS 中任何能写出来的类型都隐式包含了 <code>never</code> 对象，这使得任何类型与 <code>never</code> 类型求并集得到的都是自身，从而使本来是单元素集的 <code>never</code> 类型在<strong>概念</strong>上变成了空集。</p>
<p>从这个角度理解 <code>Head&lt;string[]&gt;</code> 就说得通了：<code>string[]</code> 集合中的空数组元素被映射为了 <code>never</code>，而其他元素被映射为了 <code>string</code>；因为 <code>string | never</code> 依然是 <code>string</code>，所以最终返回 <code>string</code>。</p>
<h3>泛型 <code>Tail</code></h3>
<p>第三条泛型 <code>Tail</code> 提取元组 <code>T</code> 的<strong>尾项</strong>（即除去第一项后剩余的那些项）的类型。</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Tail</span>&lt;T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[]&gt; = T <span class="hljs-keyword">extends</span> [] ?
  <span class="hljs-built_in">never</span> : T <span class="hljs-keyword">extends</span> [<span class="hljs-built_in">unknown</span>, ...infer R] ? R : T;
</code></pre><p>有点复杂。</p>
<p>让我们先来看一个简易版本：</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">SimpleTail</span>&lt;T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[]&gt; = T <span class="hljs-keyword">extends</span> [<span class="hljs-built_in">unknown</span>, ...infer R] ? R : <span class="hljs-built_in">never</span>;
</code></pre><p><code>SimpleTail</code> 在形式上和 JS 代码很像：使用剩余参数运算符，把元组中除去第一项的剩余部分提取出来。简单的测试也没有问题：</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">SimpleTail</span>_Test1 = <span class="hljs-title class_">SimpleTail</span>&lt;[]&gt;;                    <span class="hljs-comment">// =&gt; never</span>
<span class="hljs-keyword">type</span> <span class="hljs-title class_">SimpleTail</span>_Test2 = <span class="hljs-title class_">SimpleTail</span>&lt;[<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-built_in">string</span>]&gt;;        <span class="hljs-comment">// =&gt; [2, string]</span>
<span class="hljs-keyword">type</span> <span class="hljs-title class_">SimpleTail</span>_Test3 = <span class="hljs-title class_">SimpleTail</span>&lt;[<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, ...<span class="hljs-built_in">string</span>[]]&gt;;   <span class="hljs-comment">// =&gt; [2, ...string[]]</span>
</code></pre><p>但是，如果用 <code>string[]</code> 测试一下，得到了 <code>never</code>。这就不太对劲了：</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">SimpleTail</span>_Test3 = <span class="hljs-title class_">SimpleTail</span>&lt;<span class="hljs-built_in">string</span>[]&gt;;              <span class="hljs-comment">// =&gt; never</span>
</code></pre><p>在真实世界中，<code>string[]</code> 集合中的几乎所有元素（除空数组对象外），取尾项操作都是有意义的。事实上，如果我们举一些例子进行归纳的话，一定可以得出结论：对 <code>string[]</code> 取尾项的结果<strong>就</strong>是 <code>string[]</code>。但是，根据 <code>SimpleTail</code> 的实现：<code>string[]</code> 又确实不是 <code>[unknown, ...unknown[]]</code> 的子集，我们只能返回 <code>never</code>。</p>
<p>再来看正式版本的 <code>Tail</code>：</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Tail</span>&lt;T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[]&gt; = T <span class="hljs-keyword">extends</span> [] ?
  <span class="hljs-built_in">never</span> : T <span class="hljs-keyword">extends</span> [<span class="hljs-built_in">unknown</span>, ...infer R] ? R : T;
</code></pre><ol>
<li>分支 1：如果 <code>T</code> 是空数组单元素集的子集，我们可以断定：<code>T</code> 只能是空数组单元素集或 <code>never</code>，此时返回 <code>never</code>；</li>
<li>分支 2：如果 <code>T</code> 是「由所有「拥有第一项的数组」组成的集合」的子集，我们可以断定：<code>T</code> 不可能包含空数组元素，此时用类似 <code>SimpleTail</code> 中的形式提取出尾项类型。</li>
<li>分支 3：如果上述两者都不满足，我们直接返回 <code>T</code>。</li>
</ol>
<p>传入 <code>string[]</code> 测试一下，通过分支 3，得到了预期的类型：<code>string[]</code>。</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Tail</span>_Test4 = <span class="hljs-title class_">Tail</span>&lt;<span class="hljs-built_in">string</span>[]&gt;;                           <span class="hljs-comment">// =&gt; string[]</span>
</code></pre><p>你真的笃定吗？如果 <code>T</code> 即不满足分支 1 也不满足分支 2，就一定是类似于 <code>string[]</code> 或 <code>number[]</code> 这种纯粹的数组类型，而不可能是<strong>其他</strong>类型吗？</p>
<p>让我们归纳一下数组（包括元组）类型的<strong>写法</strong>：（我们不关心数组项的具体类型，全部用 <code>unknown</code> 代替）</p>
<ol>
<li>空数组：<code>[]</code>；</li>
<li>纯粹的数组：<code>unknown[]</code>；</li>
<li>元组：<code>[unknownA, unknownB, unknownC]</code>；</li>
<li>带剩余项的元组：<code>[unknownA, unknownB, ...unknownC[]]</code>。</li>
</ol>
<p>经过归纳，我们发现定义数组类型的写法<strong>只有</strong>上面这 4 种，<strong>没有</strong>其他的了！能够<strong>写出来</strong>的数组类型，能够<strong>算出来</strong>（其他泛型返回）的数组类型，最后都能归纳到其中。这 4 种写法就是 TS 处理数组类型的<strong>边界</strong>，换言之 TS 无法产生「无法用这 4 种写法组合出来」的数组类型。</p>
<p>正是基于对以上知识的理解，我们确信只有第 2 种写法（纯粹的数组类型）才能走到分支 3。才能够放心地在分支 3 里返回直接 <code>T</code>。</p>
<p>注意，这里还有一个陷阱。考虑传入并集的情况：</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Tail</span>_Test5 = <span class="hljs-title class_">Tail</span>&lt;[] | <span class="hljs-built_in">string</span>[] | [<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>]&gt;;          <span class="hljs-comment">// string[] | [2, 3]</span>
</code></pre><p>根据集合论，并集的映射，应由组成并集的<strong>每个</strong>单个集合，分别映射之后，再对<strong>多个结果集</strong>取并集，作为最终的结果。</p>
<p><code>Tail</code> 没有令我们失望，它正确地返回了预期的类型。但这是有条件的，泛型中的分支条件必须满足<strong>分发条件类型</strong>的约束：即条件必须是泛型参数<strong>直接</strong> <code>extends</code> 某个类型（形如 <code>T extends SOMETYPE</code>），如果我们把 <code>Tail</code> 实现中的第一个条件 <code>T extends []</code> 换成 <code>Length&lt;T&gt; extends 0</code>，分发条件类型的约束失效，命题「<code>T</code> 只可能是这 4 种写法之一」不复存在，—— 大厦由此坍塌。</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">BrokenTail</span>&lt;T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[]&gt; = <span class="hljs-title class_">Length</span>&lt;T&gt; <span class="hljs-keyword">extends</span> <span class="hljs-number">0</span> ?
  <span class="hljs-built_in">never</span> : T <span class="hljs-keyword">extends</span> [<span class="hljs-built_in">unknown</span>, ...infer R] ? R : T;

<span class="hljs-keyword">type</span> <span class="hljs-title class_">BrokenTail</span>_Test6 = <span class="hljs-title class_">BrokenTail</span>&lt;[] | [<span class="hljs-number">1</span>, <span class="hljs-number">3</span>] | <span class="hljs-built_in">string</span>[]&gt;;  <span class="hljs-comment">// =&gt; [] | [3] | string[]</span>
</code></pre><p>你是否已经体会到泛型编程的某种<strong>笨拙</strong>之处？集合映射的规则（即泛型的语义）是基于<strong>集合内元素</strong>的，但泛型的实现者必须基于<strong>集合本身</strong>的运算来回答「映射之后是什么集合」的问题。这需要从真实世界的角度切实地归纳总结，才能保障映射的<strong>正确性</strong>和<strong>最小性</strong>。</p>
<h3>转换函数的类型</h3>
<p>目前，柯里化转换函数 <code>curry</code> 的类型定义如下：接收一个任意函数，返回 <code>unknown</code>。</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Curry</span> = <span class="hljs-function">(<span class="hljs-params"><span class="hljs-attr">toCurry</span>: <span class="hljs-title class_">Function</span></span>) =&gt;</span> <span class="hljs-built_in">unknown</span>;

<span class="hljs-keyword">declare</span> <span class="hljs-keyword">const</span> <span class="hljs-attr">curry</span>: <span class="hljs-title class_">Curry</span>;
</code></pre><p>我们要把 <code>unknown</code> 换成一个<strong>更精巧的类型</strong>，这样用户在使用 <code>curry</code> 返回的结果（即柯里化函数）时，就能够获得正确的类型提示了。</p>
<p>显然，这个「更精巧的类型」具体是什么，取决于调用 <code>curry</code> 时传入的那个函数。我们使用<strong>泛型约束</strong>把传入函数的参数 <code>P</code> 和返回类型 <code>R</code> 提取出来：</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Curry</span> = &lt;P <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], R&gt;<span class="hljs-function">(<span class="hljs-params"><span class="hljs-attr">toCurry</span>: (...args: P) =&gt; R</span>) =&gt;</span> <span class="hljs-title class_">Curried</span>&lt;P, R&gt;;
</code></pre><p>然后，将 <code>P</code> 和 <code>R</code> 传递 <code>Curried</code> 泛型，作为柯里化函数的类型（即前述的「更精巧的类型」）。</p>
<blockquote>
<p>注意，<code>Curry</code> 不是泛型映射，只是一个具有泛型约束的函数类型。</p>
</blockquote>
<h3>泛型 <code>CurriedV1</code></h3>
<p><code>CurriedV1</code> 是 <code>Curried</code> 泛型的第一版实现，它支持最简单的柯里化（每次只消费一个参数）。</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">CurriedV1</span>&lt;P <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], R&gt; = P <span class="hljs-keyword">extends</span> [] ?
  R : <span class="hljs-function">(<span class="hljs-params"><span class="hljs-attr">arg</span>: <span class="hljs-title class_">Head</span>&lt;P&gt;</span>) =&gt;</span> <span class="hljs-title class_">CurriedV1</span>&lt;<span class="hljs-title class_">Tail</span>&lt;P&gt;, R&gt;;
</code></pre><p>泛型是可以递归调用的，<code>CurriedV1</code> 就是这样，当它每次递归地调用自己，元组 <code>P</code> 的规模就减一，直到其变为空数组，结束递归。</p>
<p>测试一下，很完美：</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">CurriedV1</span>_Test1 = <span class="hljs-title class_">CurriedV1</span>&lt;[<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>], <span class="hljs-number">0</span>&gt;; <span class="hljs-comment">// =&gt; (arg: 1) =&gt; (arg: 2) =&gt; (arg: 3) =&gt; 0</span>
</code></pre><p>你可能会问：如果传入一个无限（未知）长度的数组类型，比如 <code>number[]</code>，TS 会不会陷入死循环？让我们试一试：</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">CurriedV1</span>_Test2 = <span class="hljs-title class_">CurriedV1</span>&lt;<span class="hljs-built_in">number</span>[], <span class="hljs-number">0</span>&gt;; <span class="hljs-comment">// =&gt; (arg: number) =&gt; ...</span>
</code></pre><p>TS 没有报错或陷入死循环，而是仍然映射出了一种可以无穷调用下去的函数类型。所以，我们可以得出结论：递归时逐渐缩减规模并不是泛型递归的必要条件。</p>
<p>事实上，泛型的某种<strong>惰性</strong>机制允许我们去为诸如 JS 中的<strong>循环引用对象</strong>或<strong>返回自身的函数</strong>定义类型：</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Foo</span>&lt;T&gt; = { <span class="hljs-attr">foo</span>: <span class="hljs-title class_">Foo</span>&lt;T&gt; };
<span class="hljs-keyword">declare</span> <span class="hljs-keyword">const</span> <span class="hljs-attr">foo</span>: <span class="hljs-title class_">Foo</span>&lt;<span class="hljs-built_in">number</span>&gt;;
foo.<span class="hljs-property">foo</span>.<span class="hljs-property">foo</span>.<span class="hljs-property">foo</span>.<span class="hljs-property">foo</span>.<span class="hljs-property">foo</span>;            <span class="hljs-comment">// =&gt; 属性 foo 可以无限取下去</span>

<span class="hljs-keyword">type</span> <span class="hljs-title class_">Bar</span>&lt;T <span class="hljs-title function_">extends</span> () =&gt; <span class="hljs-built_in">number</span>&gt; = <span class="hljs-function">() =&gt;</span> <span class="hljs-title class_">Bar</span>&lt;T&gt;;
<span class="hljs-keyword">declare</span> <span class="hljs-keyword">const</span> <span class="hljs-attr">bar</span>: <span class="hljs-title class_">Bar</span>&lt;<span class="hljs-function">() =&gt;</span> <span class="hljs-number">1</span>&gt;;
<span class="hljs-title function_">bar</span>()()()()();                      <span class="hljs-comment">// =&gt; 函数 bar 可以无限调用下去</span>
</code></pre><blockquote>
<p>讲到这里，其实大部分「从集合视角看泛型」的领悟已经陈述完毕了。接下来，我会加快一些速度，把更高级的柯里化的类型实现讲完。</p>
</blockquote>
<h3>CurriedV2：允许不定项参数</h3>
<p>如果柯里化函数可以接收不定项参数（形如 <code>curried(1)(2,3)(4)</code>），就会更易用一些。我的实现是：</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Prepend</span>&lt;E, T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[]&gt; = [E, ...T];

<span class="hljs-keyword">type</span> <span class="hljs-title class_">Drop</span>&lt;N <span class="hljs-keyword">extends</span> <span class="hljs-built_in">number</span>, P <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[] = []&gt; =
    <span class="hljs-title class_">Length</span>&lt;T&gt; <span class="hljs-keyword">extends</span> N ? P : <span class="hljs-title class_">Drop</span>&lt;N, <span class="hljs-title class_">Tail</span>&lt;P&gt;, <span class="hljs-title class_">Prepend</span>&lt;<span class="hljs-built_in">unknown</span>, T&gt;&gt;;

<span class="hljs-keyword">type</span> <span class="hljs-title class_">PartialTuple</span>&lt;T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[]&gt; = <span class="hljs-title class_">Partial</span>&lt;T&gt; &amp; <span class="hljs-built_in">unknown</span>[];

<span class="hljs-keyword">type</span> <span class="hljs-title class_">CurriedV2</span>&lt;P <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], R&gt; =
    <span class="hljs-title class_">Length</span>&lt;P&gt; <span class="hljs-keyword">extends</span> <span class="hljs-number">0</span>
    ? R
    : &lt;T <span class="hljs-keyword">extends</span> <span class="hljs-title class_">PartialTuple</span>&lt;P&gt;&gt;<span class="hljs-function">(<span class="hljs-params">...<span class="hljs-attr">args</span>: T</span>) =&gt;</span> <span class="hljs-title class_">CurriedV2</span>&lt;<span class="hljs-title class_">Drop</span>&lt;<span class="hljs-title class_">Length</span>&lt;T&gt;, P&gt;, R&gt;;

<span class="hljs-keyword">type</span> <span class="hljs-title class_">Curry</span> = &lt;P <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], R&gt;<span class="hljs-function">(<span class="hljs-params"><span class="hljs-attr">fn</span>: (...args: P) =&gt; R</span>) =&gt;</span> <span class="hljs-title class_">CurriedV2</span>&lt;P, R&gt;;

<span class="hljs-keyword">declare</span> <span class="hljs-keyword">const</span> <span class="hljs-attr">curry</span>: <span class="hljs-title class_">Curry</span>;
<span class="hljs-keyword">declare</span> <span class="hljs-keyword">const</span> <span class="hljs-attr">toCurry</span>: <span class="hljs-function">(<span class="hljs-params"><span class="hljs-attr">a1</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">a2</span>: <span class="hljs-number">2</span>, <span class="hljs-attr">a3</span>: <span class="hljs-number">3</span>, <span class="hljs-attr">a4</span>: <span class="hljs-number">4</span></span>) =&gt;</span> <span class="hljs-number">0</span>;

<span class="hljs-keyword">const</span> curried = <span class="hljs-title function_">curry</span>(toCurry);
<span class="hljs-title function_">curried</span>(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>)(<span class="hljs-number">3</span>, <span class="hljs-number">4</span>); <span class="hljs-comment">// =&gt; 0</span>
</code></pre><h3>泛型 <code>Prepend</code></h3>
<p>泛型 <code>Prepend</code> 在元组类型前加上一项。</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Prepend</span>&lt;E, T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[]&gt; = [E, ...T];

<span class="hljs-keyword">type</span> <span class="hljs-title class_">Prepend</span>_Test1 = <span class="hljs-title class_">Prepend</span>&lt;<span class="hljs-number">1</span>, [<span class="hljs-number">2</span>]&gt;;                   <span class="hljs-comment">// ==&gt; [1, 2]</span>
<span class="hljs-keyword">type</span> <span class="hljs-title class_">Prepend</span>_Test2 = <span class="hljs-title class_">Prepend</span>&lt;<span class="hljs-number">1</span>, [<span class="hljs-number">2</span>, ...<span class="hljs-number">3</span>[]]&gt;;           <span class="hljs-comment">// ==&gt; [1, 2, ...3[]]</span>
<span class="hljs-keyword">type</span> <span class="hljs-title class_">Prepend</span>_Test3 = <span class="hljs-title class_">Prepend</span>&lt;<span class="hljs-number">1</span> | <span class="hljs-number">2</span>, <span class="hljs-number">3</span>[]&gt;;               <span class="hljs-comment">// ==&gt; [1 | 2, ...3[]]</span>
</code></pre><p>注意，<code>Prepend</code> 不是条件类型，自然不满足分发条件类型，所以 <code>Prepend_Test3</code> 是 <code>[1 | 2, ...3[]]</code> 而不是 <code>[1, ...3[]] | [2, ...3[]]</code>。如果你想要得到后者，可以将 <code>Prepend</code> 的实现放在条件类型内，如下所示：</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">DistributedPrepend</span>&lt;E <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>, T <span class="hljs-keyword">extends</span>&gt; = E <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span> ?
  [E, ...T] : <span class="hljs-built_in">never</span>;

<span class="hljs-keyword">type</span> <span class="hljs-title class_">DistributedPrepend</span>_Test1 =
  <span class="hljs-title class_">DistributedPrepend</span>&lt;<span class="hljs-number">1</span> | <span class="hljs-number">2</span>, <span class="hljs-number">3</span>[]&gt;;    <span class="hljs-comment">// ==&gt; [1, ...3[]] | [2, ...3[]]</span>
</code></pre><blockquote>
<p>本文后续讨论假设所有传入的类型都是不分散的（即非并集的形式），也不再讨论分发条件类型的问题。</p>
</blockquote>
<h3>泛型 <code>Drop</code></h3>
<p>泛型 <code>Drop</code> 负责从元组中删掉头部的 <code>N</code> 个元素。<code>Drop</code> 也是递归的，每次递归删掉一个元素，同时放置一个 <code>unknown</code> 到元组 <code>T</code> 中。当元组 <code>T</code> 的长度与 <code>N</code> 相等时，说明已经删掉了足够多的元素，把剩下的元素返回即可。</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Drop</span>&lt;N <span class="hljs-keyword">extends</span> <span class="hljs-built_in">number</span>, P <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[] = []&gt; =
    <span class="hljs-title class_">Length</span>&lt;T&gt; <span class="hljs-keyword">extends</span> N ? P : <span class="hljs-title class_">Drop</span>&lt;N, <span class="hljs-title class_">Tail</span>&lt;P&gt;, <span class="hljs-title class_">Prepend</span>&lt;<span class="hljs-built_in">unknown</span>, T&gt;&gt;;
</code></pre><p>简单地测试，没有问题。</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Drop</span>_Test1 = <span class="hljs-title class_">Drop</span>&lt;<span class="hljs-number">2</span>, [<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>, <span class="hljs-number">4</span>]&gt;;    <span class="hljs-comment">// =&gt; [3, 4]</span>
<span class="hljs-keyword">type</span> <span class="hljs-title class_">Drop</span>_Test2 = <span class="hljs-title class_">Drop</span>&lt;<span class="hljs-number">5</span>, [<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>, <span class="hljs-number">4</span>]&gt;;    <span class="hljs-comment">// =&gt; never</span>
<span class="hljs-keyword">type</span> <span class="hljs-title class_">Drop</span>_Test3 = <span class="hljs-title class_">Drop</span>&lt;<span class="hljs-number">5</span>, [<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, ...<span class="hljs-number">3</span>[]]&gt;;  <span class="hljs-comment">// =&gt; 3[]</span>
</code></pre><p><code>Drop</code> 的关键在于，使用了一个空数组，也就是第三个泛型参数 <code>T</code> 来进行计数。</p>
<blockquote>
<p>有趣的是，类似的机制可以用来实现整数的加减法：</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">FromLength</span>&lt;N <span class="hljs-keyword">extends</span> <span class="hljs-built_in">number</span>, P <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[] = []&gt; = 
    <span class="hljs-title class_">Length</span>&lt;P&gt; <span class="hljs-keyword">extends</span> N ? P : <span class="hljs-title class_">FromLength</span>&lt;N, <span class="hljs-title class_">Prepend</span>&lt;<span class="hljs-built_in">unknown</span>, P&gt;&gt;;

<span class="hljs-keyword">type</span> <span class="hljs-title class_">Add</span>&lt;
  A <span class="hljs-keyword">extends</span> <span class="hljs-built_in">number</span>, 
  B <span class="hljs-keyword">extends</span> <span class="hljs-built_in">number</span>, 
  <span class="hljs-title class_">Res</span> <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[] = <span class="hljs-title class_">FromLength</span>&lt;A&gt;, <span class="hljs-title class_">Count</span> <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[] = []
&gt; = <span class="hljs-title class_">Length</span>&lt;<span class="hljs-title class_">Count</span>&gt; <span class="hljs-keyword">extends</span> B ? 
      <span class="hljs-title class_">Length</span>&lt;<span class="hljs-title class_">Res</span>&gt; : 
      <span class="hljs-title class_">Add</span>&lt;A, B, <span class="hljs-title class_">Prepend</span>&lt;<span class="hljs-built_in">unknown</span>, <span class="hljs-title class_">Res</span>&gt;, <span class="hljs-title class_">Prepend</span>&lt;<span class="hljs-built_in">unknown</span>, <span class="hljs-title class_">Count</span>&gt;&gt;;

<span class="hljs-keyword">type</span> <span class="hljs-title class_">Sub</span>&lt;
  A <span class="hljs-keyword">extends</span> <span class="hljs-built_in">number</span>,
  B <span class="hljs-keyword">extends</span> <span class="hljs-built_in">number</span>,
  <span class="hljs-title class_">Res</span> <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[] = [], <span class="hljs-title class_">Count</span> <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[] = <span class="hljs-title class_">FromLength</span>&lt;B&gt;
&gt; = <span class="hljs-title class_">Length</span>&lt;<span class="hljs-title class_">Count</span>&gt; <span class="hljs-keyword">extends</span> A ? 
      <span class="hljs-title class_">Length</span>&lt;<span class="hljs-title class_">Res</span>&gt; : 
      <span class="hljs-title class_">Sub</span>&lt;A, B, <span class="hljs-title class_">Prepend</span>&lt;<span class="hljs-built_in">unknown</span>, <span class="hljs-title class_">Res</span>&gt;, <span class="hljs-title class_">Prepend</span>&lt;<span class="hljs-built_in">unknown</span>, <span class="hljs-title class_">Count</span>&gt;&gt;;

<span class="hljs-keyword">type</span> <span class="hljs-title class_">Eight</span> = <span class="hljs-title class_">Add</span>&lt;<span class="hljs-number">3</span>, <span class="hljs-number">5</span>&gt;;     <span class="hljs-comment">// =&gt; 8</span>
<span class="hljs-keyword">type</span> <span class="hljs-title class_">Four</span> = <span class="hljs-title class_">Sub</span>&lt;<span class="hljs-number">9</span>, <span class="hljs-number">5</span>&gt;;      <span class="hljs-comment">// =&gt; 4</span>
</code></pre></blockquote>
<h3>泛型 <code>PartialTuple</code></h3>
<p>泛型 <code>PartialTuple</code> 的故事要从 TS 的官方泛型 <code>Partial</code> 开始讲。我们知道 <code>Partial</code> 泛型可以将一个对象类型的所有属性都变得可选。当它作用于数组时，会使数组的每一项变成可选，比如 <code>Partial&lt;[number, string]&gt;</code> 可以得到<strong>类似</strong>于 <code>[number?, string?]</code> 的类型。</p>
<p>我们期望 <code>CurriedV2</code> 支持不定项参数，因此需要从定项参数元组中抽取出「元组的前任意项」类型：比如定项参数是类型 <code>[1, 2, 3]</code>，那么不定项参数可以是 <code>[1]</code>，<code>[1, 2]</code> 或者 <code>[1, 2, 3]</code>。然而，TS 目前的类型运算没办法实现「元组的前任意项」这样的映射规则，而 <code>Partial</code> 是最接近的实现（最小超集）。</p>
<p>为什么又需要 <code>PartialTuple</code> 呢？因为被 <code>Partial</code> 转换后的类型已经不再是元组了：诸如 <code>length</code>，<code>map</code> 等属性也成了可选属性，这使得形如 <code>{0: &#39;Hello&#39;}</code> 这样的对象也在 <code>Partial&lt;[string]&gt;</code> 的集合内。<code>PartialTuple</code> 将这部分不属于元组的元素剔除在外。</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">PartialTuple</span>&lt;T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[]&gt; = <span class="hljs-title class_">Partial</span>&lt;T&gt; &amp; <span class="hljs-built_in">unknown</span>[];
</code></pre><p>原文直接使用 <code>Partial</code> 而不报错，这是 TS 的一个 bug：对于 <code>Partial</code> 传入元组类型后，究竟还是不是元组，在不同的条件下判断不一致。我提交了 <a href="https://github.com/microsoft/TypeScript/issues/47128">issue</a> 和<a href="https://www.typescriptlang.org/play?ts=4.1.5#code/C4TwDgpgBAIgjAHgAoD4oF4pKhAHsCAOwBMBnKAV0IGtCB7Ad0IG0BdKAfigAoA6fgIYAuLAIBOwAJYCANshQBKDGgBudScSgjCEFRDEBuKAHpjOMWLpiAsACg7oSLABMyHPiJlKNekzZpMPkERJHEpWXkldFV1YiNTKFIKAGNkiFJSOzsEgBUAC2hgBjp3AQBbMBloUjy6ChlNACM6YDzElLSMiE0rKAAzAUkq4izbIA">最简复现</a>。</p>
<h3>泛型 <code>CurriedV2</code></h3>
<p><code>CurriedV2</code> 与 <code>CurriedV1</code> 在框架上有点类似：</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">CurriedV1</span>&lt;P <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], R&gt; = 
  P <span class="hljs-keyword">extends</span> [] ? R : 
    <span class="hljs-function">(<span class="hljs-params"><span class="hljs-attr">arg</span>: <span class="hljs-title class_">Head</span>&lt;P&gt;</span>) =&gt;</span> <span class="hljs-title class_">CurriedV1</span>&lt;<span class="hljs-title class_">Tail</span>&lt;P&gt;, R&gt;;

<span class="hljs-keyword">type</span> <span class="hljs-title class_">CurriedV2</span>&lt;P <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], R&gt; =
  P <span class="hljs-keyword">extends</span> [] ? R : 
    &lt;T <span class="hljs-keyword">extends</span> <span class="hljs-title class_">PartialTuple</span>&lt;P&gt;&gt;<span class="hljs-function">(<span class="hljs-params">...<span class="hljs-attr">args</span>: T</span>) 
      =&gt;</span> <span class="hljs-title class_">CurriedV2</span>&lt;<span class="hljs-title class_">Drop</span>&lt;<span class="hljs-title class_">Length</span>&lt;T&gt;, P&gt;, R&gt;;
</code></pre><p>最重要的一点区别是，<code>CurriedV2</code> 为柯里化函数引入了泛型约束，这样每次调用时，就能动态提取出传入参数的数量，并据此计算此次调用应该返回的类型。</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">CurriedV1</span>_Test1 = <span class="hljs-title class_">CurriedV1</span>&lt;[<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>], <span class="hljs-number">0</span>&gt;; 
<span class="hljs-comment">// =&gt; (arg: 1) =&gt; (arg: 2) =&gt; (arg: 3) =&gt; 0</span>

<span class="hljs-keyword">type</span> <span class="hljs-title class_">CurriedV2</span>_Test1 = <span class="hljs-title class_">CurriedV2</span>&lt;[<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>], <span class="hljs-number">0</span>&gt;;
<span class="hljs-comment">// =&gt; &lt;T extends PartialTuple&lt;[1, 2, 3]&gt;&gt;(...args: T)</span>
<span class="hljs-comment">//   =&gt; CurriedV2&lt;Drop&lt;Length&lt;T&gt;, [1, 2, 3], []&gt;, 0&gt;</span>
</code></pre><p>简单测试，我们发现 <code>CurriedV2_Test1</code> 无法直白给出柯里化函数的类型，因为每一步调用后得到类型，只有调用的时候才能（根据参数）确定。</p>
<h3>CurriedV3：支持剩余参数</h3>
<p>有些函数的参数分为两个部分：固定参数和剩余参数。比如这样的 <code>toCurry</code>：在前四个固定参数之后，你可以传入任意个类型为 <code>5</code> 的剩余参数：</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">declare</span> <span class="hljs-keyword">const</span> <span class="hljs-attr">toCurry</span>: <span class="hljs-function">(<span class="hljs-params"><span class="hljs-attr">a1</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">a2</span>: <span class="hljs-number">2</span>, <span class="hljs-attr">a3</span>: <span class="hljs-number">3</span>, <span class="hljs-attr">a4</span>: <span class="hljs-number">4</span>, ...<span class="hljs-attr">args</span>: <span class="hljs-number">5</span>[]</span>) =&gt;</span> <span class="hljs-number">0</span>;

<span class="hljs-comment">// 必须在最后一次调用时一次性传入所有剩余参数</span>
<span class="hljs-title function_">curry</span>(toCurry)(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>)(<span class="hljs-number">4</span>, <span class="hljs-number">5</span>, <span class="hljs-number">5</span>);
</code></pre><p>如果柯里化可以支持这种函数，无疑会更好：这就是 <code>CurriedV3</code> 的目标。我的实现是：</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">CurriedV3</span>&lt;P <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], R&gt; =
    P <span class="hljs-keyword">extends</span> [<span class="hljs-built_in">unknown</span>, ...<span class="hljs-built_in">unknown</span>[]]
    ? &lt;T <span class="hljs-keyword">extends</span> <span class="hljs-title class_">PartialTuple</span>&lt;P&gt;&gt;<span class="hljs-function">(<span class="hljs-params">...<span class="hljs-attr">args</span>: T</span>) =&gt;</span> <span class="hljs-title class_">CurryV3</span>&lt;<span class="hljs-title class_">Drop</span>&lt;<span class="hljs-title class_">Length</span>&lt;T&gt;, P&gt;, R&gt;
    : R;

<span class="hljs-keyword">type</span> <span class="hljs-title class_">Curry</span> = &lt;P <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], R&gt;<span class="hljs-function">(<span class="hljs-params"><span class="hljs-attr">fn</span>: (...args: P) =&gt; R</span>) =&gt;</span> <span class="hljs-title class_">CurriedV3</span>&lt;P, R&gt;;

<span class="hljs-keyword">declare</span> <span class="hljs-keyword">const</span> <span class="hljs-attr">curry</span>: <span class="hljs-title class_">Curry</span>;
<span class="hljs-keyword">declare</span> <span class="hljs-keyword">const</span> <span class="hljs-attr">toCurry</span>: <span class="hljs-function">(<span class="hljs-params"><span class="hljs-attr">a1</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">a2</span>: <span class="hljs-number">2</span>, <span class="hljs-attr">a3</span>: <span class="hljs-number">3</span>, <span class="hljs-attr">a4</span>: <span class="hljs-number">4</span>, ...<span class="hljs-attr">args</span>: <span class="hljs-number">5</span>[]</span>) =&gt;</span> <span class="hljs-number">0</span>;

<span class="hljs-keyword">const</span> curried = <span class="hljs-title function_">curry</span>(toCurry);
<span class="hljs-keyword">const</span> result = <span class="hljs-title function_">curried</span>(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>)(<span class="hljs-number">4</span>，<span class="hljs-number">5</span>，<span class="hljs-number">5</span>);
</code></pre><p><code>CurriedV3</code> 与 <code>CurriedV2</code> 的区别<strong>仅仅</strong>在于递归结束条件不同：<code>CurriedV3</code> 通过判断满足 <code>P extends [unknown, ...unknown[]]</code> 推断出 <code>P</code> 仍然包含固定项，此时继续递归；不满足此条件说明 <code>P</code> 中只有剩余参数了，结束递归。</p>
<p>得益于严密的 <code>Drop</code> 以及背后的 <code>Tail</code> —— 它们妥善处理了纯粹数组和包含剩余项元组的情况 —— <code>CurriedV3</code> 的递归部分和 <code>CurriedV2</code> 是完全一致的。</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Drop</span>_Test3 = <span class="hljs-title class_">Drop</span>&lt;<span class="hljs-number">5</span>, [<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, ...<span class="hljs-number">3</span>[]]&gt;;  <span class="hljs-comment">// =&gt; 3[]</span>
<span class="hljs-keyword">type</span> <span class="hljs-title class_">Tail</span>_Test5 = <span class="hljs-title class_">Tail</span>&lt;<span class="hljs-number">1</span>[]&gt;;                <span class="hljs-comment">// =&gt; 1[]</span>
</code></pre><p>如果 <code>Drop</code> 和 <code>Tail</code> 对上述较为边缘的处理不够完善（比如直接返回 <code>never</code> 或 <code>[]</code>），<code>CurriedV1</code> 和 <code>CurriedV2</code> 并不会受什么影响，但是 <code>CurriedV3</code> 的实现就没那么容易了。</p>
<h3>CurriedV4: 支持占位符</h3>
<p>柯里化中的占位符，能够帮助我们延迟传入参数的时机。比如：</p>
<pre><code class="hljs language-typescript"><span class="hljs-comment">// 普通的调用</span>
<span class="hljs-title function_">curried</span>(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>)(<span class="hljs-number">4</span>, <span class="hljs-number">5</span>);
<span class="hljs-comment">// 占位符调用</span>
<span class="hljs-title function_">curried</span>(<span class="hljs-number">1</span>, __, <span class="hljs-number">3</span>)(<span class="hljs-number">2</span>, <span class="hljs-number">4</span>, <span class="hljs-number">5</span>);
<span class="hljs-comment">// 甚至</span>
<span class="hljs-title function_">curried</span>(<span class="hljs-number">1</span>, __, <span class="hljs-number">3</span>)(__, <span class="hljs-number">4</span>)(<span class="hljs-number">2</span>, <span class="hljs-number">5</span>);
</code></pre><p>这就是 <code>CurriedV4</code> 的目标。我的实现是：</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Equal</span>&lt;X, Y&gt; = X <span class="hljs-keyword">extends</span> Y ? Y <span class="hljs-keyword">extends</span> X ? <span class="hljs-literal">true</span> : <span class="hljs-literal">false</span> : <span class="hljs-literal">false</span>;

<span class="hljs-keyword">type</span> <span class="hljs-title class_">Item</span>&lt;T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[]&gt; = T <span class="hljs-title function_">extends</span> (infer R)[] ? R : <span class="hljs-built_in">never</span>;

<span class="hljs-keyword">type</span> <span class="hljs-title class_">PlaceholderTuple</span>&lt;T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], M <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>&gt; =
  { [P <span class="hljs-keyword">in</span> keyof T]?: T[P] | M } &amp; <span class="hljs-built_in">unknown</span>[];

<span class="hljs-keyword">type</span> <span class="hljs-title class_">Reverse</span>&lt;T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], R <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[] = []&gt; =
    <span class="hljs-title class_">Equal</span>&lt;<span class="hljs-title class_">Length</span>&lt;T&gt;, <span class="hljs-built_in">number</span>&gt; <span class="hljs-keyword">extends</span> <span class="hljs-literal">true</span>
    ? <span class="hljs-title class_">Item</span>&lt;T&gt;[]
    : T <span class="hljs-keyword">extends</span> [<span class="hljs-built_in">unknown</span>, ...<span class="hljs-built_in">unknown</span>[]]
    ? <span class="hljs-title class_">Reverse</span>&lt;<span class="hljs-title class_">Tail</span>&lt;T&gt;, <span class="hljs-title class_">Prepend</span>&lt;<span class="hljs-title class_">Head</span>&lt;T&gt;, R&gt;&gt;
    : R;

<span class="hljs-keyword">type</span> <span class="hljs-title class_">Join</span>&lt;P <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[]&gt; = 
    P <span class="hljs-keyword">extends</span> [<span class="hljs-built_in">unknown</span>, ...<span class="hljs-built_in">unknown</span>[]] ? <span class="hljs-title class_">Join</span>&lt;<span class="hljs-title class_">Tail</span>&lt;P&gt;, <span class="hljs-title class_">Prepend</span>&lt;<span class="hljs-title class_">Head</span>&lt;P&gt;, T&gt;&gt; : T;

<span class="hljs-keyword">type</span> <span class="hljs-title class_">Concat</span>&lt;P <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[]&gt; = <span class="hljs-title class_">Join</span>&lt;<span class="hljs-title class_">Reverse</span>&lt;P&gt;, T&gt;;

<span class="hljs-keyword">type</span> <span class="hljs-title class_">PlaceholderMatched</span>&lt;
  T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], S <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], M <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>, R <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[] = []
&gt; = T <span class="hljs-keyword">extends</span> [<span class="hljs-built_in">unknown</span>, ...<span class="hljs-built_in">unknown</span>[]] ?
      <span class="hljs-title class_">PlaceholderMatched</span>&lt;<span class="hljs-title class_">Tail</span>&lt;T&gt;, <span class="hljs-title class_">Tail</span>&lt;S&gt;, M, <span class="hljs-title class_">Head</span>&lt;T&gt; <span class="hljs-keyword">extends</span> M ? <span class="hljs-title class_">Prepend</span>&lt;<span class="hljs-title class_">Head</span>&lt;S&gt;, R&gt; : R&gt;
      : <span class="hljs-title class_">Reverse</span>&lt;R&gt;;

<span class="hljs-keyword">type</span> __ = <span class="hljs-string">&#x27;__&#x27;</span>;
<span class="hljs-keyword">type</span> <span class="hljs-title class_">CurriedV4</span>&lt;P <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], R&gt; =
    P <span class="hljs-keyword">extends</span> [<span class="hljs-built_in">unknown</span>, ...<span class="hljs-built_in">unknown</span>[]]
    ? &lt;T <span class="hljs-keyword">extends</span> <span class="hljs-title class_">PlaceholderTuple</span>&lt;P, __&gt;&gt;<span class="hljs-function">(<span class="hljs-params">...<span class="hljs-attr">args</span>: T</span>) =&gt;</span>
        <span class="hljs-title class_">CurriedV4</span>&lt;<span class="hljs-title class_">Concat</span>&lt;<span class="hljs-title class_">PlaceholderMatched</span>&lt;T, P, __&gt;, <span class="hljs-title class_">Drop</span>&lt;<span class="hljs-title class_">Length</span>&lt;T&gt;, P&gt;&gt;, R&gt;
    : R;

<span class="hljs-keyword">type</span> <span class="hljs-title class_">Curry</span> = &lt;P <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], R&gt;<span class="hljs-function">(<span class="hljs-params"><span class="hljs-attr">fn</span>: (...args: P) =&gt; R</span>) =&gt;</span> <span class="hljs-title class_">CurriedV4</span>&lt;P, R&gt;;

<span class="hljs-keyword">declare</span> <span class="hljs-keyword">const</span> <span class="hljs-attr">curry</span>: <span class="hljs-title class_">Curry</span>;
<span class="hljs-keyword">declare</span> <span class="hljs-keyword">const</span> <span class="hljs-attr">toCurry</span>: <span class="hljs-function">(<span class="hljs-params"><span class="hljs-attr">a1</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">a2</span>: <span class="hljs-number">2</span>, <span class="hljs-attr">a3</span>: <span class="hljs-number">3</span>, <span class="hljs-attr">a4</span>: <span class="hljs-number">4</span>, ...<span class="hljs-attr">args</span>: <span class="hljs-number">5</span>[]</span>) =&gt;</span> <span class="hljs-number">0</span>;
<span class="hljs-keyword">declare</span> <span class="hljs-keyword">const</span> <span class="hljs-attr">__</span>: __;

<span class="hljs-keyword">const</span> curried = <span class="hljs-title function_">curry</span>(toCurry);
<span class="hljs-title function_">curried</span>(<span class="hljs-number">1</span>, __, <span class="hljs-number">3</span>)(<span class="hljs-number">2</span>, <span class="hljs-number">4</span>, <span class="hljs-number">5</span>, <span class="hljs-number">5</span>);          <span class="hljs-comment">// =&gt; 0</span>
<span class="hljs-title function_">curried</span>(<span class="hljs-number">1</span>, __, <span class="hljs-number">3</span>)(__, <span class="hljs-number">4</span>)(<span class="hljs-number">2</span>);            <span class="hljs-comment">// =&gt; 0</span>
</code></pre><h3>泛型 <code>Equal</code></h3>
<p>泛型 <code>Equal</code> 判断两个类型是不是完全相等（注意，仍然是集合运算，<code>true</code> 和 <code>false</code> 表示包含布尔值的单元素集合）。</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Equal</span>&lt;X, Y&gt; = X <span class="hljs-keyword">extends</span> Y ? Y <span class="hljs-keyword">extends</span> X ? <span class="hljs-literal">true</span> : <span class="hljs-literal">false</span> : <span class="hljs-literal">false</span>;

<span class="hljs-keyword">type</span> <span class="hljs-title class_">Equal</span>_Test1 = <span class="hljs-title class_">Equal</span>&lt;<span class="hljs-built_in">number</span>, <span class="hljs-number">1</span>&gt;;            <span class="hljs-comment">// =&gt; false</span>
<span class="hljs-keyword">type</span> <span class="hljs-title class_">Equal</span>_Test2 = <span class="hljs-title class_">Equal</span>&lt;<span class="hljs-built_in">number</span>, <span class="hljs-built_in">number</span>&gt;;       <span class="hljs-comment">// =&gt; true</span>
</code></pre><h3>泛型 <code>Item</code></h3>
<p>泛型 <code>Item</code> 从数组类型中提取出数组项的可能类型。</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Item</span>&lt;T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[]&gt; = T <span class="hljs-title function_">extends</span> (infer R)[] ? R : <span class="hljs-built_in">never</span>;

<span class="hljs-keyword">type</span> <span class="hljs-title class_">Item</span>_Test1 = <span class="hljs-title class_">Item</span>&lt;<span class="hljs-built_in">string</span>[]&gt;; <span class="hljs-comment">// =&gt; string</span>
<span class="hljs-keyword">type</span> <span class="hljs-title class_">Item</span>_Test2 = <span class="hljs-title class_">Item</span>&lt;[<span class="hljs-built_in">string</span>, ...<span class="hljs-number">1</span>[]]&gt;; <span class="hljs-comment">// =&gt; string | 1</span>
</code></pre><h3>泛型 <code>PlaceholderTuple</code></h3>
<p>泛型 <code>PlaceholderTuple</code> 与 <code>PartialTuple</code> 很类似，它不仅使元组的每一项变成可选，而且使每一项都可能是传入的类型 <code>M</code>。</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">PlaceholderTuple</span>&lt;T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], M <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>&gt; =
  { [P <span class="hljs-keyword">in</span> keyof T]?: T[P] | M } &amp; <span class="hljs-built_in">unknown</span>[];
</code></pre><h3>泛型 <code>Reverse</code></h3>
<p>泛型 <code>Reverse</code> 将元组头尾翻转。</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Reverse</span>&lt;T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], R <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[] = []&gt; =
    <span class="hljs-title class_">Equal</span>&lt;<span class="hljs-title class_">Length</span>&lt;T&gt;, <span class="hljs-built_in">number</span>&gt; <span class="hljs-keyword">extends</span> <span class="hljs-literal">true</span>
    ? <span class="hljs-title class_">Item</span>&lt;T&gt;[]
    : T <span class="hljs-keyword">extends</span> [<span class="hljs-built_in">unknown</span>, ...<span class="hljs-built_in">unknown</span>[]]
    ? <span class="hljs-title class_">Reverse</span>&lt;<span class="hljs-title class_">Tail</span>&lt;T&gt;, <span class="hljs-title class_">Prepend</span>&lt;<span class="hljs-title class_">Head</span>&lt;T&gt;, R&gt;&gt;
    : R;
</code></pre><p>泛型 <code>Reverse</code> 值得稍作展开。先看核心部分（从 <code>T extends</code> 开始）：接收数组类型 <code>T</code>，递归地调用自己，每次递归将 <code>T</code> 的头元素取下来，从头部推入 <code>R</code> 中。当 <code>T</code> 消耗殆尽，<code>R</code> 自然就是翻转后的数组。</p>
<p>对于固定长度的元组类型，这样做没问题。但是，如果想要翻转不固定长度的数组类型呢？</p>
<p>通过真实世界中的简单的归纳，我们知道 <code>Reverse&lt;string[]&gt;</code> 应该是 <code>string[]</code>，映射仍然是完美的；对于 <code>Reverse&lt;[string, ...number[]]&gt;</code>，我们只能将其映射为 <code>Array&lt;string | number&gt;</code> —— 我们之前说过，泛型的返回时常比我们预期的类型要宽泛，这不可避免。</p>
<p><code>Reverse</code> 实现的前两行（非核心部分），就是用来处理上述两种不固定长度数组类型的。</p>
<p>测试一下：</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Reverse</span>_Test1 = <span class="hljs-title class_">Reverse</span>&lt;[<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>]&gt;;                <span class="hljs-comment">// =&gt; [3, 2, 1]</span>
<span class="hljs-keyword">type</span> <span class="hljs-title class_">Reverse</span>_Test2 = <span class="hljs-title class_">Reverse</span>&lt;<span class="hljs-built_in">unknown</span>[]&gt;;                <span class="hljs-comment">// =&gt; unknown[]</span>
<span class="hljs-keyword">type</span> <span class="hljs-title class_">Reverse</span>_Test3 = <span class="hljs-title class_">Reverse</span>&lt;[<span class="hljs-built_in">string</span>, ...<span class="hljs-built_in">number</span>[]]&gt;;    <span class="hljs-comment">// =&gt; Array&lt;string | number&gt;</span>
</code></pre><h3>泛型 <code>Join</code></h3>
<p>泛型 <code>Join</code> 将两个元组类型「头对头连接起来」。注意，第一个参数必须是固定项的元组类型。</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Join</span>&lt;P <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[]&gt; =
  P <span class="hljs-keyword">extends</span> [<span class="hljs-built_in">unknown</span>, ...<span class="hljs-built_in">unknown</span>[]] ? <span class="hljs-title class_">Join</span>&lt;<span class="hljs-title class_">Tail</span>&lt;P&gt;, <span class="hljs-title class_">Prepend</span>&lt;<span class="hljs-title class_">Head</span>&lt;P&gt;, T&gt;&gt; : T;

<span class="hljs-keyword">type</span> <span class="hljs-title class_">Join</span>_Test1 = <span class="hljs-title class_">Join</span>&lt;[<span class="hljs-number">1</span>, <span class="hljs-number">2</span>], [<span class="hljs-number">3</span>, <span class="hljs-number">4</span>]&gt;;         <span class="hljs-comment">// =&gt; [2, 1, 3, 4]</span>
<span class="hljs-keyword">type</span> <span class="hljs-title class_">Join</span>_Test2 = <span class="hljs-title class_">Join</span>&lt;[<span class="hljs-number">1</span>, <span class="hljs-number">2</span>], [<span class="hljs-number">3</span>, ...<span class="hljs-number">4</span>[]]&gt;;    <span class="hljs-comment">// =&gt; [2, 1, 3, ...4[]]</span>
<span class="hljs-keyword">type</span> <span class="hljs-title class_">Join</span>_Test3 = <span class="hljs-title class_">Join</span>&lt;[<span class="hljs-number">1</span>, ...<span class="hljs-number">2</span>[]], [<span class="hljs-number">3</span>, <span class="hljs-number">4</span>]&gt;;    <span class="hljs-comment">// =&gt; ts error</span>
</code></pre><h3>泛型 <code>Concat</code></h3>
<p>泛型 <code>Concat</code> 将两个元组类型顺序连接起来。同理，第一个参数也必须是固定项的元组类型。</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Concat</span>&lt;P <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[]&gt; = <span class="hljs-title class_">Join</span>&lt;<span class="hljs-title class_">Reverse</span>&lt;P&gt;, T&gt;;

<span class="hljs-keyword">type</span> <span class="hljs-title class_">Concat</span>_Test1 = <span class="hljs-title class_">Concat</span>&lt;[<span class="hljs-number">1</span>, <span class="hljs-number">2</span>], [<span class="hljs-number">3</span>, <span class="hljs-number">4</span>]&gt;;         <span class="hljs-comment">// =&gt; [1, 2, 3, 4]</span>
<span class="hljs-keyword">type</span> <span class="hljs-title class_">Concat</span>_Test2 = <span class="hljs-title class_">Concat</span>&lt;[<span class="hljs-number">1</span>, <span class="hljs-number">2</span>], [<span class="hljs-number">3</span>, ...<span class="hljs-number">4</span>[]]&gt;;    <span class="hljs-comment">// =&gt; [1, 2, 3, ...4[]]</span>
<span class="hljs-keyword">type</span> <span class="hljs-title class_">Concat</span>_Test3 = <span class="hljs-title class_">Concat</span>&lt;[<span class="hljs-number">1</span>, ...<span class="hljs-number">2</span>[]], [<span class="hljs-number">3</span>, <span class="hljs-number">4</span>]&gt;;    <span class="hljs-comment">// =&gt; ts error</span>
</code></pre><h3>泛型 <code>PlaceholderMatched</code></h3>
<p>泛型 <code>PlaceholderMatched</code> 将元组 <code>T</code> 中的类型为 <code>M</code> 的项找出来，然后从元组 <code>S</code> 中提取出对应位置的项，顺序存放在一个新的元组里 <code>R</code>，并最终返回。</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">PlaceholderMatched</span>&lt;
  T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], S <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], M <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>, R <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[] = []
&gt; = T <span class="hljs-keyword">extends</span> [<span class="hljs-built_in">unknown</span>, ...<span class="hljs-built_in">unknown</span>[]] ? 
      <span class="hljs-title class_">PlaceholderMatched</span>&lt;<span class="hljs-title class_">Tail</span>&lt;T&gt;, <span class="hljs-title class_">Tail</span>&lt;S&gt;, M, <span class="hljs-title class_">Head</span>&lt;T&gt; <span class="hljs-keyword">extends</span> M ? <span class="hljs-title class_">Prepend</span>&lt;<span class="hljs-title class_">Head</span>&lt;S&gt;, R&gt; : R&gt;
      : <span class="hljs-title class_">Reverse</span>&lt;R&gt;;
</code></pre><p>有一点拗口。简单看一下测试就知道 <code>PlaceholderMatched</code> 的具体作用了：</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> __ = <span class="hljs-string">&#x27;__&#x27;</span>;
<span class="hljs-keyword">type</span> <span class="hljs-title class_">PlaceholderMatched</span>_Test1 = 
  <span class="hljs-title class_">PlaceholderMatched</span>&lt;[<span class="hljs-number">1</span>, __, __, <span class="hljs-number">4</span>], [<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>, <span class="hljs-number">4</span>, <span class="hljs-number">5</span>], __&gt;; <span class="hljs-comment">// =&gt; [2, 3]</span>
</code></pre><h3>泛型 <code>CurriedV4</code></h3>
<p>最后来看柯里化后函数类型的完全体 <code>CurriedV4</code>：</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> __ = <span class="hljs-string">&#x27;__&#x27;</span>;

<span class="hljs-keyword">type</span> <span class="hljs-title class_">CurriedV4</span>&lt;P <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], R&gt; =
    P <span class="hljs-keyword">extends</span> [<span class="hljs-built_in">unknown</span>, ...<span class="hljs-built_in">unknown</span>[]]
    ? &lt;T <span class="hljs-keyword">extends</span> <span class="hljs-title class_">PlaceholderTuple</span>&lt;P, __&gt;&gt;<span class="hljs-function">(<span class="hljs-params">...<span class="hljs-attr">args</span>: T</span>) =&gt;</span>
        <span class="hljs-title class_">CurriedV4</span>&lt;<span class="hljs-title class_">Concat</span>&lt;<span class="hljs-title class_">PlaceholderMatched</span>&lt;T, P, __&gt;, <span class="hljs-title class_">Drop</span>&lt;<span class="hljs-title class_">Length</span>&lt;T&gt;, P&gt;&gt;, R&gt;
    : R;
</code></pre><p><code>CurriedV4</code> 与 <code>CurriedV3</code> 的区别在递归部分。我们用 <code>PlaceholderTuple&lt;P, __&gt;</code> 约束柯里化函数的入参，这样调用者就可以传入占位符常量 <code>__</code> 了。</p>
<p>单次递归中，我们将「被占位的元素」构成的元组类型提取出来（即 <code>PlaceholderMatched&lt;T, P, __&gt;</code>），然后与此次调用消耗参数后剩余的参数（即 <code>Drop&lt;Length&lt;T&gt;, P&gt;&gt;</code>）连接起来，作为新的参数 <code>P</code>，传入下一次递归。</p>
<p>测试一下，完美。</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Curry</span> = &lt;P <span class="hljs-keyword">extends</span> <span class="hljs-built_in">unknown</span>[], R&gt;<span class="hljs-function">(<span class="hljs-params"><span class="hljs-attr">fn</span>: (...args: P) =&gt; R</span>) =&gt;</span> <span class="hljs-title class_">CurriedV4</span>&lt;P, R&gt;;

<span class="hljs-keyword">declare</span> <span class="hljs-keyword">const</span> <span class="hljs-attr">curry</span>: <span class="hljs-title class_">Curry</span>;
<span class="hljs-keyword">declare</span> <span class="hljs-keyword">const</span> <span class="hljs-attr">toCurry</span>: <span class="hljs-function">(<span class="hljs-params"><span class="hljs-attr">a1</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">a2</span>: <span class="hljs-number">2</span>, <span class="hljs-attr">a3</span>: <span class="hljs-number">3</span>, <span class="hljs-attr">a4</span>: <span class="hljs-number">4</span>, ...<span class="hljs-attr">args</span>: <span class="hljs-number">5</span>[]</span>) =&gt;</span> <span class="hljs-number">0</span>;
<span class="hljs-keyword">declare</span> <span class="hljs-keyword">const</span> <span class="hljs-attr">__</span>: __;

<span class="hljs-keyword">const</span> curried = <span class="hljs-title function_">curry</span>(toCurry);

<span class="hljs-title function_">curried</span>(<span class="hljs-number">1</span>, __, <span class="hljs-number">3</span>)(<span class="hljs-number">2</span>, <span class="hljs-number">4</span>, <span class="hljs-number">5</span>, <span class="hljs-number">5</span>);          <span class="hljs-comment">// =&gt; 0</span>
<span class="hljs-comment">// =&gt; CurriedV4&lt;[1, 2, 3, 4, ...5[]], 0&gt; =&gt; CurriedV4&lt;[2, 4, ...5[]], 0&gt;</span>

<span class="hljs-title function_">curried</span>(<span class="hljs-number">1</span>, __, <span class="hljs-number">3</span>)(__, <span class="hljs-number">4</span>)(<span class="hljs-number">2</span>);            <span class="hljs-comment">// =&gt; 0</span>
<span class="hljs-comment">// =&gt; CurriedV4&lt;[1, 2, 3, 4, ...5[]], 0&gt; =&gt; CurriedV4&lt;[2, 4, ...5[]], 0&gt; </span>
<span class="hljs-comment">//    =&gt; CurriedV4&lt;[2, ...5[]], 0&gt;</span>
</code></pre><h3>小结</h3>
<p>虽然本文中，对集合的讨论主要集中在前半部分，但是促使我去思考的，其实是对后面几个更高级的场景的实践。我发现，把这些实践的领悟套用在最开始的几个简单泛型上进行陈述，似乎更加清晰。</p>
<p>原文中，一开始的基础泛型就不是很严密，比如 <code>Head</code> 泛型是这样的：</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Head</span>&lt;T <span class="hljs-keyword">extends</span> <span class="hljs-built_in">any</span>[]&gt; = T <span class="hljs-keyword">extends</span> [<span class="hljs-built_in">any</span>, ...<span class="hljs-built_in">any</span>[]] ? T[<span class="hljs-number">0</span>] : <span class="hljs-built_in">never</span>;
</code></pre><p>这导致 <code>Head&lt;string[]&gt;</code> 返回的是 <code>never</code>，明显与从集合视角看上去的情形不符。</p>
<p>原文的很多基础类型，都存在没有处理妥善的边缘用例，所以当问题越来越复杂之时，泛型实现就会越来越不可控。后来原作者开始引入 <code>Cast</code> 泛型，把推导到边缘的不准确的类型强行转换回来。</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">type</span> <span class="hljs-title class_">Cast</span>&lt;X, Y&gt; = X <span class="hljs-keyword">extends</span> Y ? X : Y;
</code></pre><p>这引发了我的思考，这些基础泛型究竟<strong>应该</strong>实现成什么样？在反复的实践中，我发现凭借直觉写出来的代码往往不够准确，有那么一刻，我领悟到我缺少的其实是一种集合的视角；而一旦从集合的视角理解了泛型运算的实质，似有一种豁然开朗之感：什么能做，什么不能做，哪里可以妥协，哪里只能放弃，就都可以确定地分析出来了。</p>
<p>（完）</p>
]]></description>
            <link>https://xieguanglei.github.io/blog/2022-01-16/ji-he-shi-jiao-xia-de-typescript-fan-xing-kai-fa-shi-jian</link>
            <guid isPermaLink="true">https://xieguanglei.github.io/blog/2022-01-16/ji-he-shi-jiao-xia-de-typescript-fan-xing-kai-fa-shi-jian</guid>
            <pubDate>Sun, 16 Jan 2022 00:00:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Flutter Framework 源码解析（3）—— RenderTree 概述]]></title>
            <description><![CDATA[<p>2019 年开始的 Flutter Framework 源码解析系列，一下子竟然搁置了接近两年。这两天过年，又想重新拾起来。仔细读了自己之前写的那两篇，幸好还能看懂，试了试电脑上的运行环境，居然也还能跑起来。近两年过去了，Flutter 升级到了 1.20，而我还是用的 1.2.2 版本，好在 UI 渲染的内核原理，想来不会发生什么大变化，所以这篇文章仍将基于老版本来进行。</p>
<p>我们直接开始吧：</p>
<p>两年前，我们讲到了 Layer —— 离 Engine 最近的一层。Layer 层之上是 RenderTree，这是 Flutter 渲染的核心：Flex 布局，绝对定位，文字排版，等等都是在 RenderTree 中完成的。本质上，你看到的每一个字、每一个色块、图片为什么出现在了屏幕的那个位置，就是由 RenderTree 决定。从这一层开始，我们会接触到一些和 CSS 中相通的概念。</p>
<p>顾名思义，RenderTree 在运行时是一棵树，其中的每一个节点都是一个 RenderObject 对象。这棵树的根，一般是 RenderView 对象（RenderView 继承自 RenderObject）。</p>
<h2>最简单的 Demo</h2>
<p>我们从一个最简单的 demo 开始：</p>
<pre><code class="hljs language-dart"><span class="hljs-keyword">void</span> main(){

  PipelineOwner pipelineOwner = PipelineOwner();

  RenderView rv = RenderView(configuration: ViewConfiguration(
    size: <span class="hljs-built_in">window</span>.physicalSize / <span class="hljs-built_in">window</span>.devicePixelRatio,
    devicePixelRatio: <span class="hljs-built_in">window</span>.devicePixelRatio,
  ), <span class="hljs-built_in">window</span>: <span class="hljs-built_in">window</span>);

  rv.attach(pipelineOwner);
  rv.scheduleInitialFrame();

  RenderDecoratedBox rdb = RenderDecoratedBox(
    decoration: BoxDecoration(color: Color.fromARGB(<span class="hljs-number">255</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">255</span>))
  );

  rv.child = rdb;

  <span class="hljs-built_in">window</span>.onDrawFrame = (){
    pipelineOwner.flushLayout();
    pipelineOwner.flushCompositingBits();
    pipelineOwner.flushPaint();
    rv.compositeFrame();
  };
  <span class="hljs-built_in">window</span>.scheduleFrame();
}
</code></pre><p>在这个例子中，我们创建了一个 RenderView 对象 <code>rv</code>，又创建了一个 RenderDecoratedBox 对象 <code>rdb</code>,并且把 <code>rdb</code> 设置成为 <code>rv</code> 的子节点。具体的，这里我们把 <code>rdb</code> 赋值给了 <code>rv.child</code>，这是因为 RenderView 是「只可以有一个子节点」的 RenderObject；如果遇到那些可能有多个子节点的 RenderObject，比如后面要说的 RenderStack，就需要使用 insert 等相关方法来管理子节点了；当然，还有一部分 RenderObject 是不可以有子节点的。</p>
<blockquote>
<p>在正常的 Flutter 应用中，RenderTree 由更上游的模块维护，在这个例子中，我们手动创建和管理 RenderTree。</p>
</blockquote>
<p>其实在此之前，我们还创建了一个 PipelineOwner 对象 <code>pipelineOwner</code>，这是渲染管线主对象。在创建完 RenderView 对象后，我立刻把 <code>rv</code> 挂载到了 <code>pipelineOwner</code> 对象上。由于 <code>rv</code> 是根节点，所以后续的子节点都会自动与 <code>pipelineOwner</code> 产生关联。</p>
<p><figure><img src="https://xieguanglei.github.io/blog/2021-02-16/flutterframework-yuan-ma-jie-xi-3-rendertree-gai-shu/O1CN01hUxDwj24KQSKMnoHm_!!6000000007372-2-tps-480-200.png" alt="RenderTree"><figurecaption>RenderTree</figcaption></figure></p>
<p>这是一棵最简单的 RenderTree 了。根节点 RenderView 对象 <code>rv</code> 的尺寸和屏幕一致，而 <code>rv</code> 的子节点，RenderDecoratedBox 对象 <code>rdb</code> 的尺寸也被拉伸为和 RenderView 相同，所以整个屏幕都是蓝色的。如下图所示：</p>
<p><figure><img src="https://xieguanglei.github.io/blog/2021-02-16/flutterframework-yuan-ma-jie-xi-3-rendertree-gai-shu/O1CN017BtfFi1sHQTMiOhXt_!!6000000005741-2-tps-300-633.png" alt="最简单的 Demo"><figurecaption>最简单的 Demo</figcaption></figure></p>
<blockquote>
<p>RenderView 和 RenderDecoratedBox，都继承自 RenderBox。Flutter 目前只有 RenderBox 这一种形状的 RenderObject，几乎所有的 RenderObject 对象都派生自 RenderBox（RenderObject 也许是为其他不规则形状预留的基类），所以在这篇文章里，RenderBox 和 RenderObject 基本是等价的。</p>
</blockquote>
<p>完成了 RenderTree 的构建，只是搭建好了一个数据结构。真正的渲染（包括布局、绘制、合成）是由 PipelineOwner 驱动的。</p>
<p>因此，我们在 <code>window.onDrawFrame</code> 方法（如果你看过前两篇文章，应该已经熟悉这个方法了，我这里就是把他当做类似 Web 环境中的 <code>requestAnimationFrame</code> 来使用）中手动调用 PipelineOwner 上的各个方法来驱动渲染。具体的，我们依次调用了 <code>flushLayout</code>，<code>flushCompositingBits</code>，<code>flushPaint</code> 方法，来进行布局和绘制。所谓布局，就是确定 RenderTree 每个节点的位置和尺寸；所谓绘制，就是根据 RenderTree，生成一个或多个栅格图像（在这个例子中，只有一个），用于屏幕上的显示。</p>
<h3>三部曲之一：flushLayout</h3>
<p>下面这段代码是 <code>flushLayout</code> 方法的核心逻辑：对 RenderObject 列表 <code>_nodesNeedingLayout</code> 按照深度进行排序（这个深度其实就是在 RenderTree 中节点的深度，比如这里 <code>rv</code> 的深度就是 0，<code>rdb</code> 是 1），并依次调用其中每个元素的 <code>_layoutWithoutResize</code> 方法，然后清空 <code>_nodesNeedingLayout</code>。</p>
<pre><code class="hljs language-dart"><span class="hljs-comment">// PipelineOwner#flushLayout</span>
<span class="hljs-keyword">void</span> flushLayout() {
  <span class="hljs-keyword">while</span> (_nodesNeedingLayout.isNotEmpty) {
    <span class="hljs-keyword">final</span> <span class="hljs-built_in">List</span>&lt;RenderObject&gt; dirtyNodes = _nodesNeedingLayout;
    _nodesNeedingLayout = &lt;RenderObject&gt;[];
    <span class="hljs-keyword">for</span> (RenderObject node <span class="hljs-keyword">in</span> dirtyNodes..sort(
      (RenderObject a, RenderObject b) =&gt; a.depth - b.depth)
    ) {
      <span class="hljs-keyword">if</span> (node._needsLayout &amp;&amp; node.owner == <span class="hljs-keyword">this</span>)
        node._layoutWithoutResize();
    }
  }
}
</code></pre><p><code>_nodesNeedingLayout</code> 是 PipelineOwner 的内部成员属性，表示需要重新布局的节点；同时有 <code>_nodesNeedingCompositingBitUpdate</code> 和 <code>_nodesNeedingPaint</code> 列表，后面两个小节会用到。RenderTree 初始化完成后，这三个列表中都只有一个节点，那就是根节点 <code>rv</code>。运行过程中，如果某个时候只需要更新部分节点，那么这三个列表中就可能包含若干个在其他节点。</p>
<p>在这个例子中，我们调用了 RenderView 的 <code>_layoutWithoutResize</code>。经过层层调用，最终实质调用的方法是 <code>performLayout</code> 方法。<code>performLayout</code> 是 RenderObject 留给派生类实现自身布局逻辑的方法。这个<strong>自身布局逻辑</strong>，就是 <strong>确定自己的 _size 属性（包含了 width 和 height）</strong>，所以你需要在 <code>performLayout</code> 中为更新 <code>_size</code>。RenderView 表示整个设备屏幕，所以 <code>performLayout</code> 方法逻辑就是：将自己的尺寸设置为<strong>屏幕的尺寸</strong>（也就是把 <code>configuration.size</code> 赋值给 <code>_size</code>）。<strong>然后</strong>（注意，还没有结束）命令子节点按照以下约束条件「紧贴着 RenderView 的尺寸（最大和窗口一样大，最小也和窗口一样大）」进行布局。</p>
<blockquote>
<p>BoxConstraint 是盒装布局的约束条件，包含两个矩形，一个最大矩形和一个最小矩形。当你调用一个 <code>RenderBox#layout</code> 并传入约束条件时，你期望这个 RenderObject 布局之后的尺寸，落在最大矩形和最小矩形之间。这一部分在后面在讲布局的时候会详细地讲解。</p>
</blockquote>
<pre><code class="hljs language-dart"><span class="hljs-comment">// RenderView#performLayout</span>
<span class="hljs-keyword">void</span> performLayout() {
  _size = configuration.size;
  <span class="hljs-keyword">if</span> (child != <span class="hljs-keyword">null</span>)
      child.layout(BoxConstraints.tight(_size));
}
</code></pre><p>RenderDecoratedBox 的 <code>performLayout</code> 方法由基类 RenderProxyBox 实现，逻辑是这样：如果没有子节点，就设置为约束条件的最小矩形；如果有子节点，就调用子节点的的 <code>layout</code> 并传入相同的约束条件，最后将自己的尺寸设置为子节点的尺寸，如下所示。</p>
<pre><code class="hljs language-dart"><span class="hljs-comment">// RenderProxyBox#performLayout</span>
<span class="hljs-keyword">void</span> performLayout() {
  <span class="hljs-keyword">if</span> (child != <span class="hljs-keyword">null</span>) {
    child.layout(constraints, parentUsesSize: <span class="hljs-keyword">true</span>);
    size = child.size;
  } <span class="hljs-keyword">else</span> {
    performResize();
  }
}

<span class="hljs-comment">// RenderBox#performResize：RenderProxy#performResize 由基类 RenderBox 实现</span>
<span class="hljs-keyword">void</span> performResize() {
  size = constraints.smallest;
}
</code></pre><p>这段逻辑有点绕，但没关系，布局的时候会详细讲，现在要记住的是，RenderDecoratedBox 也会调用子节点的 layout，只不过现在 <code>rdb</code> 没有子节点，所以将自身的 <code>_size</code> 设置为了 <code>constraints.smallest</code>，也就是屏幕大小。又因为我们将 RenderDecoratedBox 的颜色设置为蓝色，所以程序运行的到的结果就是，整个屏幕全部呈现为蓝色。</p>
<p>图：整个屏幕全部是蓝色。</p>
<blockquote>
<p>我们看到，调用 RenderTree 中某个节点的 <code>layout</code> 可能会逐级向下调用以这个节点为根的子树中的所有节点的 <code>layout</code>（当然这取决于派生类对 <code>performLayout</code> 的实现），所以 PipelineOwner 上的方法名是 <code>flushLayout</code>，是刷新、自上而下冲洗（就像瀑布一样）的意思。</p>
</blockquote>
<h3>三部曲之二：flushCompositingBits</h3>
<p>第二步，调用 PipelineOwner 对象的 <code>flushCompositingBits</code> 方法。这个方法和 <code>flushLayout</code> 很类似，也是先对 <code>_nodesNeedingCompositingBitsUpdate</code> 进行深度排序，然后一次调用列表中每一项的 <code>_updateCompositingBits()</code> 方法，最后清除 <code>_nodesNeedingCompositingBitsUpdate</code>。</p>
<pre><code class="hljs language-dart"><span class="hljs-comment">// PipelineOwner#flushCompositingBits</span>
<span class="hljs-keyword">void</span> flushCompositingBits() {
  _nodesNeedingCompositingBitsUpdate.sort(
    (RenderObject a, RenderObject b) =&gt; a.depth - b.depth
  );
  <span class="hljs-keyword">for</span> (RenderObject node <span class="hljs-keyword">in</span> _nodesNeedingCompositingBitsUpdate) {
    node._updateCompositingBits();
  }
  _nodesNeedingCompositingBitsUpdate.clear();
}
</code></pre><p><code>RenderObject#_updateCompositingBits()</code> 方法如下。这个方法本质上没有做实质性的事情，只是更新了一些标记属性。具体的作用我们在后面的篇幅里再讨论，现在即使不看这个方法，也对本篇内容的理解没有影响。</p>
<pre><code class="hljs language-dart"><span class="hljs-comment">// RenderObject#_updateCompositingBits</span>
<span class="hljs-keyword">void</span> _updateCompositingBits() {
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">bool</span> oldNeedsCompositing = _needsCompositing;
  _needsCompositing = <span class="hljs-keyword">false</span>;
  visitChildren((RenderObject child) {
    child._updateCompositingBits();
    <span class="hljs-keyword">if</span> (child.needsCompositing)
      _needsCompositing = <span class="hljs-keyword">true</span>;
  });
  <span class="hljs-keyword">if</span> (oldNeedsCompositing != _needsCompositing)
    markNeedsPaint();
}
</code></pre><h3>三部曲之三：flushPaint</h3>
<p>第三步，调用 PipelineOwner 对象的 <code>flushPaint</code> 方法。还是老套路，先对 <code>_nodesNeedingPaint</code> 列表按照深度进行排序，然后对其中的每一项，使用 PaintingContext 进行绘制，最后清空 <code>_nodesNeedingPaint</code>。</p>
<pre><code class="hljs language-dart"><span class="hljs-comment">// PipelineOwner#flushPaint</span>
<span class="hljs-keyword">void</span> flushPaint() {
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">List</span>&lt;RenderObject&gt; dirtyNodes = _nodesNeedingPaint;
  _nodesNeedingPaint = &lt;RenderObject&gt;[];
  <span class="hljs-keyword">for</span> (RenderObject node <span class="hljs-keyword">in</span> dirtyNodes..sort(
    (RenderObject a, RenderObject b) =&gt; b.depth - a.depth)
  ) {
    PaintingContext.repaintCompositedChild(node);
  }
}
</code></pre><p>注意，这里调用了 PaintingContext 上的一个静态方法 <code>repaintCompositedChild</code>。此方法会基于当前的 RenderObject（在本例中就是 RenderView）创建一个 PaintingContext 实例（这个概念很重要），实例内新建一个 Recorder 对象和相应的 Canvas 对象（前两篇中有讲过这两个对象的使用方法）。经过层层调用，<code>repaintCompositedChild</code> 会调用 PaintingContext实质性地调用到 RenderObject 的 <code>paint</code> 方法。同样，<code>paint</code> 方法也是 RenderObject 预留给派生类实现自身绘制逻辑的，RenderView 的 <code>paint</code> 方法，就是继续调用子节点的 <code>paint</code> 方法。</p>
<pre><code class="hljs language-dart"><span class="hljs-comment">// RenderView#paint</span>
<span class="hljs-keyword">void</span> paint(PaintingContext context, Offset offset) {
  <span class="hljs-keyword">if</span> (child != <span class="hljs-keyword">null</span>)
    context.paintChild(child, offset);
  }
}

<span class="hljs-comment">// RenderDecoratedBox#paint</span>
<span class="hljs-keyword">void</span> paint(PaintingContext context, Offset offset) {
  _painter ??= _decoration.createBoxPainter(markNeedsPaint);
  <span class="hljs-keyword">final</span> ImageConfiguration filledConfiguration = 
    configuration.copyWith(size: size);
  <span class="hljs-keyword">super</span>.paint(context, offset);
  _painter.paint(context.canvas, offset, filledConfiguration);
}
</code></pre><p>和 <code>performLayout</code> 一样，大部分有子节点的 RenderObject，基本上都会调用子 RenderObject 的 paint 方法，并将 PaintingContext 实例传递下去。经过这个过程，PaintingContext 实例会从 RenderTree 的多个节点上收集绘制操作到 Recorder 中，并在 repaintCompositedChild 的最后，收集起来，生成 picture 挂载到 layer 上。</p>
<blockquote>
<p>每一个 RenderObject 都有一个 layer 属性，至少是一个 OffsetLayer。</p>
</blockquote>
<p>最终，我们基于 RenderView#layer 创建了一个 PaintingContext，实质性的绘制发生在 RenderDecoratedBox#paint 中。</p>
<h3>最后一步：compositeBits</h3>
<p>最后一步是调用 RenderView 的 <code>compositeFrame</code> 方法，内部的代码就是前两篇中讲过的生成 SceneBuilder 和根据 layer 生成 scene 的过程。比较简单。</p>
<pre><code class="hljs language-dart"><span class="hljs-comment">// RenderView#compositeFrame</span>
<span class="hljs-keyword">void</span> compositeFrame() {
  <span class="hljs-keyword">final</span> SceneBuilder builder = SceneBuilder();
  <span class="hljs-keyword">final</span> Scene scene = layer.buildScene(builder);
  _<span class="hljs-built_in">window</span>.render(scene);
}
</code></pre><h3>总结</h3>
<p>对上面这个最简 Demo 的运行过程作一个简单的总结：</p>
<p><img src="https://xieguanglei.github.io/blog/2021-02-16/flutterframework-yuan-ma-jie-xi-3-rendertree-gai-shu/O1CN01ifA5Py1gHdGJOo0gz_!!6000000004117-2-tps-750-406.png" alt=""></p>
<ol>
<li>首先，构建以 RenderView 为根，且只有一个子节点 RenderDecoratedBox 的 RenderTree，进行一些初始化工作，保证 PipelineOwner 和 RenderTree 相连接。</li>
<li>然后，运行 PipelineOwner 的 flushLayout 方法，依次调用 RenderView 和 RenderDecoratedBox 的 layout 方法，最终确定了这两个节点的尺寸 size。</li>
<li>然后，运行 PipelineOwner 的 flushCompositingBits 方法。这个方法后面再讲，目前可以先无视。</li>
<li>然后，运行 PipelineOwner 的 flushPaint 方法，依次调用 RenderView 和 RenderDecoratedBox 的 paint 方法（只有后者的 paint 中有实质性的绘制行为）。在这个过程中，RenderDecoratedBox 的 paint 方法中对 canvas 的调用被记录到了 RenderView 的 layer 中。</li>
<li>最后，把 RenderView 的 layer 绘制到屏幕上。</li>
</ol>
<h2>局部更新 RenderTree</h2>
<p>上面说过，初始化的时候，<code>_nodesNeedingXXX</code> 中只有作为根节点的 RenderView 对象。但是，在程序运行的过程中，随着用户的输入，RenderTree 也可以发生变化，变化后可能会有一些其他节点也进入 <code>_nodesNeedingXXX</code> 中，这时候 flushXXX 方法操作的对象就只有部分节点了，这也就是<strong>局部更新</strong>。</p>
<p>下面这个例子就模拟了 RenderTree 局部更新的过程。在这个例子中，我们初始化了一个稍微复杂一点的 RenderTree。我们引入了 RenderRepaintBoundary，RenderStack 和 RenderConstrainedBox。</p>
<p><img src="https://xieguanglei.github.io/blog/2021-02-16/flutterframework-yuan-ma-jie-xi-3-rendertree-gai-shu/O1CN01g93Ahc1kJF7JjfK7o_!!6000000004662-2-tps-180-449.png" alt=""></p>
<blockquote>
<p>引入 RenderStack 和 RenderConstrainedBox 的原因是：在前一个例子中，RenderView 会强制的使用与设备屏幕完全相同的约束（约束的最大矩形和最小矩形都和设备屏幕一样，这种约束又称为 tight 类型约束）来对其子节点进行排版，不管你传入什么子节点，这个子节点本身 layout 之后的尺寸一定是和设备尺寸完全一样的。因此我们引入 RenderStack 来为下面的子节点「松绑」（loose），虽然 RenderStack 自己的尺寸被强制设定为和屏幕一样，但子节点就不必受这个约束了。这样 RenderConstrainedBox 对象 <code>rcb</code> 就能够为 RenderDecoratedBox 对象 <code>rdb</code> 重新规划尺寸了：初始化的时候设置为 <code>tight(100, 100)</code>。</p>
<p>引入 RenderRepaintBoundary 的原因是为了简单地演示 layer 合成的过程。很快就会讲到。</p>
</blockquote>
<p>在程序运行 3 秒之后，我们为 RenderConstrainedBox 对象 <code>rcb</code> 重新设定约束条件，由 <code>tight(100,100)</code> 重新设置为 <code>tight(200,200)</code>。执行程序，最初屏幕左上角是一个玫红色小方块，3 秒之后突然变成之前的 4 倍大了。看到下面这段代码，是不是有一点点 DOM 操作的感觉了？</p>
<pre><code class="hljs language-dart"><span class="hljs-keyword">void</span> main(){

  PipelineOwner pipelineOwner = PipelineOwner();

  RenderView rv = RenderView(configuration: ViewConfiguration(
    size: <span class="hljs-built_in">window</span>.physicalSize / <span class="hljs-built_in">window</span>.devicePixelRatio,
    devicePixelRatio: <span class="hljs-built_in">window</span>.devicePixelRatio,
  ), <span class="hljs-built_in">window</span>: <span class="hljs-built_in">window</span>);

  rv.attach(pipelineOwner);
  rv.scheduleInitialFrame();

  RenderRepaintBoundary rrb = RenderRepaintBoundary();

  RenderStack rs = RenderStack(textDirection: TextDirection.ltr);

  RenderConstrainedBox rcb = RenderConstrainedBox(
    additionalConstraints: BoxConstraints.tight(Size(<span class="hljs-number">100</span>, <span class="hljs-number">100</span>))
  );
  RenderDecoratedBox rdb = RenderDecoratedBox(
    decoration: BoxDecoration(color: Color.fromARGB(<span class="hljs-number">255</span>, <span class="hljs-number">255</span>, <span class="hljs-number">0</span>, <span class="hljs-number">255</span>))
  );

  rv.child = rrb;
  rrb.child = rs;
  rs.insert(rcb);
  rcb.child = rdb;

  <span class="hljs-built_in">window</span>.onDrawFrame = (){
    pipelineOwner.flushLayout();
    pipelineOwner.flushCompositingBits();
    pipelineOwner.flushPaint();
    rv.compositeFrame();
  };
  <span class="hljs-built_in">window</span>.scheduleFrame();

  <span class="hljs-keyword">new</span> Future.delayed(<span class="hljs-keyword">const</span> <span class="hljs-built_in">Duration</span>(milliseconds: <span class="hljs-number">3000</span>), (){
    rcb.additionalConstraints = 
      BoxConstraints.tight(Size(<span class="hljs-number">200</span>, <span class="hljs-number">200</span>));
    <span class="hljs-built_in">window</span>.scheduleFrame();
  });
}
</code></pre><p><figure><img src="https://xieguanglei.github.io/blog/2021-02-16/flutterframework-yuan-ma-jie-xi-3-rendertree-gai-shu/O1CN01klaxof2134bTCC5Tc_!!6000000006928-2-tps-741-634.png" alt="动态更新RenderTree"><figurecaption>动态更新RenderTree</figcaption></figure></p>
<p>3 秒之后，当为 <code>rcb.additionalConstraints</code> 的这条语句还没有执行的时候，PipelineOwner 是内部是干净的：<code>_nodesNeedingXXX</code> 全部是空数组。当我们为 <code>rcb.additionalConstraints</code> 赋值的时候，触发 <code>additionalConstraints</code> 这个 setter，在其中调用 <code>RenderObject#markNeedsLayout</code> 方法，将 RenderStack 添加到了 <code>_nodesNeedingLayout</code> 中。</p>
<blockquote>
<p>为什么 <code>rcb</code> 上的 setter 会把 <code>rcb</code> 的 parent <code>rs</code> 而不是它自己放到 <code>_nodesNeedingLayout</code> 中呢？这和 <code>_relayoutBoundary</code> 有关，简单地说，每一个 RenderObject 都有一个排版边界 <code>layoutBoundary</code>，可能是自己，也可能是自己的父节点或父节点的父节点；这个排版边界表达的意思是：如果节点变化了，那么该从哪儿重新开始排版 —— 显然，如果某个节点的祖先节点的尺寸依赖了你的尺寸，那么这个节点变化后，祖先节点也得重新排版（这又引入另一个概念 parentUseSize）。这些在后续有关排版的篇幅里会详细讲。</p>
<p>所以，<code>markXXX</code> 往往会向上追溯祖先节点的。</p>
</blockquote>
<pre><code class="hljs language-dart"><span class="hljs-keyword">void</span> markNeedsLayout() {
  <span class="hljs-keyword">if</span> (_relayoutBoundary != <span class="hljs-keyword">this</span>) {
   markParentNeedsLayout();
  } <span class="hljs-keyword">else</span> {
   _needsLayout = <span class="hljs-keyword">true</span>;
   owner._nodesNeedingLayout.add(<span class="hljs-keyword">this</span>);
  }
}
</code></pre><p>接下来，我们调用 <code>window.scheduleFrame</code>，这会在下一帧调用 <code>window.onDrawFrame</code>，又进入到三部曲的流程中。</p>
<p>首先，执行 <code>flushLayout</code>，对 RenderStack 进行排版。排版完成后，在每次布局都会走到的 RenderObject#layout 公共方法中，对调用在 RenderStack 上调用 markNeedsPaint 方法，把 RenderRepaintBoundary 对象放到 <code>_nodesNeedingPaint</code> 中。</p>
<blockquote>
<p>为什么在 <code>rs</code> 上调用 <code>markNeedsPaint</code> 会把 RenderRepaintBoundary 对象 <code>rrb</code> 而不是 <code>rs</code> 自己放到 <code>_nodesNeedPaint</code> 中来呢？其实，和 <code>markNeedsLayout</code> 很像，<code>markNeedsPaint</code> 也会向上追溯祖先节点，直到 <code>_isRepaintBoundary</code> 为 <code>true</code> 的祖先节点（这里就是 RenderRepaintBoundary）。为了简化开销，我们会尽量把相同时机更新的内容分层绘制，然后合并以提高渲染性能。但是分层和合成本身也有开销。RenderRepaintBoundary 其实就是「用来对应一层的」RenderObject，背后对应的就是 PictureLayer。RenderRepaintBoundary 出现在哪里是人为指定，后面也会详细讲。</p>
</blockquote>
<p>接下来就是 <code>flushPaint</code> 了。这时候 <code>_nodesNeedingPaint</code> 中只有有一个元素 <code>rrb</code>，那么会基于 <code>rrb</code> 的 layer 生成一个 PaintingContext 实例来绘制，绘制的内容全部存储在 <code>rrb</code> 的 layer 中。</p>
<p>最后就是调用 RenderView 的 <code>compositeBits</code>，把 <code>rv</code> 的 layer 绘制到屏幕上去。你可能会问，我们重绘明明是在 <code>rrb</code> 上进行的，为什么还是把 <code>rv</code> 的 layer 绘制到屏幕上呢？其实，在 PaintingContext 对象的 <code>paintChild</code> 方法中，有一个 <code>appendLayer</code>（所以直到这里，才和前一篇中 layer 的操作联系起来了）。也就是说，在 3 秒前第一次 flushPaint 的时候，<code>rrb</code> 的 layer 就已经是 <code>rv</code> 的 layer 的 child 了。</p>
<pre><code class="hljs language-dart"><span class="hljs-comment">// PaintingContext#paintChild</span>
<span class="hljs-keyword">void</span> paintChild(RenderObject child, Offset offset) {
 <span class="hljs-keyword">if</span> (child.isRepaintBoundary) {
   stopRecordingIfNeeded();
   _compositeChild(child, offset);
 } <span class="hljs-keyword">else</span> {
   child._paintWithContext(<span class="hljs-keyword">this</span>, offset);
 }
}

<span class="hljs-comment">// PaintingContext#_compositeChild</span>
<span class="hljs-keyword">void</span> _compositeChild(RenderObject child, Offset offset) {
 child._layer.offset = offset;
 appendLayer(child._layer);
}
</code></pre><blockquote>
<p>Layer 关系的解除也在 PaintingContext 中，具体的，在 <code>_repaintCompositedChild</code> 方法中有一句 <code>child._layer.removeAllChildren()</code> 的调用。</p>
</blockquote>
<p>最后对这个例子简单地总结一下：</p>
<p><figure><img src="https://xieguanglei.github.io/blog/2021-02-16/flutterframework-yuan-ma-jie-xi-3-rendertree-gai-shu/O1CN01xQ6IPs1wboZKGYJNF_!!6000000006327-2-tps-750-927.png" alt="动态更新RenderTree"><figurecaption>动态更新RenderTree</figcaption></figure></p>
<ol>
<li>初始化 RenderTree 并进行第一次渲染，与前一个例子的步骤一致。</li>
<li>值得注意的是，在第一次渲染的 <code>flushPaint</code> 过程中，我们把 RenderRepaintBoundary 对象 <code>rrb</code> 的 layer 追加（append）到了 RenderView 的 layer 的子 layer 中。</li>
<li>3 秒后，更新 <code>rcb.additionalConstraints</code>，通过 setter 调用 <code>markNeedsLayout</code>，将 <code>rs</code> 添加到 <code>_nodesNeedingLayout</code> 中。</li>
<li>再次执行 <code>onDrawFrame</code>，首先调用 <code>flushLayout</code>；在 <code>rs</code> 执行 <code>layout</code> 方法过程中，调用 <code>markNeedsPaint</code>，将 <code>rrb</code> 添加到 <code>_nodesNeedingPaint</code> 中。</li>
<li>接着调用 <code>flushPaint</code>，此时 <code>_nodesNeedingPaint</code> 只有 <code>rrb</code> 一个元素，所以这里实际上是对根为 <code>rrb</code> 的子树进行重绘。重绘过程与第一个 demo 中一致，实质上还是绘图命令还是从 <code>rdb</code> 中收集到的。重绘后的结果保存在 <code>rrb</code> 的 layer 上。</li>
<li>最后把 <code>rv</code> 的 layer 绘制到屏幕上。因为之前的 <code>rrb</code> 的 layer 已经是 <code>rv</code> 的 layer 的子 layer 了，所以这一步就把更新后的结果也绘制到了屏幕上。</li>
</ol>
<p>这一篇就先讲这么多吧。</p>
]]></description>
            <link>https://xieguanglei.github.io/blog/2021-02-16/flutterframework-yuan-ma-jie-xi-3-rendertree-gai-shu</link>
            <guid isPermaLink="true">https://xieguanglei.github.io/blog/2021-02-16/flutterframework-yuan-ma-jie-xi-3-rendertree-gai-shu</guid>
            <pubDate>Tue, 16 Feb 2021 00:00:00 GMT</pubDate>
        </item>
    </channel>
</rss>