内容
简介
Tracery是一个内容生成库,最初由@GalaxyKate创建;您可以在Tracery.io上找到更多信息
这个实现虽然受到原始设计的极大启发,但增加了更多功能。
Tracery的内容生成基于一套输入规则。规则决定了如何生成内容。
安装
- 克隆或下载此仓库
- 要使用playground,请打开Playgrounds/Tracery.playground
- 此项目构建iOS和macOS框架目标,可以链接到您的项目
家庭使用方法
import Tracery
// create a new Tracery engine
var t = Tracery {[
"msg" : "hello world"
]}
t.expand("well #msg#")
// output: well hello world
我们创建一个Tracery引擎的实例,同时传入一个规则字典。这个字典的键是规则名称,而每个键的值则代表该规则的扩展内容。
然后,我们使用Tracery来扩展指定的规则实例。
请注意,我们提供的输入是一个模板字符串,它包含#msg#
,这是我们希望在#
标记内扩展的规则。Tracery评估模板,识别规则,并用其扩展内容替换它。
我们可以有多个规则。
t = Tracery {[
"name": "jack",
"age": "10",
"msg": "#name# is #age# years old",
]}
t.expand("#msg#") // jack is 10 years old
注意,我们指定了扩展#msg#
,这会触发扩展#name#
和#age#
规则?Tracery能够递归扩展规则,直到无法再进一步扩展。
一个规则可以有多个候选扩展。
t = Tracery {[
"name": ["jack", "john", "jacob"], // we can specify multiple values here
"age": "10",
"msg": "#name# is #age# years old",
]}
t.expand("#msg#")
// jacob is 10 years old <- name is randomly picked
t.expand("#msg#")
// jack is 10 years old <- name is randomly picked
t.expand("#name# #name#")
// will print out two different random names
在上面的示例中,每当Tracery看到规则#name#
时,它都会选择其中之一候选值;在本例中,名字可以是“jack”、“john”或“jacob”。
这就是内容生成所需。通过为每个规则指定不同的候选值,每次调用expand
都可以得到不同的结果。
现在让我们尝试基于一首流行的儿歌创建一个句子。
t = Tracery {[
"boy" : ["jack", "john"],
"girl" : ["jill", "jenny"],
"sentence": "#boy# and #girl# went up the hill."
]}
t.expand("#sentence#")
// output: john and jenny went up the hill
所以,我们得到句子的第一部分,如果我们想要添加第二行,使得最终输出为
"john和jenny上山去了,john跌倒了,jenny也跟着跌倒了"
// the following will fail
// to produce the correct output
t.expand("#boy# and #girl# went up the hill, #boy# fell down, and so did #girl#")
// sample output:
// jack and jenny went up the hill, john fell down, and so did jill
问题在于,任何出现的#rule#
都会被其候选值之一替换。因此,当我们写两次#boy#
时,它可能会被替换成完全不同的名字。
为了记住值,我们可以使用标签。
标签
标签允许将规则扩展的结果持久化到临时变量中。
t = Tracery {[
"boy" : ["jack", "john"],
"girl" : ["jill", "jenny"],
"sentence": "[b:#boy#][g:#girl#] #b# and #g# went up the hill, #b# fell down, and so did #g#"
]}
t.expand("#sentence#")
// output: jack and jill went up the hill, jack fell down, and so did jill
标签使用格式[tagName:tagValue]
创建。在上面的示例中,我们首先创建两个标签,`b`和`g`,来存储`boy`和`girl`名字的值。稍后,我们可以使用`#b#`和`#g#`,就像它们是新的规则一样,Tracery将根据需要替换它们的存储值。
标签也可以简单地包含一个值或一组值。标签也可以出现在`#rules#`内。标签是可变的,可以设置任意次数。
简单故事
下面是一个更复杂的例子,它生成一个简短的故事。
t = Tracery {[
"name": ["Arjun","Yuuma","Darcy","Mia","Chiaki","Izzi","Azra","Lina"],
"animal": ["unicorn","raven","sparrow","scorpion","coyote","eagle","owl","lizard","zebra","duck","kitten"],
"mood": ["vexed","indignant","impassioned","wistful","astute","courteous"],
"story": ["#hero# traveled with her pet #heroPet#. #hero# was never #mood#, for the #heroPet# was always too #mood#."],
"origin": ["#[hero:#name#][heroPet:#animal#]story#"]
]}
t.expand("#origin#")
// sample output:
// Darcy traveled with her pet unicorn. Darcy was never vexed, for the unicorn was always too indignant.
随机数字
下面是生成随机数字的另一个例子
t.expand("[d:0,1,2,3,4,5,6,7,8,9] random 5-digit number: #d##d##d##d##d#")
// sample output:
// random 5-digit number: 68233
在
如果一个标签名称与规则匹配,则该标签将优先,并且总是被计算。
现在我们熟悉了这些内容,我们将讨论规则修饰符。
修饰符
在扩展规则时,有时我们可能需要将其输出大写或以某种方式转换。Tracery 引擎允许定义规则扩展。
一种规则扩展称为修饰符。
import Tracery
var t = Tracery {[
"city": "new york"
]}
// add a bunch of modifiers
t.add(modifier: "caps") { return $0.uppercased() }
t.add(modifier: "title") { return $0.capitalized }
t.add(modifier: "reverse") { return String($0.characters.reversed()) }
t.expand("#city.caps#")
// output: NEW YORK
t.expand("#city.title#")
// output: New York
t.expand("#city.reverse#")
// output: kroy wen
修饰符的强大之处在于它们可以链接。
t.expand("#city.reverse.caps#")
// output: KROY WREN
t.expand("There once was a man named #city.reverse.title#, who came from the city of #city.title#.")
// output: There once was a man named Kroy Wen, who came from the city of New York.
在Tracery.io的原始实现中,有几个修饰符,允许给单词添加前缀a和an、复数、大写等。该库采用了另一种方法,并提供自定义端点,以便可以添加所需的任何修饰符。
下一个规则扩展选项是添加自定义规则方法。
方法
与修饰符不同,修饰符接收规则当前候选值的输入,方法可以用来定义可以接受参数的修饰符。
方法与修饰符的编写和调用方式相同。
import Tracery
var t = Tracery {[
"name": ["Jack Hamilton", "Manny D'souza", "Rihan Khan"]
]}
t.add(method: "prefix") { input, args in
return args[0] + input
}
t.add(method: "suffix") { input, args in
return input + args[0]
}
t.expand("#name.prefix(Mr. )#") // Mr. Jack Hamilton
像修饰符一样,它们可以链式调用。事实上,任何类型的规则扩展都可以链式调用。
t.expand("#name.prefix(Mr. ).suffix( woke up tired.)#") // Mr. Rihan Khan woke up tired.
方法的力量来源于这样一个事实,即方法的参数本身可以是规则(或标签)。Tracery将扩展这些,并将正确的值传递给方法。
t = Tracery {[
"count": [1,2,3,4],
"name": ["jack", "joy", "jason"]
]}
t.add(method: "repeat") { input, args in
let count = Int(args[0]) ?? 1
return String(repeating: input, count: count)
}
// repeat a randomly selected name, a random number of times
t.expand("#name.repeat(#count#)")
// repeat a tag's value 3 times
t.expand("[name:benzi]#name.repeat(3)#")
注意我们如何创建一个名为
name
的标签,该标签重写了name
规则。标签始终比规则优先。
调用
还有一种类型的规则扩展,称为调用
。与修饰符和方法不同,它们不处理参数、预期的返回值是字符串,调用不需要这些。
调用与修饰符的语法相同 #rule.call_something#
,只是它们不修改任何结果。
为了展示调用是如何工作的,我们将创建一个跟踪规则扩展的调用。
import Tracery
var t = Tracery {[
"f": "f"
"letter" : ["a", "b", "c", "d", "e", "#f.track#"]
]}
t.add(call: "track") {
print("rule 'f' was expanded")
}
t.expand("#letter#")
在上面的代码片段中,规则字母有5个候选项,其中4个基本上是字符串值,但第五个是规则。是的,规则可以自由混合,并且可以出现在任何地方。因此,在这个例子中,规则f
可以展开成基本的字符串f
。注意我们还添加了跟踪调用。
现在,每当letter
将规则f
作为扩展的候选时,都会调用.track
。
规则扩展可以放在一对
#
内。例如,如果我们创建了一个总是将其输入添加到'y'的修饰符,命名为yo
,并且有一个规则候选#.yo#
,则该表达式计算为字符串"yo";因为没有任何要展开的规则,修饰符以空字符串作为输入参数。
到此为止,我们已经基本涵盖了基础知识。以下章节将介绍更多高级主题,这些主题涉及对候选选择过程的更多控制。
高级用法
自定义内容选择器
我们知道一个规则可以有多种候选。默认情况下,Tracery随机选择一个候选选项,但选择过程保证是严格均匀的。
也就是说,如果有一个有5个选项的规则,并且该规则评估了100次,那么这5个选项将各被选中20次。
这很容易证明
import Tracery
var t = Tracery {[
"option": [ "a", "b", "c", "d", "e" ]
]}
var tracker = [String: Int]()
t.add(modifier: "track") { input in
let count = tracker[input] ?? 0
tracker[input] = count + 1
return input
}
func runOptionRule(times: Int, header: String) {
tracker.removeAll()
for _ in 0..<times {
_ = t.expand("#option.track#")
}
let sep = String(repeating: "-", count: header.characters.count)
print(sep)
print(header)
print(sep)
tracker.forEach {
print($0.key, $0.value)
}
}
runOptionRule(times: 100, header: "default")
// output will be
// b 20
// e 20
// a 20
// d 20
// c 20
这很好,默认实现可能对大多数情况足够了。但是,你可能需要支持对规则候选进行确定性选择。例如,你可能希望按顺序选择候选,或者永远选择第一个可用的候选,或者使用某种伪随机生成器来选择可重复的值。
为了支持这些情况以及更多情况,Tracery提供了为每个规则指定自定义内容选择器的选项。
选择第一个项目选择器
让我们看一个简单的例子。
// create a selector that always picks the first
// item from the available items
class AlwaysPickFirst : RuleCandidateSelector {
func pick(count: Int) -> Int {
return 0
}
}
// attach this new selector to rule: option
t.setCandidateSelector(rule: "option", selector: AlwaysPickFirst())
runOptionRule(times: 100, header: "pick first")
// output will be:
// a 100
如你所见,只选择了a。
自定义随机项目选择器
例如,让我们创建一个自定义随机选择器。
class Arc4RandomSelector : RuleCandidateSelector {
func pick(count: Int) -> Int {
return Int(arc4random_uniform(UInt32(count)))
}
}
t.setCandidateSelector(rule: "option", selector: Arc4RandomSelector())
// do a new dry run
runOptionRule(times: 100, header: "arc4 random")
// sample output, will vary when you try
// b 18
// e 25
// a 20
// d 15
// c 22
请注意,当使用 arc4random_uniform
时,值选择的分布是如何变化的。随着时间的推移,运行次数增加,arc4random_uniform
倾向于均匀分布,这与 Tracery 中的默认实现不同,即使 5 次运行也保证所有 5 个选项都至少被选择一次。
t = Tracery {[
"option": [ "a", "b", "c", "d", "e" ]
]}
t.add(modifier: "track") { input in
let count = tracker[input] ?? 0
tracker[input] = count + 1
return input
}
runOptionRule(times: 5, header: "default")
// output will be
// b 1
// e 1
// a 1
// d 1
// c 1
既然我们相当了解一个给定规则的规则内容选择机制,那么让我们来探讨加权分布问题。
假设我们需要一个特定的候选者比另一个候选者多选择 5 次。
指定此内容的一种方法如下:
t = Tracery {[
"option": [ "a", "a", "a", "a", "a", "b" ]
]}
t.add(modifier: "track") { input in
let count = tracker[input] ?? 0
tracker[input] = count + 1
return input
}
runOptionRule(times: 100, header: "default - weighted")
// sample output, will vary over runs
// b 17 ~> 20% of 100
// a 83 ~> 80% of 100, i.e. 5 times 20
这可能在简单情况下有效,但如果您有更多候选者,并且有更复杂的权重分布规则,事情可能会很快变得混乱。
为了更好地控制候选者的表示,Tracery 允许自定义候选者提供者。
自定义候选者提供者
加权分布
// This class implements two protocols
// RuleCandidateSelector - which as we have seen before is used to
// to select content in a custom way
// RuleCandidatesProvider - the protocol which needs to be
// adhered to to provide customised content
class ExampleWeightedCandidateSet : RuleCandidatesProvider, RuleCandidateSelector {
// required for RuleCandidatesProvider
let candidates: [String]
let runningWeights: [(total:Int, target:Int)]
let totalWeights: UInt32
init(_ distribution:[String:Int]) {
distribution.values.map { $0 }.forEach {
assert($0 > 0, "weights must be positive")
}
let weightedCandidates = distribution
.map { ($0, $1) }
candidates = weightedCandidates
.map { $0.0 }
runningWeights = weightedCandidates
.map { $0.1 }
.scan(0, +)
.enumerated()
.map { ($0.element, $0.offset) }
totalWeights = distribution
.values
.map { $0 }
.reduce(0) { $0.0 + UInt32($0.1) }
}
// required for RuleCandidateSelector
func pick(count: Int) -> Int {
let choice = Int(arc4random_uniform(totalWeights) + 1) // since running weight start at 1
for weight in runningWeights {
if choice <= weight.total {
return weight.target
}
}
// we will never reach here
fatalError()
}
}
t = Tracery {[
"option": ExampleWeightedCandidateSet(["a": 5, "b": 1])
]}
t.add(modifier: "track") { input in
let count = tracker[input] ?? 0
tracker[input] = count + 1
return input
}
runOptionRule(times: 100, header: "custom weighted")
// sample output, will vary by run
// b 13
// a 87
// as before, option b is 5 times
// more likely to chosen over a
通过将自定义候选者提供者作为规则的扩展,我们可以完全控制候选者的列出。在这个实现中,我们不需要像之前那样重复指定 a
5 次,而只需要指定一次 a
,以及其在整体分布中预期的权重。
这提供了一种非常强大的机制来控制候选人规则扩展的可用的内容。例如,你可以编写一个自定义候选者提供者,根据外部条件是否满足展示 50 个候选人,否则展示 100 个。可能性是无限的。
总结:
- 使用 修饰符 和 方法 修改扩展的规则
- 在规则级别可以指定自定义的 候选者选择器 以控制如何扩展规则
- 使用自定义的 候选者提供者 可以给您更大的控制权,关于规则扩展的可能性
递归
规则扩展
可以定义递归规则。在此过程中,必须提供一个至少包含一个可以退出递归的规则候选者。
import Tracery
// suppose we wish to generate a random binary string
// if we write the rules as
var t = Tracery {[
"binary": [ "0 #binary#", "1 #binary#" ]
]}
t.expand("#binary#")
// will output:
// ⛔️ stack overflow
// Since there is no option to exit out of the rule expansion
// We can add one explicitly
t = Tracery {[
"binary": [ "0#binary#", "1#binary#", "" ]
]}
print(t.expand("attempt 2: #binary#"))
// while this works, if we run this a couple of times
// you will notice that the output is as follows:
// all possible outputs:
// attempt 2: 1
// attempt 2: 0
// attempt 2: 01
// attempt 2: 10
// attempt 2: <- empty
// all possible outputs are limited because the built-in
// candidate selector is guaranteed to quickly select the ""
// candidate to maintain a strict uniform distribution
// we can fix this
t = Tracery {[
"binary": WeightedCandidateSet([
"0#binary#": 10,
"1#binary#": 10,
"": 1
])
]}
print(t.expand("attempt 3: #binary#"))
// sample outputs:
// attempt 3: 011101000010100001010
// attempt 3: 1010110
// attempt 3: 10101100101 and so on
// Now we have more control, as we are stating that we are 20 times more
// likely to continue with a binary rule than the exit
如果需要生成特定长度的随机序列,可以选择创建自定义的RuleCandidateSelector
,或编写/生成一组非递归规则。
可以通过更改
Tracery.maxStackDepth
属性来控制递归规则可以扩展的深度。
日志记录
可以通过更改Tracery.logLevel
属性来控制日志记录行为。
Tracery.logLevel = .verbose
t = Tracery {[
"binary": WeightedCandidateSet([
"0#binary#": 10,
"1#binary#": 10,
"": 1
])
]}
print(t.expand("attempt 3: #binary#"))
// sample output:
// attempt 3: 101010100011001001011
// attempt 3: 001001011111
// attempt 3: 1110010111121111
// attempt 3: 10
// this will print the entire trace that Tracery generates, you will see detailed output regarding rule validation, parsing, rule expansion - useful to debug and understand how rules are processed.
可用的日志选项包括
无
errors
- 打印任何解析错误(默认)warnings
- 打印警告,例如突出显示递归规则、循环引用、可能无效的规则等info
- 打印信息性消息,例如 tracery 引擎的状态verbose
- 打印跟踪级别消息,您可以详细了解引擎如何解析文本和评估规则
链式评估
考虑以下示例
t = Tracery {[
"b" : ["0", "1"],
"num": "#b##b#",
"10": "one_zero",
"00": "zero_zero",
"01": "zero_one",
"11": "one_one",
]}
t.expand("#num#")
// will print either 01, 10, 11 or 10
t.add(modifier: "eval") { [unowned t] input in
// treat input as a rule and expand it
return t.expand("#\(input)#")
}
t.expand("#num.eval#")
// will now print one_zero, zero_zero, zero_one or one_one
我们现在有一种机制可以根据另一规则的扩展结果来扩展一个规则。
分层标签存储
默认情况下,标签具有全局作用域。这意味着标签可以在任何地方设置,其值可以通过任何层次的规则在规则扩展的任何级别上访问。我们可以使用分层存储来限制标签访问。
规则使用堆栈进行扩展。每个规则评估都在堆栈的特定深度上进行。如果层 n
的规则扩展为两个子规则,则两个子规则将在层 n+1
上进行评估。标签的层 n
将与创建它的层 n
的规则相同。
当层 n
的规则尝试扩展标签时,Tracery 会检查层 n
上是否存在该标签,或从层 n-1,...0 查找值,直到找到为止。
以下示例中,我们使用层次存储在规则扩展的不同级别上推送和弹出匹配的开括号和闭括号。当规则子扩展完成后,会记住匹配的闭括号。
let options = TraceryOptions()
options.tagStorageType = .heirarchical
let braceTypes = ["()","{}","<>","«»","𛰫𛰬","⌜⌝","ᙅᙂ","ᙦᙣ","⁅⁆","⌈⌉","⌊⌋","⟦⟧","⦃⦄","⦗⦘","⫷⫸"]
.map { braces -> String in
let open = braces[braces.startIndex]
let close = braces[braces.index(after: braces.startIndex)]
return "[open:\(open)][close:\(close)]"
}
let h = Tracery(options) {[
"letter": ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P"],
"bracetypes": braceTypes,
"brace": [
// open with current symbol,
// create new symbol, open brace pair and evaluate a sub-rule call to brace
// finally close off with orginal symbol and matching close brace pair
"#open##symbol# #origin##symbol##close# ",
"#open##symbol# #origin##symbol##close# #origin#",
// exits recursion
"",
],
// start with a symbol and bracetype
"origin": ["#[symbol:#letter#][#bracetypes#]brace#"]
]}
h.expand("#origin#")
// sample outputs:
// {L ⌜D D⌝ (P 𛰫O O𛰬 <F ⦃C C⦄ F> P) L}
// ⁅M ᙅK Kᙂ ᙦE {O O} Eᙣ M⁆
// ⌈C C⌉
// <K K>
控制流程
if块
支持if块。您可以使用if块检查规则是否与条件匹配,并根据这一结果输出不同的内容。格式为[if condition then rule (else rule)]
。else
部分是可选的。
条件表达为:rule condition_operator rule
。左边和右边的rule
都会被扩展,并根据指定的condition operator
来检查它们的输出。
以下条件运算符是被允许的
==
检查在扩展后左右两边是否相等!=
检查在扩展后左右两边是否不相等in
检查扩展后的左边是否包含在右边的扩展候选中not in
检查扩展后的左边不包含在右边的扩展候选中
import Foundation
import Tracery
var t = Tracery {[
"digit" : [0,1,2,3,4,5,6,7,8,9],
"binary": [0,1],
"is_binary": "is binary",
"not_binary": "is not binary",
// check if generated digit is binary
"msg_if_binary" : "[d:#digit#][if #d# in #binary# then #d# #is_binary# else #d# #not_binary#]",
// ouput only if generated digit is zero
"msg_if_zero" : "[d:#digit#][if #d# == 0 then #d# zero]"
]}
t.expand("#msg_if_binary#")
t.expand("#msg_if_zero#")
while块
while块可以用来创建循环。它采用以下形式:[while condition do rule]
。只要condition
评估为true,就会扩展在do
部分指定的rule
。
// print out a number that does not contain digits 0 or 1
t.expand("[while #[d:#digit#]d# not in #binary# do #d#]")
文本格式
Tracery也可以识别定义在普通文本文件中的规则。该文件必须包含一组规则定义,规则定义在中括号内指定,每个候选定义在一行上。以下是一个示例文件:
[binary]
0#binary#
1#binary#
#empty#
[empty]
上述文件是一个基本二进制数生成器。以下是另一个用于虚构名称的例子。
[fable]
#the# #adjective# #noun#
#the# #noun#
#the# #noun# Who #verb# The #adjective# #noun#
[the]
The
The Strange Story of The
The Tale of The
A
The Origin of The
[adjective]
Lonely
Missing
Greedy
Valiant
Blind
[noun]
Hare
Hound
Beggar
Lion
Frog
[verb]
Finds
Meets
Tricks
Outwits
此输入文件将生成如下输出:
A Greedy Frog
The Beggar
The Origin of The Hare Who Finds The Missing Lion
The Strange Story of The Hound
The Tale of The Blind Frog
您可以使用Tracery.init(path:)
构造函数从普通文本文件中消费规则。
Tracery 语法
本节试图描述 Tracery 的语法规范。
rule_candidate -> ( plain_text | rule | tag )*
tag -> [ tag_name : tag_value ]
tag_name -> plain_text
tag_value -> tag_value_candidate (,tag_value_candidate)*
tag_value_candidate -> rule_candidate
rule -> # (tag)* | rule_name(.modifier|.call|.method)* | control_block* #
rule_name -> plain_text
modifier -> plain_text
call -> plain_text
method -> method_name ( param (,param)* )
method_name -> plain_text
param -> plain_text | rule
control_block -> if_block | while_block
condition_operator -> == | != | in | not in
condition -> rule condition_operator rule
if_block -> [if condition then rule (else rule)]
while_block -> [while condition do rule]
结论
Swift 中的 Tracery 是由 Benzi 开发的。
原始的 JavaScript 库可以在 Tracery.io 找到。
此 README 文件是使用 playme 自动生成的。