LoginSignup
29
16

More than 3 years have passed since last update.

swift-evolution note 6/5

Last updated at Posted at 2019-06-05

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

またテンプレですが、間違いがあった場合などはご指摘をお願いします :bow:

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としてもRectangletopLeftにはアクセスできない(隠蔽される)
  • @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っぽくない

  • applyinvoke は SwiftのAPIガイドラインに沿っていない。

    • APIガイドラインでは副作用のあるなしで関数名を変更(副作用のある場合は動詞、ない場合は動詞+edなど)するが、applyinvokeは明らかに動詞
    • 一方callは動詞でも名詞でもあるのでこちらのほうが自然

また、以下のように特殊な文法を使う提案もありました。

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変更しています)

[Accepted with Modification] SE-0253 - Callable values of user-defined nominal types - Announcements - Swift Forums

また、しれっと書いてあるのですが、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の

個人的にはカンマをつけるのがめんどくさいときがあったのでなるほどなあという気持ちです。

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.

(from: https://forums.swift.org/t/important-evolution-discussion-of-the-new-dsl-feature-behind-swiftui/25168/49)

らしく、ステータスとしては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も以下にあります。

swift-evolution/XXXX-function-builders.md at 9992cf3c11c2d5e0ea20bee98657d93902d5b174 · apple/swift-evolution · GitHub

簡単な例を出すと以下のようになります。

// 以下のソースコードを書くと
@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を見てるだけでも今すぐ使いたい!と思うような機能がたくさんありました。
今後ますます便利になっていくといいなと思います!

29
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
16