WWDC 2019が開催されましたね!
個人的にはSwiftUIで盛り上がっていました。
そんなSwiftUIのサンプルコードに次のようなものがありました。
struct Content: View {
@State var model = Themes.listModel
var body: some View {
List(model.items, action: model.selectItem) { item in
Image(item.image)
VStack(alignment: .leading) {
Text(item.title)
Text(item.subtitle)
.color(.gray)
}
}
}
}
(from: https://developer.apple.com/xcode/swiftui/)
この短いコードにはSwift5まででは実現できない機能が複数含まれています。
しかし、ほとんどは突然出てきた機能ではなく、Swift Forumsで提案され、コミュニティが議論し、Core Teamがレビューして実装された機能です。
この記事では、swift-evolutionのproposalsから自分が興味深いと思った機能を(適当に)拾い、紹介します。
ただし、全てのSwift5.1の新機能は紹介しませんし、個人的に面白いと思ったOpaque Result Typeはすでに素晴らしい紹介記事があるのでここでは言及しません。
Swift 5.1 に導入される Opaque Result Type とは何か - Qiita
またテンプレですが、間違いがあった場合などはご指摘をお願いします
Implemented (Swift 5.1)
すでにマージされていてSwift5.1から使えるようになる提案。
Synthesize default values for the memberwise initializer
Swift5までは以下のようにstructを書くとイニシャライザが自動生成されていました。
struct Dog {
var age: Int = 0
var name: String
// ↑を書くと↓のイニシャライザが自動生成される
init(age: Int, name: String)
}
// Dog.ageにはデフォルト値があるが、イニシャライザで指定する必要がある
let dog = Dog(age: 0, name: "Sparky")
しかしage
にはデフォルト値として0
があるので以下のようなイニシャライザを生成してほしいよねという提案。
struct Dog {
var age: Int = 0
var name: String
// ↑を書くと↓のイニシャライザが自動生成される
init(age: Int = 0, name: String)
}
// ageがデフォルト値でいい場合、省略可能になった!
let dog = Dog(name: "Sparky")
なお、注意点としてTupleの場合はデフォルト値にはならないみたいです。
struct Dog {
var (age, name) = (0, "Sparky")
// init(age: Int = 0, name: String = "Sparky")とはならない
init(age: Int, name: String)
}
またプロパティがletの場合は今まで通りイニシャライザは生成されません。
struct Dog {
let age: Int = 0
var name: String
// ageは自動生成されない
init(name: String)
}
Key Path Member Lookup
Swift4.2ではDynamic Member Lookupが追加されました。
[参考]Swift4.2で導入された機能 Dynamic Member LookupでSwiftのコードをよりCOOLにしよう - Qiita
これにより、実行時にプロパティを決定することが可能になり、動的型言語との親和性が向上しました。
ところで、実行時にプロパティを決定できる方法がSwiftにはまだあります。
その1つがKeyPathです。
KeyPathを使うことで特定の状況で簡潔に書くことができます。
// これは多分コンパイルエラー起こしそうだけど雰囲気で。。。
struct Lens<T> {
let getter: () -> T
let setter: (T) -> Void
var value: T {
get {
return getter()
}
set {
setter(newValue)
}
}
}
extension Lens {
func project<U>(_ keyPath: WritableKeyPath<T, U>) -> Lens<U> {
return Lens<U>(
getter: { self.value[keyPath: keyPath] },
setter: { self.value[keyPath: keyPath] = $0 })
}
}
KeyPathを使わない場合上の例だとprojectでクロージャを渡したり色々面倒そうですね。
アクセスは以下のようにできます。
struct Point {
var x, y: Double
}
struct Rectangle {
var topLeft, bottomRight: Point
}
func projections(lens: Lens<Rectangle>) {
let topLeft: Lens<Point> = lens.project(\.topLeft)
let top: Lens<Double> = lens.project(\.topLeft.y)
}
現時点でも結構簡潔なのですが、いちいち\.topLeft
や\.topLeft.y
のように\
を使用しなくていいよね、直接lens.topLeft
のようにアクセスしたいよね、ということで以下のように書けるようにしようぜという提案。
@dynamicMemberLookup
struct Lens<T> {
let getter: () -> T
let setter: (T) -> Void
var value: T {
get {
return getter()
}
set {
setter(newValue)
}
}
subscript<U>(dynamicMember keyPath: WritableKeyPath<T, U>) -> Lens<U> {
return Lens<U>(
getter: { self.value[keyPath: keyPath] },
setter: { self.value[keyPath: keyPath] = $0 })
}
}
func projections(lens: Lens<Rectangle>) {
// コンパイラは`let topLeft = lens[dynamicMember: \.topLeft]`に展開する
let topLeft: Lens<Point> = lens.topLeft
}
基本的に文字ベースの@dynamicMemberLookupと似た感じであり、以下のようなルールになります。
- KeyPathに指定できるのは@dynamicMember型に含まれないプロパティ名
-
Lens<Rectangle>.topLeft
プロパティがあった場合、lens.topLeft
としてもRectangle
のtopLeft
にはアクセスできない(隠蔽される)
-
- @dynamicMemberLookupはextensionには書けない
- @dynamicMemberLookupには引数ラベルがdynamicMemberで型がkey path型(e.g., KeyPath, WritableKeyPath)の単一のパラメータ
- 文字列ベースとKeyPathベースの両方のdynamicMemberがあった場合、KeyPathが優先される
- 文字列よりKeyPathのほうがより多くの情報を持っているため
Static and class subscript
今までインスタンス限定だったsubscriptをstatic(とclass)プロパティでも使えるようにしようぜという提案。
以下みたいな感じ。
public enum Environment {
public static subscript(_ name: String) -> String? {
get {
return getenv(name).map(String.init(cString:))
}
set {
guard let newValue = newValue else {
unsetenv(name)
return
}
setenv(name, newValue, 1)
}
}
}
Environment["PATH"] += ":/some/path"
また、@dynamicMemberLookupを使って以下のようにも書けるようになります。
@dynamicMemberLookup
public enum Environment {
public static subscript(_ name: String) -> String? {
// as before
}
public static subscript(dynamicMember name: String) -> String? {
get { return self[name] }
set { self.name = newValue }
}
}
Environment.PATH += ":/some/path"
結構便利そう。
これにKeyPath member lookupみたいにKeyPathでもアクセスできるようになればもっと便利そうですが、staticなKeyPathは結構複雑らしく、この提案には含まれません。
Implicit returns from single-expression functions
クロージャは単一の式である場合、returnを省略できます。
それを普通の関数とかでも認めようという提案。
以下のように書けます。
// 今までクロージャでしか書けなかったけど...
let closure: () -> Int = { 1 }
// 関数でもOK!
func add(lhs: Int, rhs: Int) -> Int { lhs + rhs }
var location: Location {
get {
// getでもOK!
.init(latitude: lat, longitude: long)
}
set {
self.lat = newValue.latitude
self.long = newValue.longitude
}
}
struct Echo<T> {
// subscriptでもOK!
subscript(_ value: T) -> T { value }
}
class Derived: Base {
// initでも書けるけど実質nilだけを返す場合のみ
required init?() { nil }
}
なおこの提案は破壊的変更が含まれているのですが、該当するパターンは以下です。
func bad(value: Int = 0) -> Int { return 0 }
func bad() -> Never { return fatalError() }
func callBad() -> Int { bad() }
現在のSwiftのバージョンではcallBad
を読んだ場合、() -> Never
のほうのbad
で解決されます。
しかし、この提案では(value: Int = 0) -> Int
のほうのbad
で解決されるようになるみたいです。
ただこのようなパターンは非常にレアで、実際互換性チェックのテストは問題なく動いているようです。
また、KotlinやScalaでは単一の式であった場合、{}
自体を省略できます。
// Kotlin
fun squareOf(x: Int): Int = x * x
// Scala
def squareOf(x: Int): Int = x * x
ただこれをSwiftでやろうとすると以下のようなときに問題になります。
class Foo {
var get: ()
var value: Void {
// このときの get = () は
// get { return () } ?
// self.get = () ?
get = ()
}
}
新しい文法にもなるため、言語の複雑度を上げてまで取り入れない模様。
またRustでは単一式ではなく、関数中の最後の式もセミコロンをつけなければ戻り値になります。
fn call() -> i64 {
foo();
bar();
baz()
}
ただSwiftで似たようなことをやる場合、例えば以下の関数
func evenOrOdd(_ int: Int) -> EvenOrOdd {
if int % 2 == 0 {
return .even
}
.odd
}
.odd
は省略できているのに.even
はreturnが必須で少し残念です。
Rustではifも式なので以下のように書けます。
fn even_or_odd(i: i64) -> EvenOrOdd {
if i % 2 == 0 {
EvenOrOdd::Even
} else {
EvenOrOdd::Odd
}
}
Swiftもifを式にすればいいのですが、それは大きな変更でありこの提案の範囲外になります。
なお、冒頭に書いたSwiftUIのサンプルコードで
var body: some View {
List(...)
}
のようなget-onlyなプロパティでreturnがないコードが出てきましたが、この提案の機能を使っているそうです。
Accepted
採用され将来的にSwiftに実装される予定の提案。
Key Path Expressions as Functions
以下のようなstructがあった場合、
struct User {
let email: String
let isAdmin: Bool
}
User
の配列からemail
だけを取得したい場合、Swiftではよく以下のように書きます。
users.map { $0.email }
でもいちいちあるプロパティだけを参照するのにクロージャで書くのめんどいよね、Swiftには特定のプロパティを指定する構文がすでにあるよねということで以下のように書きたい。
users.map(\.email)
ただmapとかfilterとかにKeyPathのオーバーロードを追加していくのは負けだよねということで上のように書いたら以下のようなコードをコンパイラが生成するようにしない?という提案。
//↓を書くと
users.map(\.email)
//↓のようなコードに展開される
users.map { $0[keyPath: \User.email] }
現状、一旦変数に入れて型推論した場合は (Root) -> Valueにはなりません。
// users.mapを考慮して (Root) -> Valueに展開して欲しいが・・・・
let f = \User.email
// 現状はそうならず、KeyPath<User, String>になってしまう
users.map(f) // error: Cannot convert value of type 'WritableKeyPath<Person, String>' to expected argument type '(Person) throws -> String'
// 型を指定すればセーフ
let f: (User) -> String = \.email
// キャストしてもセーフ
let f = \.email as (User) -> String
// 実際には↓のように展開される
let f: (User) -> String = { kp in { root in root[keyPath: kp] } }(\User.email)
また子プロパティを指定したりselfで自分を指定したりできます。
users.map(\.email.count) // ok
[1, nil, 3, nil , 5].compactMap(\.self) // ok
注意点として、関数としてのKeyPath式の中に関数等の処理を書いた場合、関数が呼び出されるたびに中の処理も呼び出されるのではなく、KeyPath式が評価されたときに処理が呼び出されます。
var nextIndex = 0
func makeIndex() -> {
defer { nextIndex += 1}
return nextIndex
}
let getFirst = \Array<Int>.[makeIndex()]
// ↑のように書いた場合、Compilerは↓のようなコードを生成する
// let index = makeIndex() // 1
// let getFirst = { array: Array<Int> in array[index] }
let getSecond = \Array<Int>.[makeIndex()]
// なので何回呼び出しても同じindexを参照する
assert(getFirst([1, 2, 3]) == 1)
assert(getFirst([1, 2, 3]) == 1)
assert(getFirst([1, 2, 3]) == 1)
assert(getSecond([1, 2, 3]) == 2)
assert(getSecond([1, 2, 3]) == 2)
assert(getSecond([1, 2, 3]) == 2)
Callable values of user-defined nominal types
現在、Swiftでは関数のように呼び出し可能な値が3つあります。
- 関数型の値
- 型名
- Tのような型があった場合T()と呼べる(実際はT.init()になる)
- @dynamicCallable型の値
しかし、以下のような型がある場合
struct Adder {
var base: Int
func add(_ x: Int) -> Int {
return base + x
}
}
let add3 = Adder(base: 3)
add3.add(10) // => 13
Adder
は型でaddすることを表現しておりそれに対してaddを呼ぶのは文法的に少し冗長です。
ということで以下のように呼びたいよねという提案。
struct Adder {
var base: Int
func call(_ x: Int) -> Int {
return base + x
}
}
let add3 = Adder(base: 3)
add3(10) // add3.call(10) に展開される
callメソッドを定義することで関数型のように値から直接関数を呼び出せるようになります。
なおcallable value
(call メソッドを持っている値)は暗黙的に関数型にはできない予定です(やるとしてもあとで)
let add1 = Adder(base: 1)
let f: (Int) -> Int = add1 // コンパイルエラー(予定)
let f: (Int) -> Int = add1.call // やるとしたらcallを直接指定
let f = add1 as (Int) -> Int // もしかしたらasキャストはOKになるかも?
また、これを利用して関数型をprotocolのように使うことによって効率化できる可能性があるそうです。
struct BoundClosure<T, F: (T) -> ()>: () -> () {
var function: F
var value: T
func call() { return function(value) }
}
let f = BoundClosure({ print($0)}, x)
f()
こうすることでクロージャのサイズをあらかじめ決定でき、最適化されやすくなるそうです(Rustとかは似たようなことをやっているっぽい)
参考: https://forums.swift.org/t/pitch-introduce-static-callables/21732/2
(ここいらへん詳しくないので誰か・・・)
なお呼び出し方は色々提案されたみたいです。
call
という名前の代わりにapply
またはinvoke
にしようという提案もありましたが、以下の理由によりcall
が優勢っぽいです。
-
Swiftっぽくない
-
Functions — The Swift Programming Language (Swift 5) でも
apply
やinvoke
という言葉を使わずに、関数呼び出しは"function calls"という言葉を使っている。
-
Functions — The Swift Programming Language (Swift 5) でも
-
apply
やinvoke
は SwiftのAPIガイドラインに沿っていない。- APIガイドラインでは副作用のあるなしで関数名を変更(副作用のある場合は動詞、ない場合は動詞+edなど)するが、
apply
やinvoke
は明らかに動詞 - 一方
call
は動詞でも名詞でもあるのでこちらのほうが自然
- APIガイドラインでは副作用のあるなしで関数名を変更(副作用のある場合は動詞、ない場合は動詞+edなど)するが、
また、以下のように特殊な文法を使う提案もありました。
struct Adder {
var base: Int
// 関数名がない方法
func(_ x: Int) -> Int {}
// 関数名を_(underscore)で書く方法
func _(_ x: Int) -> Int {}
// `self` keyword を使う方法
func self(_ x: Int) -> Int {}
// call という修飾子を使う方法
call func(_ x: Int) -> Int {}
}
ただし、これらは上で述べた
let f = add1.call
みたいなことをしたいときに困ります。
またcall修飾子をつける方法はパーサに変更が入り、シンプルではありません。
その点call
メソッドによる方法は.init
と動作が一致しており、統一的です。
また属性ベースやProtocolベースの提案もありましたが、どれもノイズが多くなるなどで結局call
のほうが良いという方向っぽいです。
私がこの部分を書いていたときは提案のレビュー中だったのですが、レビューが終了し、Acceptされました。
ただCore Teamはcall
という名前ではなくcallFunction
に修正しています(ただ、callFunction
は不評で、このあとでさらにcallAsFunction
に変更しています)
また、しれっと書いてあるのですが、Core Teamは@dynamicCallable
や@dynamicMemberLookup
について、属性をつける制限を外す検討をしているようです。
Returned for revision
議論し直し。
Eliding commas from multiline expression lists
現在Swiftでは1行に複数のステートメントを書くのでなければ末尾のセミコロンはオプショナルでつけてもつけなくてもどちらでも大丈夫です。
let a = 1 //;は必要ない
let b = 1; // もちろん明示的につけてもOK
ところで関数呼び出し時は引数が複数ある場合カンマで分ける必要があります。
let a = foo(
bar: bar(),
baz: baz(),
nyarn: nyarn()
)
これをセミコロンみたいに改行で区切れるようにしてカンマを消そうぜという提案。
let a = foo(
bar: bar()
baz: baz()
nyarn: nyarn()
)
これにより地味に便利なのが引数リストの最後をコメントアウトしたいとき。
カンマありの場合最後の引数をコメントアウトするとエラーになります。
print(
"red",
"green",
"blue", // error: unexpected ',' separator
// "cerulean"
)
しかしカンマなしならそのままコンパイルが通ります。
print(
"red"
"green"
"blue" // OK!
// "cerulean"
)
なお配列リテラルは最後がカンマでもコンパイルは通ります。
let array = [
1,
2, // OK!
// 3,
]
関数の引数でも最後のカンマを許可すればいいじゃないかという意見もありますが、前に似たような議論があり、却下されています。
Allow trailing commas in parameter lists and tuples
なおこの提案についてCore Teamの
- Chris Lattner氏は反対しています。
- Douglas Gregor氏は支持しています。
- Ben Cohen氏は支持しているみたいです?
- Ted Kremenek氏(Project Lead)はCode of Conductを守って議論してねと言っています。
個人的にはカンマをつけるのがめんどくさいときがあったのでなるほどなあという気持ちです。
Property Delegates
端的に言うとKotlinのDelegated PropertiesみたいなのをSwiftにも導入しようぜという提案。
以下のように定義すると
@propertyDelegate
enum Lazy<Value> {
case uninitialized(() -> Value)
case initialized(Value)
init(initialValue: @autoclosure @escaping () -> Value) {
self = .uninitialized(initialValue)
}
var value: Value {
mutating get {
switch self {
case .uninitialized(let initializer):
let value = initializer()
self = .initialized(value)
return value
case .initialized(let value):
return value
}
}
set {
self = .initialized(newValue)
}
}
}
以下のように使えるようになります。
@Lazy var foo = 1738
コンパイラは上記の@Lazy
がついたプロパティを見つけると以下のように展開する想定です。
var $foo: Lazy<Int> = Lazy<Int>(initialValue: 1738)
var foo: Int {
get { return $foo.value }
set { $foo.value = newValue }
}
なおこの$fooはコンパイラが生成する値ですが、コード上でもアクセスできる予定です。
extension Lazy {
/// newValue で初期化し直す
mutating func reset(_ newValue: @autoclosure @escaping () -> Value) {
self = .uninitialized(newValue)
}
}
$foo.reset(42)
またdelegateValue
という値を定義することで$foo
でアクセスしたときにdelegateValue
の値を使えます。
protocol Copyable: AnyObject {
func copy() -> Self
}
@propertyDelegate
struct CopyOnWrite<Value: Copyable> {
init(initialValue: Value) {
value = initialValue
}
private(set) var value: Value
var delegateValue: Value {
mutating get {
if !isKnownUniquelyReferenced(&value) {
value = value.copy()
}
return value
}
set {
value = newValue
}
}
}
@CopyOnWrite var storage: MyStorageBuffer
let index = storage.index(of: ...)
// delegateValue が参照される
$storage.append(...)
ちなみに現在Swiftでは$
で始まる変数名を許可していません。
なのでこの提案が採用された場合、その制限は緩められる予定です。
ただし、今までと同じように自分で定義することはできません。
@Lazy var x = 17
print($x) // OK
let $y = 17 // error: cannot declare entity with $-prefixed name '$y'
この提案は議論し直しとなり、以下のProposalのような方向性で議論しています。
swift-evolution/0258-property-wrappers.md at property-wrappers · DougGregor/swift-evolution · GitHub
主に以下の内容に変更されました。
- 名前は"property delegates"から"property wrappers"に変更された(やっぱりdelegateは紛らわしかった)
- property wrapper型にパラメータがないinitがある場合、それを使用するプロパティは暗黙的にそのinitで初期化される
- property wrapper型をネストできる
-
@A @B var x: Int = 42
があった場合、var $x: A<B<Int>> = A(initialValue: B(initialValue: 42))
で初期化される
-
- property wrapper型を使用するプロパティはgetとsetを持てない
- 既存のlazyと一緒の制約
- property wrapper を持つプロパティはfinalである必要はない
- ストレージ($fooでアクセスできるやつ)はデフォルトでprivate
- 外からも見えるようにしたい場合は
public(wrapper)
のようにする
- 外からも見えるようにしたい場合は
また、property wrapperは現在も活発に議論されています。
(6/4 時点で3スレッド目)
[Pitch #3] Property wrappers (formerly known as Property Delegates) - Pitches - Swift Forums
また、冒頭で書いたようにSwiftUIに
struct Content: View {
@State var model = ...
var body: some View {
...
}
}
というコードがありましたが、この中に出てくる@State
はproperty wrappersを使用しているみたいです。
ちなみに私は読み過ごしていたのですが、
the Core Team had already decided and publicly stated that the feature was going to be eventually accepted into Swift.
らしく、ステータスとしてはReturned for revisionというよりはAccepted with Revisionな感じなんですかね・・・?(ここいらへんの英語の語感がわからない)
また、この機能は単にプロパティを拡張できるだけでなく、ユーザー定義の属性が(プロパティに対してのみにですが)使えるようになるということです。
これはSwiftにとって大きな変更ですが、依然としてプロパティに対してのみです。
もともとユーザー定義の属性を作りたいという議論はあり、Core Teamもビルド時のコード解析で使用され、バイナリには出力されない@staticAttribute
、バイナリに含まれ、リフレクションを用いて実行時に取得できる@runtimeAttribute
、そして@propertyWrapper
でカスタム属性をサポートしようという議論があります。
property wrappersはまだまだ魅力的な使い方や制限などがありますが、まだ議論中であり、大きな機能なので別の機会に詳細を書きたいと思います。
おまけ
Function Builders
冒頭で出てきたSwiftUIのサンプルコードに以下のようなものがあります。
VStack(alignment: .leading) {
Text(item.title)
Text(item.subtitle)
.color(.gray)
}
一見するとクロージャの中でただText
をinitしているだけのように見えますが、実際には縦方向に並んだテキストが表示されます。
この機能(?)は謎だったんですが、WWDCの基調講演から数時間後にSwift Forumsに以下のスレッドが突如として生えました。
IMPORTANT: Evolution discussion of the new DSL feature behind SwiftUI - Pitches - Swift Forums
あまりに突然すぎて若干物議を醸していますが、要するにSwiftでDSLのような書き方をサポートするみたいです。
実際の議論は以下で行われています。
[Pitch] Function builders - Pitches - Swift Forums
DraftのProposalも以下にあります。
簡単な例を出すと以下のようになります。
// 以下のソースコードを書くと
@TupleBuilder
func build() -> (Int, Int, Int) {
1
2
3
}
// 以下のように書いたのと同じようになる
func build() -> (Int, Int, Int) {
let _a = 1
let _b = 2
let _c = 3
return TupleBuilder.buildBlock(_a, _b, _c)
}
実際の使い方など書いていきたいのですが、ifやforなどの扱い方や制限など、内容が大きく、またまだ議論段階なので詳しい説明はあとで別に書くなりしたいと思います(と言ってる間に誰かが詳しい説明を書いてくれることを期待...)
まだまだ他にも興味深いProposalはあるのですが、とりあえずこのへんで。
Proposalsを見てるだけでも今すぐ使いたい!と思うような機能がたくさんありました。
今後ますます便利になっていくといいなと思います!