Swift GYB

综合编程 2018-07-09 阅读原文

The term “boilerplate”
goes back to the early days of print media.
Small regional newspapers had column inches to fill,
but typically lacked the writing staff to make this happen,
so many of them turned to large print syndicates
for a steady flow of content that could be added verbatim
into the back pages of their dailies.
These stories would often be provided on pre-set plates,
which resembled the rolled sheets of steel used to make boilers,
hence the name.

Through a process of metonymy,
the content itself came to be known as “boilerplate”,
and the concept was appropriated to encompass standardized, formulaic text
in contracts, form letters, and,
most relevant to this week’s article on NSHipster, code.

Not all code can be glamorous.
In fact, a lot of the low-level infrastructure that makes everything work
is a slog of boilerplate.

This is true of the Swift standard library,
which includes families of types
like signed integers ( Int8
, Int16
, Int32
, Int64
)
whose implementation varies only in the size of the respective type.

Copy-pasting code may work as a one-off solution
(assuming you manage to get it right the first time),
but it’s not sustainable.
Each time you want to make changes to these derived implementations,
you risk introducing slight inconsistencies
that cause the implementations to diverge over time —
not unlike the random mutations responsible for the variation of life on Earth.

Languages have various techniques to cope with this,
from C++ templates and Lisp macros to eval
and C preprocessor statements.

Swift doesn’t have a macro system,
and because the standard library is itself written in Swift,
it can’t take advantage of C++ metaprogramming capabilities.
Instead, the Swift maintainers use a Python script called gyb.py
to generate source code using a small set of template tags.

GYB is an acronym for “Generate Your Boilerplate”,
a reference to another Python tool, GYP
, or “Generate Your Projects”.

How GYB Works

GYB is a lightweight templating system
that allows you to use Python code
for variable substitution and flow control:

%{  }
% : ... % end
${  }

All other text is passed through unchanged.

A good example of GYB can be found in Codable.swift.gyb
.
At the top of the file,
the base Codable
types are assigned to an instance variable:

%{
codable_types = ['Bool', 'String', 'Double', 'Float',
                 'Int', 'Int8', 'Int16', 'Int32', 'Int64',
                 'UInt', 'UInt8', 'UInt16', 'UInt32', 'UInt64']
}%

Later on,
in the implementation of SingleValueEncodingContainer
,
these types are iterated over to generate the
methods declarations for the protocol’s requirements:

% for type in codable_types:
  mutating func encode(_ value: ${type}, forKey key: Key) throws
% end

Evaluating the GYB template results in the following declarations:

mutating func encode(_ value: Bool) throws
mutating func encode(_ value: String) throws
mutating func encode(_ value: Double) throws
mutating func encode(_ value: Float) throws
mutating func encode(_ value: Int) throws
mutating func encode(_ value: Int8) throws
mutating func encode(_ value: Int16) throws
mutating func encode(_ value: Int32) throws
mutating func encode(_ value: Int64) throws
mutating func encode(_ value: UInt) throws
mutating func encode(_ value: UInt8) throws
mutating func encode(_ value: UInt16) throws
mutating func encode(_ value: UInt32) throws
mutating func encode(_ value: UInt64) throws

This pattern is used throughout the file
to generate similarly formulaic declarations for methods like encode(_:forKey:)
, decode(_:forKey:)
, and decodeIfPresent(_:forKey:)
.
In total, GYB reduces the amount of boilerplate code by a few thousand LOC:

$ wc -l Codable.swift.gyb
2183 Codable.swift.gyb
$ wc -l Codable.swift
5790 Codable.swift
Important: A valid GYB template may not generate valid Swift code. If compilation errors occur in derived files, it may be difficult to determine the underlying cause.

Using GYB in Xcode

GYB isn’t part of the standard Xcode toolchain,
so you won’t find it with xcrun
.
Instead, you can download the source code
and then use the chmod
command to make gyb
executable
(the default installation of Python on macOS should be able to run gyb
):

