尽管一个用于人类交流,一个服务于机器指令,自然语言和编程语言却共享着相似的基本原理。两者均由语法规则、结构限制和组合方式所定义。
举例来说,日语以其严(luo)谨(suo)的语法规则和丰富的变形系统而闻名,而 TypeScript 的类型系统则因其表达能力和灵活性而受到推崇。当我们将这两个领域结合起来,会发生什么呢?本文将探索如何利用 TypeScript 的高级泛型编程来建模日语语法结构,让编程语言成为辅助语言学习的新工具。
由于全文较长,因此这里先预告几个后文中将演示的日语例句:
- 疑问句「为什么要演奏春日影?」
- 条件句「辛美尔的话也会这么做的」
- 组合句「好时代,来临啦」
下面,让我们从基础知识开始循序渐进吧。
从命令式编程到类型编程
在深入日语语法之前,我们可以先通过一个简单的英语动词变形例子,来理解如何将常规的 JavaScript 函数转换为 TypeScript 类型级别的实现。
首先,下面是一个简单的 JavaScript 函数,它根据第二个参数决定给英语动词添加 "ed"
还是 "ing"
:
javascript
function conjugateEnglishVerb(verb, form) {
if (form === "past") {
return verb + "ed";
} else if (form === "progressive") {
return verb + "ing";
} else {
return verb;
}
}
// 使用示例
conjugateEnglishVerb("walk", "past"); // "walked"
conjugateEnglishVerb("talk", "progressive"); // "talking"
这个函数在运行时接收参数并返回结果。这是典型的命令式编程方法,即通过一系列明确的指令告诉计算机「如何」执行任务。现在,让我们将这个逻辑转换到 TypeScript 的类型系统中:
typescript
// 定义可能的变形形式
type EnglishVerbForm = "base" | "past" | "progressive";
// 类型级别的动词变形函数
type ConjugateEnglishVerb<
Verb extends string,
Form extends EnglishVerbForm
> = Form extends "past"
? `${Verb}ed`
: Form extends "progressive"
? `${Verb}ing`
: Verb;
// 类型级别的使用示例
type WalkedVerb = ConjugateEnglishVerb<"walk", "past">; // 结果: "walked"
type TalkingVerb = ConjugateEnglishVerb<"talk", "progressive">; // 结果: "talking"
注意这里的关键转换:
- 函数参数变成了泛型参数
<Verb extends string, Form extends EnglishVerbForm>
if/else
条件语句变成了条件类型Form extends "past" ? ... : ...
- 字符串拼接
verb + "ed"
变成了模板字符串类型${Verb}ed
- 函数调用变成了类型声明和实例化
这个简单的例子用到了 TypeScript 类型系统中的三个核心工具:
- 泛型:用于参数化类型
- 条件类型:用于根据条件选择不同的类型
- 模板字符串类型:用于在类型级别操作字符串
对比这两个例子可以感受到,声明式的类型编程相比命令式编程,在与计算机沟通的方式上有根本区别:
- 命令式编程告诉计算机「如何做」,即通过一系列明确的步骤执行任务。
- 而类型编程则是在描述「是什么」,即声明类型之间的关系和约束。我们不再发出指令序列,而是构建一个类型关系网络,让编译器在编译时自行推导结果。
这种范式转变使我们能够在程序运行前就验证复杂规则的正确性,将运行时错误转变为编译时错误,非常适合建模人类语法这种(纸面上)具有严格结构约束的系统。
扩展语法规则特例
当然,实际的语言规则要比上面的简单变换复杂得多,存在着许多人为规定的特例。让我们稍微扩展英语动词变形的例子,考虑一些特殊规则:
typescript
// 更复杂的英语动词变形
type ConjugateEnglishVerbAdvanced<
Verb extends string,
Form extends EnglishVerbForm
> = Form extends "past"
? Verb extends `${infer Base}e`
? `${Base}ed` // 如果以 e 结尾,只加 d
: Verb extends `${infer Base}y`
? `${Base}ied` // 如果以 y 结尾,变 y 为 ied
: `${Verb}ed`
: Form extends "progressive"
? Verb extends `${infer Base}e`
? `${Base}ing` // 如果以 e 结尾,去掉 e 再加 ing
: `${Verb}ing`
: Verb;
// 示例
type MovedVerb = ConjugateEnglishVerbAdvanced<"move", "past">; // "moved",不是 "moveed"
type CriedVerb = ConjugateEnglishVerbAdvanced<"cry", "past">; // "cried",不是 "cryed"
type MovingVerb = ConjugateEnglishVerbAdvanced<"move", "progressive">; // "moving",不是 "moveing"
这里我们使用了 infer
关键字来从类型表达式(即所谓的「模式」)中抽取出特定的片段,并在结果中重用它。这就是类型系统中名为模式匹配的强大工具。
日语动词变形基础:了解五段动词
对于没有接触过日语的读者,这里先简单介绍一下日语动词变形的基本概念。
在日语中,动词会根据时态、语气、礼貌程度等因素发生变形。这与英语中的 "walk/walked/walking" 类似,但日语的变形系统更加系统化且规则丰富。
日语的动词主要分为三类:五段动词、一段动词和不规则动词。其中,五段动词是最常见也是变形规则最复杂的一种。所谓的「五段」来源于传统日语语法中的元音行变化(あ、い、う、え、お五行),意味着这类动词在变形时词尾会在这五个行中变化。
以「買う」(买)这个五段动词为例,它在几种不同形式下的变化如下:
- 基本形(字典形):買う (kau)
- て形(连接形):買って (katte)
- た形(过去式):買った (katta)
- ない形(否定形):買わない (kawanai)
- 命令形:買え (kae)
如果用 JavaScript 函数来实现这种变形逻辑,大致会是这样:
javascript
function conjugateGodanVerb(stem, ending, form) {
if (form === "て形") {
if (ending === "う" || ending === "つ" || ending === "る") {
return stem + "って";
} else if (ending === "く") {
return stem + "いて";
} else if (ending === "ぐ") {
return stem + "いで";
} else if (ending === "す") {
return stem + "して";
} else if (ending === "ぬ" || ending === "ぶ" || ending === "む") {
return stem + "んで";
}
} else if (form === "た形") {
if (ending === "う" || ending === "つ" || ending === "る") {
return stem + "った";
} else if (ending === "く") {
return stem + "いた";
} else if (ending === "ぐ") {
return stem + "いだ";
} else if (ending === "す") {
return stem + "した";
} else if (ending === "ぬ" || ending === "ぶ" || ending === "む") {
return stem + "んだ";
}
}
// 其他形式的变形...
return stem + ending; // 默认返回字典形
}
// 使用示例
console.log(conjugateGodanVerb("買", "う", "て形")); // "買って"
console.log(conjugateGodanVerb("書", "く", "た形")); // "書いた"
这个函数展示了五段动词变形的基本逻辑:根据词尾(ending
)和目标形式(form
)选择相应的变形规则。虽然它确实能处理五段动词的变形,但这种命令式的 if-else
实现有一些明显的缺点:
- 首先,它只能在运行时检查并处理错误,无法在编译阶段提供语法正确性保证。
- 其次,这种函数难以与更大的语言结构系统集成,缺乏组合性。
- 最后,随着规则增加,嵌套的条件判断会导致代码复杂度急剧上升,维护困难。
相比之下,TypeScript 的类型系统提供了一种声明式的方法,能够在编译期捕获错误,并以更优雅、更具表达力的方式描述语言规则间的关系。通过将这些变形规则提升到类型级别,我们可以构建一个能静态验证日语语法正确性的系统,这正是下文中将要展示的方法。
使用类型编程实现日语动词变形
有了上面这些铺垫,我们现在可以开始构建日语动词变形的类型系统了。我们先从简单的例子开始:
typescript
// 定义简化的日语动词类型
type SimpleJapaneseVerb = {
type: "godan" | "ichidan";
stem: string;
ending: string;
};
// 定义变形形式
type JapaneseVerbForm = "辞書形" | "て形" | "た形";
// 简化的日语动词变形
type ConjugateSimpleJapaneseVerb<
V extends SimpleJapaneseVerb,
Form extends JapaneseVerbForm
> = Form extends "辞書形"
? `${V["stem"]}${V["ending"]}`
: Form extends "て形"
? V["type"] extends "godan"
? V["ending"] extends "う"
? `${V["stem"]}って`
: V["ending"] extends "く"
? `${V["stem"]}いて`
: `${V["stem"]}${V["ending"]}`
: V["type"] extends "ichidan"
? `${V["stem"]}て`
: `${V["stem"]}${V["ending"]}`
: Form extends "た形"
? V["type"] extends "godan"
? V["ending"] extends "う"
? `${V["stem"]}った`
: V["ending"] extends "く"
? `${V["stem"]}いた`
: `${V["stem"]}${V["ending"]}`
: V["type"] extends "ichidan"
? `${V["stem"]}た`
: `${V["stem"]}${V["ending"]}`
: `${V["stem"]}${V["ending"]}`;
// 示例
type KauVerb = { type: "godan"; stem: "買"; ending: "う" };
type KauTeForm = ConjugateSimpleJapaneseVerb<KauVerb, "て形">; // "買って"
type KauTaForm = ConjugateSimpleJapaneseVerb<KauVerb, "た形">; // "買った"
type TaberuVerb = { type: "ichidan"; stem: "食べ"; ending: "る" };
type TaberuTeForm = ConjugateSimpleJapaneseVerb<TaberuVerb, "て形">; // "食べて"
type TaberuTaForm = ConjugateSimpleJapaneseVerb<TaberuVerb, "た形">; // "食べた"
现在,我们可以扩展这个模型,以更精确地反映日语动词的分类和变形规则。我们将使用接口继承来表示不同类型的动词及其特性:
typescript
// 动词基础接口
interface Verb {
dictionary: string;
}
// 五段动词
interface GodanVerb extends Verb {
stem: string;
ending: "う" | "く" | "ぐ" | "す" | "つ" | "ぬ" | "ぶ" | "む" | "る";
}
// 一段动词
interface IchidanVerb extends Verb {
stem: string;
ending: "る";
}
// 不规则动词
interface IrregularVerb extends Verb {
dictionary: "する" | "来る";
}
// 定义变形形式
type ConjugationForm = "て形" | "た形" | "ない形" | "辞書形" | "命令形";
// 更完整的日语动词变形系统
type ConjugateVerb<
T extends Verb,
Form extends ConjugationForm
> = T extends GodanVerb
? GodanConjugation<T, Form>
: T extends IchidanVerb
? IchidanConjugation<T, Form>
: T extends IrregularVerb
? IrregularConjugation<T, Form>
: never;
// 示例
type 買う = GodanVerb & { stem: "買"; ending: "う" };
type 買うて形 = ConjugateVerb<買う, "て形">; // 買って
type 買うた形 = ConjugateVerb<買う, "た形">; // 買った
type 食べる = IchidanVerb & { stem: "食べ"; ending: "る" };
type 食べるて形 = ConjugateVerb<食べる, "て形">; // 食べて
type 食べるた形 = ConjugateVerb<食べる, "た形">; // 食べた
这种方法展示了 TypeScript 类型系统的强大表达能力。通过使用泛型、条件类型和模板字符串类型,我们可以在编译时验证和转换日语动词,就像在运行时使用函数一样。
在接下来的部分中,我们将继续使用这些基本能力来模拟更复杂的日语句型结构,如疑问句和条件句。
疑问句体操:为什么要演奏春日影?
日语中的疑问句通常由疑问词(如なんで、なぜ、どうして)和句尾的疑问助词(如の、か)构成。观察长崎爽世女士的名言「なんで春日影やったの」,我们可以识别出其结构组成:
- なんで(为什么)- 疑问副词
- 春日影(春日影)- 句子的宾语
- やった(演奏了)- 动词「やる」的过去式形式
- の - 表示疑问的助词
这种结构在日语中非常常见,我们可以抽象出一个通用模式:[疑问词] + [主题/宾语] + [动词变形] + [疑问助词]
。为了在类型系统中表示这种模式,我们需要一个专门的泛型类型 InterrogativePhrase
。
typescript
// 按类别分类的疑问词
type WhyInterrogative = "なぜ" | "なんで" | "どうして";
type WhenInterrogative = "いつ";
type WhereInterrogative = "どこ";
type WhoInterrogative = "だれ" | "誰";
type WhatInterrogative = "何" | "なに";
type HowInterrogative = "どう" | "どうして";
type WhatKindInterrogative = "どんな";
type WhichInterrogative = "どれ";
// 疑问副词类型
type InterrogativeAdverb =
| WhyInterrogative
| WhenInterrogative
| WhereInterrogative
| WhoInterrogative
| WhatInterrogative
| HowInterrogative
| WhatKindInterrogative
| WhichInterrogative;
type InterrogativePhrase<
Adv extends InterrogativeAdverb,
Subject extends string,
V extends Verb,
VForm extends ConjugationForm,
QP extends Particle = "か" // 疑问句一般默认以か为助词,可覆写
> = `${Adv}${Subject}${ConjugateVerb<V, VForm>}${QP}`;
这个泛型类型可以接收疑问副词、主题(或宾语)、动词、动词形式和疑问助词作为参数,然后将它们按照日语语法规则组合成一个完整的疑问句类型。
现在,让我们看看如何使用这个类型系统来表示「なんで春日影やったの」这句话:
typescript
// 定义动词"やる"(做/演奏)
type やる = GodanVerb & { stem: "や"; ending: "る" };
// 定义专有名词"春日影"
type 春日影 = ProperNoun<"春日影">;
// 构建完整的疑问句
type なんで春日影やったの = InterrogativePhrase<
WhyInterrogative, // 疑问词"なんで"(为什么)
春日影, // 宾语"春日影"
やる, // 动词"やる"
"た形", // 动词以过去式形式出现
"の" // 疑问助词"の"
>;
// 类型检查示例
const validQuestion: なんで春日影やったの = "なんで春日影やったの"; // "为什么要演奏春日影?"
通过这种方式,TypeScript 的类型系统不仅可以表示日语疑问句的结构,还能在编译时验证句子的语法正确性。这展示了类型系统能如何帮助我们学习和掌握日语语法规则。
条件句体操:辛美尔的话也会这么做的
日语中的条件句表达假设或者条件关系,有多种表达方式,如なら、たら、れば、と等。在这些条件表达中,「なら」常用于表示「如果是...的话」,通常接在名词或名词短语之后。
让我们分析芙莉莲女士的名言「ヒンメルならそうした」这个条件句:
- ヒンメル - 人名「辛美尔」,作为条件的主体
- なら - 条件助词,表示「如果是...的话」
- そう - 指示副词,表示「那样」
- した - 动词"する"(做)的过去时态
这个句子的结构可以概括为:[条件主体] + [条件助词なら] + [结果]
,其中结果部分是「そうした」(那样做了)。
为了在类型系统中表示这种结构,我们需要使用前面定义的几个关键类型:
typescript
// 条件助词类型
type ConditionalParticle = "なら" | "たら" | "れば" | "と";
// 指示词类型(需要补充定义)
type Demonstrative = "こう" | "そう" | "ああ" | "どう";
// 条件句结构
type ConditionalPhrase<
Subject extends string,
CP extends ConditionalParticle,
Result extends string
> = `${Subject}${CP}${Result}`;
// 指示动作组合
type DemonstrativeAction<
Demo extends string,
V extends Verb,
F extends ConjugationForm = "辞書形"
> = `${Demo}${ConjugateVerb<V, F>}`;
现在,让我们使用这些类型来表示「ヒンメルならそうした」:
typescript
// 定义专有名词"ヒンメル"
type ヒンメル = ProperNoun<"ヒンメル">;
// 定义する动词
type する = IrregularVerb & { dictionary: "する" };
// 创建そうした模式(そうする的过去形)
type そうした = DemonstrativeAction<Demonstrative & "そう", する, "た形">;
// 创建条件句"ヒンメルならそうした"
type ヒンメルならそうした = ConditionalPhrase<ヒンメル, "なら", そうした>;
// 类型检查示例
const properExample: ヒンメルならそうした = "ヒンメルならそうした"; // "辛美尔的话也会这么做的"
// const wrongExample: ヒンメルならそうした = "ヒンメルならそうする"; // 错误:动词形式不匹配
这个例子展示了如何使用类型系统精确地表示日语条件句结构,并在编译时检查句子的语法正确性。
组合句体操:好时代,来临啦!
日语中常见的一种表达方式是通过逗号连接多个简短的语句,形成一个富有节奏感的组合句。这些短句各自独立,但组合起来传达一个连贯的意思。让我们分析田所浩二先辈的名言「いいよ、来いよ」,它由三个部分组成:
- いいよ - 由形容词「いい」(好)和强调助词「よ」组成
- 日语逗号,用于分隔短句
- 来いよ - 由动词「来る」(来)的命令形「来い」和强调助词「よ」组成
这个句子结构可以概括为:[短句1]、[短句2]
,其中每个短句都可以有不同的语法结构。
为了在类型系统中表示这种组合句结构,我们可以使用以下类型:
typescript
// 带助词的短语
type PhraseWithParticle<
Phrase extends string,
P extends Particle
> = `${Phrase}${P}`;
// 通过日语逗号连接的短语
type ConnectedPhrases<P1 extends string, P2 extends string> = `${P1}、${P2}`;
而对于形容词和动词的变形,只需使用前面定义的变形系统即可:
typescript
// 定义い形容词"いい"(好的),注意它是不规则变形
type いい = IAdjective & { stem: "い"; ending: "い"; irregular: true };
type いいよ = PhraseWithParticle<ConjugateAdjective<いい, "基本形">, "よ">;
// 定义不规则动词"来る"(来)
type 来る = IrregularVerb & { dictionary: "来る" };
type 来いよ = PhraseWithParticle<ConjugateVerb<来る, "命令形">, "よ">;
// 连接两个短句 -> "いいよ、来いよ"
type いいよ来いよ = ConnectedPhrases<いいよ, 来いよ>;
// 类型检查示例
const correctPhrase1: いいよ = "いいよ"; // "真好啊!"
const correctPhrase2: 来いよ = "来いよ"; // "快来吧!"
const correctFullPhrase: いいよ来いよ = "いいよ、来いよ"; // "好时代,来临啦!"
// const incorrectPhrase: いいよ来いよ = "いいよ、くるよ"; // 错误:动词形式不匹配
这样,我们就可以用类型系统表示出由多个独立短句组成的组合句了。
总结与展望
本文通过 TypeScript 的高级类型系统探索了日语语法结构。我们从基础的动词变形开始,逐步构建了能够表示疑问句、条件句和组合句的类型系统。
传统上,这种用复杂类型系统来标注自然语言的代码编写成本极高,基本只能用来炫技。然而,现在的情况已经发生了根本性的变化------只要类型系统设计合理,LLM 已经可以为几乎任何自然语言准确地标注出 TypeScript 类型。这意味着普通人哪怕不懂 TypeScript,也能在学习语言时获得来自 LLM 的强类型提示。更有趣的是,如果你会写 TypeScript,你实际上就可以解读几乎任何一门自然语言的语法!
值得澄清的是,这个项目的范围严格限定在单向地通过 TypeScript 类型系统标注句子,而不涉及将任意字符串 parse 回语法类型------那是 LLM 更擅长的工作。所有纸质语言教材中的例句都是弱类型的,而这种方法可以为学习者提供强类型的指导。
这种借助 TypeScript 类型系统帮助学习语言的方法论,几乎可以推广到所有的自然语言。基于这一理念,我在 GitHub 上建立了 TypedGrammar 组织。目前的日语类型系统代码已经开源在 github.com/typedgramma... 。虽然目前还只是一个技术原型,但要完善到对普通人的语言学习产生实际价值,基本是一个考验耐心的 scalable 的工作。希望后续能一边实践一边分享,与感兴趣的朋友们共同学习进步。后续希望能持续分享相关进展,欢迎感兴趣的读者关注支持,一同探索类型编程与语言学习的交叉领域。