Tracery - powerful content generation

目录

介绍

Tracery 是一个内容生成库,最初由 @GalaxyKate 创建;您可以在 Tracery.io 找到更多信息。

这个实现虽然深受原始版本的启发,但添加了更多功能。

Tracery 中的内容生成基于一组输入的规则。 这些规则决定了应该如何生成内容。

安装

手册

Cocoapods

您需要将类似 `pod 'Tracery', '~> 0.0.2'` 的内容添加到您的 Podfile 中。

target 'MyApp' do
  pod 'Tracery', '~> 0.0.2'
end

然后在您的终端中或从 CocoaPods.app 运行 `pod install`。

或者,为了进行测试运行,运行以下命令

pod try Tracery

顶部


Swift Package Manager

使用 Xcode 11 中的 Swift Package Manager 支持(File > Swift Packages > Add Package Dependency...)将 Swift 包添加到您的目标中。或者使用以下代码将 Tracery 添加到您的 Package.swift 文件中

.package(url: "https://github.com/BenziAhamed/Tracery.git", from: "0.0.2")

然后,将 Tracery 作为依赖项添加到您的目标中

.target(name: "App", dependencies: [..., "Tracery"])

基本用法

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# 时,它都会选择一个候选值; 在这个例子中,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 and jenny went up the hill, john fell down, and so did jenny too”

// 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] 格式创建。 在上面的代码段中,我们首先创建两个标签,bg,分别保存 boygirl 名称的值。 稍后,我们可以像使用新规则一样使用 #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,复数化,caps 等。 该库遵循另一种方法,并提供自定义端点,以便可以根据需要添加尽可能多的修饰符。

下一个规则展开选项是添加自定义规则方法的能力。

顶部


方法

虽然修饰符会将规则的当前候选值作为输入接收,但方法可用于定义可以接受参数的修饰符。

方法的编写和调用方式与修饰符相同。

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。 标签总是优先于规则。

顶部


调用

还有一种规则扩展类型,即call。 与使用参数和参数并且期望返回某些字符串值的修饰符和方法不同,调用不需要这样做。

调用与修饰符具有相同的语法 #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#")

在上面的代码段中,规则 letter 有 5 个候选者,其中 4 个基本上是字符串值,但第五个是规则。 是的,规则可以自由混合,并且可以出现在任何地方。 因此,在这种情况下,规则 f 可以展开为基本字符串 f。 请注意,我们还添加了 track 调用。

现在,每当 letter 选择规则 f 作为展开的候选对象时,.track 将被调用。

规则扩展可以单独放在一对 # 内部。 例如,如果我们创建了一个总是向其输入添加“yo”的修饰符,称之为 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 一次,以及其在整体分布中的预期权重,而无需像以前那样重复 a 5 次。

这提供了一种非常强大的机制来控制哪些候选者可用于规则展开。 例如,您可以编写一个自定义的候选者提供者,该提供者根据是否满足外部条件来提供 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.

可用的日志记录选项包括:

链式评估

考虑以下示例

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 进行检查。

允许使用以下条件运算符

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 自动生成的