$ wget https://github.com/apple/swift/raw/master/utils/gyb
$ wget https://github.com/apple/swift/raw/master/utils/gyb.py
$ chmod +x gyb

Move these somewhere that can be accessed from your Xcode project,
but keep them separate from your source files.
For example, a Vendor
directory at your project root.

In Xcode, click on the blue project file icon in the navigator,
select the active target in your project,
and navigate to the “Build Phases” panel.
At the top, you’ll see a +
symbol
that you can click to add a new build phase.
Select “Add New Run Script Phase”,
and enter the following into the source editor:

find . -name '*.gyb' |                                               
    while read file; do                                              
        ./path/to/gyb --line-directive '' -o "${file%.gyb}" "$file"; 
    done
Make sure to order the GYB build phase before Compile Sources.

Now when you build your project
any file with the .swift.gyb
file extension
is evaluated by GYB,
which outputs a .swift
file
that’s compiled along with the rest of the code in the project.

When to Use GYB

As with any tool,
knowing when to use it is just as important as knowing how.
Here are some examples of when you might open your toolbox and reach for GYB.

Generating Formulaic Code

Are you copy-pasting the same code for elements in a set
or items in a sequence?
A for-in loop with variable substitution might be the solution.

As seen in the example with Codable
from before,
you can declare a collection at the top of your GYB template file
and then iterate over that collection
for type, property, or method declarations:

%{ abilities = ['strength', 'dexterity', 'constitution',
                'intelligence', 'wisdom', 'charisma']
}
class Character {
  var name: String

% for ability in abilities:
  var ${type}: Int
% end
}

Just be aware that a lot of repetition is a code smell,
and may indicate that there’s a better way to accomplish your task.
Built-in language feature like protocol extensions and generics
can eliminate a lot of code duplication,
so be on the lookout to use these instead of brute-forcing with GYB.

Generating Code Derived from Data

Are you writing code based on a data source?
Try incorporating GYB into your development!

GYB files can import of Python packages like json
, xml
, and csv
,
so you can parse pretty much any kind of file you might encounter:

%{ import csv }
% with open('path/to/file.csv') as file:
    % for row in csv.DictReader(file):

If you want to see this in action,
check out Currencies.swift.gyb
which generates Swift enumerations
for each of the currencies defined by the ISO 4217
specification.

Keep compilation fast and deterministic by downloading data to files that can be checked into source control rather than making HTTP requests or database queries in GYB files.

Code generation makes it trivial to keep your code in sync
with the relevant standards.
Simply update the data file and re-run GYB.

Swift has done a lot to cut down on boilerplate recently
with the addition of compiler synthesis of Encodable
and Decodable
in 4.0, Equatable
and Hashable
in 4.1, and CaseIterable
in 4.2.
We hope that this momentum is carried in future updates to the language.

In the meantime, for everything else,
GYB is a useful tool for code generation.

Another great tool from the community for this is Sourcery
,
which allows you to write templates in Swift
(via Stencil
)
rather than Python.

“Don’t Repeat Yourself” may be a virtue in programming,
but sometimes you have to say things a few times to make things work.
When you do, you’ll be thankful to have a tool like GYB to say it for you.

责编内容by:NSHipster 【阅读原文】。感谢您的支持!

您可能感兴趣的

Swift趣闻–同名就是伤害 图片来之网络 一个无解的问题 一个不是问题的问题 一个值得好好思考的问题 1. 背景 随着swift版本迭代到4.1,...
Will raywenderlich.com Books be Updated for Swift ... Which books will we update for Swift 4? WWDC has come and gone f...
从 Swift 中的序列到类型擦除 Swift.png 如果有这样的一个需求,我希望能像数组一样,用 for 循环遍历一个类或结构体中的所有属性。就像下面这样: le...
WWDC 2018:在Swift中如何高效地使用集合 Session 229: Using Collections Effectively 所有应用都会用到了集合,为了获得最佳性能,了解背后的基础知识...
Maintaining a Swift and Objective-C Hybrid Codebas... Swift is gaining popularity among iOS developers, which is ...