LoginSignup
26
11

More than 1 year has passed since last update.

Swiftの数値リテラルをチョットダケ詳しく調べた話

Last updated at Posted at 2018-12-13

こんにちは、freddiと申します。Swiftを始めて8ヶ月少々、最近は周囲の影響からかSwiftコンパイラやSILについてちょっと勉強し始めています。

この記事は、「Swift Advent Calendar 2018」の14日目の記事として、Swiftの数値リテラルを、基本的なところから、普段のSwiftコードよりもちょっと低いレイヤーを覗いたり、実際に触れ合って見た話をします。
皆様、どうか最後までよろしくお願いいたします。

この記事では以下のSwiftコンパイラを利用して検証を行っています。

$ swiftc --version
Apple Swift version 4.2.1 (swiftlang-1000.11.42 clang-1000.11.45.1)
Target: x86_64-apple-darwin17.7.0

Swiftの数値リテラルの基本

この項目では、数値リテラルの基本をおさらいします。一部の方々には復習になるとは思いますので、ちょっとだけお付き合いください。

数値リテラルの種類(n進数)

Swiftでは、数値リテラルと言ってもいくつか種類があります。Swift.orgのLanguage Guideを見れば、17を表すリテラルにも以下の4種類が存在することが確認できます。

// Language GuideのThe BasicsのNumeric Literalsから引用しています
let decimalInteger = 17           // 17 を10進数(decimal number)で表したもの
let binaryInteger = 0b10001       // 17 を2進数(binary number)で表したもの
let octalInteger = 0o21           // 17 を8進数(octal number)で表したもの
let hexadecimalInteger = 0x11     // 17 を16進数(hexadecimal)で表したもの

10進数は接頭詞がなく、それ以外は接頭詞があるというのが、それぞれの特徴ですね。

小数を含んだ数値の表現

また、10進数と16進数は、小数点以下の数を含んだ表現が可能です。

10進数では.を利用した表現と、e($ 10^e $の指数部 $ e $)を利用した表現があります。これらは馴染みが深いと思います。

let decimal0Point01Value = 0.01       // 0.01 を10進数で表したもの
let decimal0Point01ValueWithE = 1e-2  // 0.01 をeを利用して10進数で表したもの -> 1 * 10^-2
let decimal100ValueWithE = 1e+2       // 100  をeを利用して10進数で表したもの -> 1 * 10^2

16進数では.を利用した表現はありません。試しにlet someValue = 0x0b.1bをコンパイルしたら、以下のようなエラーが出ます。1

hexadecimal floating point literal must end with an exponent
翻訳: 16進浮動小数点リテラルは指数で終わらなければならなりません

ですが、p($ 2^p $の指数部 $ p $)を利用して小数点を表現することが可能です。 
上記のエラーで出ている指数で終わらなければならなりません指数pのことのようです。また、指数部は10進数しか指定できません。

let hexadecimal0Point25ValueWithP = 0x01p-2  // 0.25 をpを利用して16進数で表したもの -> 1 * 2^-2

また、eとpを利用した数値リテラル単体は、Double型になるようです

let someHexValue = 0x0p0
print(type(of: someHexValue)) // -> Double

let someDecValue = 0e0
print(type(of: someDecValue)) // -> Double

数値リテラルの可読性を上げる書き方

数値の桁の可読性が上がる表現として、_を利用した桁区切りの表記もあります。これは小数点以下の桁にも、どの進数の表記でも利用できます(この場合、_は、コンパイル時には無視されます)。

let someBigValue = 1_000_000.000_001

また、0を利用したパディング(余白詰め)も利用できます。これは10進数のみのようです。

let someBigValue1 = 000010
let someBigValue2 = 012000

数値リテラルの取り扱われ方

では、ここから数値リテラルについてもう少し掘り下げて探検してみます。ここからは私達が普段いるSwiftコードよりも、もう少し低い中間言語の部分から見て行くことが少々多くなりますが、必要なところだけ解説を入れているので、そこだけ読んでもらっても結構です。

整数リテラルと浮動小数点リテラル

数値リテラルには、宣言時にデフォルトで設定される型があります。

たとえば、以下のように小数点が現れない数値リテラルはすべて整数型(Int)として設定されます。これらは 整数リテラル と呼ばれていることが多いです。

let someValue1 = 0
print(type(of: someValue1)) // -> Int

let someValue2 = 0x0
print(type(of: someValue2)) // -> Int

let someValue3 = 0o0
print(type(of: someValue3)) // -> Int

以下のように「小数点が出る、指数がついている」数値リテラルはすべて浮動小数点型(Int)として設定されます。これらは 浮動小数点リテラル と呼ばれていることが多いです。

let someValue1 = 0.0
print(type(of: someValue1)) // -> Double

let someValue2 = 0e1
print(type(of: someValue2)) // -> Double

let someValue3 = 0x0p0
print(type(of: someValue3)) // -> Double

では、整数リテラルと浮動小数点リテラルは、コンパイルされているときにどのように扱われるのでしょうか?以下のソースコードのSIL2を読んで考察してみようと思います。

let someIntValue    = 3
let someDoubleValue = 4.1

出力されたSILを見てみましょう(読みやすいようにSwiftのシンタックスハイライトを入れてます)。
まず、someIntValueの宣言のSILを見てみます。

  alloc_global @$S4test12someIntValueSivp         // id: %2
  %3 = global_addr @$S4test12someIntValueSivp : $*Int // user: %8
  %4 = metatype $@thin Int.Type                   // user: %7
  %5 = integer_literal $Builtin.Int2048, 3        // user: %7
  // function_ref Int.init(_builtinIntegerLiteral:)
  %6 = function_ref @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %7
  %7 = apply %6(%5, %4) : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %8
  store %7 to [trivial] %3 : $*Int                // id: %8

このSILコードをざっくり解説すると、

  1. 最初の方に出てくる%5 = integer_literal $Builtin.Int2048, 3がいわゆる整数リテラルです。
  2. そのリテラルを、Int.init(_builtinIntegerLiteral:)でIntに変換しています(%6 = ...%7 = ...の部分)。
  3. そして、変換したものをsomeIntValueに格納しています。(store %7 to [trivial] %3 : $*Int

次にsomeDoubleValueの宣言のSILを見てみます。

  alloc_global @$S4test15someDoubleValueSdvp      // id: %9
  %10 = global_addr @$S4test15someDoubleValueSdvp : $*Double // user: %15
  %11 = metatype $@thin Double.Type               // user: %14
  %12 = float_literal $Builtin.FPIEEE80, 0x40018333333333333333 // 4.09999999999999999991 // user: %14
  // function_ref Double.init(_builtinFloatLiteral:)
  %13 = function_ref @$SSd20_builtinFloatLiteralSdBf80__tcfC : $@convention(method) (Builtin.FPIEEE80, @thin Double.Type) -> Double // user: %14
  %14 = apply %13(%12, %11) : $@convention(method) (Builtin.FPIEEE80, @thin Double.Type) -> Double // user: %15
  store %14 to [trivial] %10 : $*Double           // id: %15

最初の方に出てくる%12 = float_literal $Builtin.FPIEEE80, 0x40018333333333333333がいわゆる浮動小数点リテラルです。
0x400183333333333333334.1を80bit浮動小数点数に変換したものです。どうやらSILになる時点では浮動小数点リテラルは80bit浮動小数点数に変換されるらしいです。3また、環境によっては64bitになるかもしれません

そのリテラルを、Double.init(_builtinFloatLiteral:)で... とIntのときと同じようにリテラルをDoubleのイニシャライザで変換しています。

このように、型アノテーションがない限りは、各リテラルはコンパイラ側でデフォルトで決まっている型になるようです。数値にかかわらず、どのリテラルがデフォルトでどの型になるかは、ここを見ればわかると思います

これらのことから、数値リテラルは、型アノテーションがない限りは、指定されたデフォルトの型の(リテラルを引数に取る)イニシャライザを通して、デフォルトの型になる と考えられます。

ExpressibleByIntegerLiteralプロトコルについて

最後にですが、数値リテラルと関係のあるprotocolであるExpressibleByIntegerLiteralを使ってちょっと遊んでみた話をします。

ExpressibleByIntegerLiteralとは?

ここで、ちょっと皆さんに質問です。以下のコードは、コンパイル可能でしょうか?

let someValue: CGFloat = 10

ほとんどの人が知っていると思いますが、このコードはコンパイルが可能です。(当然と思った方はすいません。)

私自身、この一行がコンパイルが通ることには、最初はかなり疑問点を抱きました。CGFloatはかなり使っていましたが、リテラルを直接代入できることを知らない間はずっと

let someValue: CGFloat = CGFloat(10)

のように書いていました。どのようにすれば、自分が作った型で直接数値リテラルを代入して変数宣言ができるようになるのでしょうか。

例として、OriginalWrappingValueTypeという型を作ってみます。この型は整数リテラルをイコールで代入する形で宣言できるようにするのが目標です。

import Foundation

struct OriginalWrappingValueType {
  var value: Int
}

let someValue: OriginalWrappingValueType = 10 // 目標!だけどコンパイルはできません

知っている人もいるかも知れませんが、Numericプロトコルに準拠させればできないことはないです。

struct OriginalWrappingValueType: Numeric {
  ...

しかし、整数リテラルの直接代入はNumericの機能ではなく、Numericがさらに準拠しているExpressibleByIntegerLiteralの準拠によって可能になります

ちなみに、ExpressibleByIntegerLiteralは、コメントで組み込みプロトコル(Intrinsic protocols)と書かれるほど、コンパイラのコアな部分に繋がりのあるプロトコルです

では、早速OriginalWrappingValueTypeを作り、ExpressibleByIntegerLiteralに準拠させてみましょう。

import Foundation

struct OriginalWrappingValueType: ExpressibleByIntegerLiteral {
  var value: Int
}

let someValue: OriginalWrappingValueType = 10

コンパイルするとエラーが出ます。どうやら、準拠するには必要なことがありそうですね。

test.swift:3:8: error: type 'OriginalWrappingValueType' does not conform to protocol 'ExpressibleByIntegerLiteral'
struct OriginalWrappingValueType: ExpressibleByIntegerLiteral {
       ^
Swift.ExpressibleByIntegerLiteral:2:20: note: protocol requires nested type 'IntegerLiteralType'; do you want to add it?
    associatedtype IntegerLiteralType : _ExpressibleByBuiltinIntegerLiteral

実はinit(integerLiteral:)だけを実装すればよいです。

struct OriginalWrappingValueType: ExpressibleByIntegerLiteral {
  var value: Int

  public init(integerLiteral value: Int) {
    self.value = value
  }
}

これで、コンパイルエラーも出なくなり、OriginalWrappingValueType型の変数は整数リテラルをイコールで代入する形で宣言できるようになりました。

import Foundation

struct OriginalWrappingValueType: ExpressibleByIntegerLiteral {
  var value: Int

  public init(integerLiteral value: Int) {
    self.value = value
  }
}

let someValue: OriginalWrappingValueType = 10 // = でセットできるようになった!

コンパイラ側では、もしリテラルがOriginalWrappingValueType型の変数に=代入される際に、先程実装したinit(integerLiteral value: Int)を呼び出して変換しているそうです。

SILを出力してみたところ、init(integerLiteral value: Int)が確かに呼ばれていることがわかりました。

  alloc_global @$S4test9someValueAA016OriginalWrappingC4TypeVvp // id: %2
  %3 = global_addr @$S4test9someValueAA016OriginalWrappingC4TypeVvp : $*OriginalWrappingValueType // user: %11
  %4 = metatype $@thin OriginalWrappingValueType.Type // user: %10
  %5 = metatype $@thin Int.Type                   // user: %8
  %6 = integer_literal $Builtin.Int2048, 10       // user: %8
  // function_ref Int.init(_builtinIntegerLiteral:)
  %7 = function_ref @$SSi22_builtinIntegerLiteralSiBi2048__tcfC : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %8
  %8 = apply %7(%6, %5) : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int // user: %10
  // function_ref OriginalWrappingValueType.init(integerLiteral:)
  // しっかりとここでExpressibleByIntegerLiteralのinit(integerLiteral:)が呼ばれている!!
  %9 = function_ref @$S4test25OriginalWrappingValueTypeV14integerLiteralACSi_tcfC : $@convention(method) (Int, @thin OriginalWrappingValueType.Type) -> OriginalWrappingValueType // user: %10
  %10 = apply %9(%8, %4) : $@convention(method) (Int, @thin OriginalWrappingValueType.Type) -> OriginalWrappingValueType // user: %11
  store %10 to [trivial] %3 : $*OriginalWrappingValueType // id: %11

先程の項で話した整数リテラルがInt型の変数になるフローと似ている気がします。
浮動小数点リテラルに関しては、ExpressibleByFloatLiteralに同じように準拠すればよいです。

ExpressibleBy...Literalと型チェックの考察

ExpressibleBy...Literalで遊んでいると、おもしろい面をもう一つ発見しました。

CGFloatとリテラルの演算で型チェックに時間がかかってしまったという記事も紹介されていますが、この記事では、以下のようなリテラルとCGFloatの混ざった計算で、型チェックにかなり時間がかかっていました。

let width = (view.bounds.width - 10 * 2 - collectionViewLayout.minimumInteritemSpacing * 4) / 4

この記事では、結局はコード内の数値リテラルをCGFloatに明示的に変換することで、型チェックの時間を削減に成功していました。

実際に、個人の環境で同じようなシチュエーションを再現しようとしました。しかし、実装中のコンパイル時に不思議な問題に直面しました。

以下が、私が問題に直面したコードです。

import Foundation

struct OriginalWrappingValueType: ExpressibleByIntegerLiteral {

  var value: Int

  public init(value: Int) {
    self.value = value
  }

  public init(integerLiteral value: Int) {
    self.value = value
  }

  static func +(lhs: OriginalWrappingValueType, rhs: OriginalWrappingValueType) -> OriginalWrappingValueType {
    return OriginalWrappingValueType(integerLiteral: lhs.value + rhs.value)
  }
  // 演算子のどれかの実装をあえて抜くのがポイント
  // static func -(lhs: OriginalWrappingValueType, rhs: OriginalWrappingValueType) -> OriginalWrappingValueType {
  //   return OriginalWrappingValueType(integerLiteral: lhs.value - rhs.value)
  // }

  static func /(lhs: OriginalWrappingValueType, rhs: OriginalWrappingValueType) -> OriginalWrappingValueType {
    return OriginalWrappingValueType(integerLiteral: lhs.value / rhs.value)
  }

  static func *(lhs: OriginalWrappingValueType, rhs: OriginalWrappingValueType) -> OriginalWrappingValueType {
    return OriginalWrappingValueType(integerLiteral: lhs.value * rhs.value)
  }
}

let someValue1 = (OriginalWrappingValueType(value: 10) * 5 - 5) / 4 - 5
let someValue2 = (OriginalWrappingValueType(value: 10) * 5 - OriginalWrappingValueType(value: 10)) / 4 - 5

コンパイルは失敗します。最後の計算時にOriginalWrappingValueType-の演算の実装が見つからないからです。

このコードは私の環境では、コンパイル結果が出るまでに700~1200ミリ秒時間を要していました。
試しに、以下のような複雑な演算で試してみました。

let someValue = (OriginalWrappingValueType(value: 10 / 5) + (5 * 4) - 4 * 5 + OriginalWrappingValueType(value: 4) / 2) / 5 - 10

すると、コンパイルエラーはもちろん、型チェックに35秒要するという結果になりました。

最後のコードで、コンパイルオプションで-dump-astを指定して、抽象構文木を出力するようにしました。すると、各ノードで大量の型を列挙している構文木が出力されました3

以下がその構文木です。(見やすくなるようにswiftのシンタックスハイライトを適用しています。)

  (top_level_code_decl
    (brace_stmt
      (pattern_binding_decl
        (pattern_named type='<<error type>>' 'someValue')
        (binary_expr type='<<error type>>' location=test.swift:32:124 range=[test.swift:32:17 - line:32:126]
          (overloaded_decl_ref_expr type='<null>' name=- number_of_decls=29 function_ref=unapplied decls=[
            Swift.(file).Float.-,
            Swift.(file).Double.-,
            Swift.(file).Float80.-,
            Swift.(file).UInt8.-,
            Swift.(file).Int8.-,
            Swift.(file).UInt16.-,
            Swift.(file).Int16.-,
            Swift.(file).UInt32.-,
            Swift.(file).Int32.-,
            Swift.(file).UInt64.-,
            Swift.(file).Int64.-,
            Swift.(file).UInt.-,
            Swift.(file).Int.-,
            Foundation.(file).Date.-,
            Foundation.(file).Decimal.-,
            Dispatch.(file).-,
            Dispatch.(file).-,
            Dispatch.(file).-,
            Dispatch.(file).-,
            CoreGraphics.(file).CGFloat.-,
            Swift.(file).FloatingPoint.-,
            Swift.(file).Numeric.-,
            Swift.(file).BinaryInteger.-,
            Swift.(file).Strideable.-,
            Swift.(file).Strideable.-,
            Swift.(file).Strideable.-,
            Swift.(file).Strideable.-,
            Foundation.(file).Measurement.-,
            Foundation.(file).Measurement.-])
          (tuple_expr implicit type='<null>'
            (binary_expr type='<null>'
              (overloaded_decl_ref_expr type='<null>' name=/ number_of_decls=20 function_ref=unapplied decls=[
                test.(file).OriginalWrappingValueType./@test.swift:23:15,
                Swift.(file).Float./,
                Swift.(file).Double./,
                Swift.(file).Float80./,
                Swift.(file).UInt8./,
                Swift.(file).Int8./,
                Swift.(file).UInt16./,

// かなり長いので中略
// かなり長いので中略
// かなり長いので中略
                              Swift.(file).FloatingPoint.*,
                              Swift.(file).Numeric.*,
                              Swift.(file).BinaryInteger.*,
                              Foundation.(file).Measurement.*,
                              Foundation.(file).Measurement.*])
                            (tuple_expr implicit type='<null>'
                              (integer_literal_expr type='<null>' value=4)
                              (integer_literal_expr type='<null>' value=5)))))
                      (binary_expr type='<null>'
                        (overloaded_decl_ref_expr type='<null>' name=/ number_of_decls=20 function_ref=unapplied decls=[
                          test.(file).OriginalWrappingValueType./@test.swift:23:15,
                          Swift.(file).Float./,
                          Swift.(file).Double./,
                          Swift.(file).Float80./,
                          Swift.(file).UInt8./,
                          Swift.(file).Int8./,
                          Swift.(file).UInt16./,
                          Swift.(file).Int16./,
                          Swift.(file).UInt32./,
                          Swift.(file).Int32./,
                          Swift.(file).UInt64./,
                          Swift.(file).Int64./,
                          Swift.(file).UInt./,
                          Swift.(file).Int./,
                          Foundation.(file).Decimal./,
                          CoreGraphics.(file).CGFloat./,
                          Swift.(file).FloatingPoint./,
                          Swift.(file).BinaryInteger./,
                          Foundation.(file).Measurement./,
                          Foundation.(file).Measurement./])
                        (tuple_expr implicit type='<null>'
                          (call_expr type='<null>' arg_labels=value:
                            (type_expr type='<null>' typerepr='OriginalWrappingValueType')
                            (tuple_expr type='<null>' names=value
                              (integer_literal_expr type='<null>' value=4)))
                          (integer_literal_expr type='<null>' value=2))))))
                (integer_literal_expr type='<null>' value=5)))
            (integer_literal_expr type='<null>' value=10))))

Swift.(file).Float./Swift.(file).FloatingPoint.*の一番最後の記号は、それぞれ+-*/が確認できるので演算子だと考えられます。
OriginalWrappingValueTypeも演算子の列挙に含まれていましたが、-が列挙されている場所にはありませんでした。
また、-演算子を実装すれば、エラーも出ずコンパイルもすぐに終了します。

これらから、型チェックに時間がかかる理由は、

  • ExpressibleByIntegerLiteralに準拠していて」かつ「四則演算のうちどれかを実装している」型は、式中の数値リテラルの変換先の型のパターンマッチングに利用されている4
  • 式が複雑になる、リテラルが増える、といったリテラルの型がすぐに決めづらい状況になればなるほど、パターンマッチングの走査の時間も増えてしまう

これらが原因なのかな、と考えています。まだ予測の段階なので、今後コンパイラのコードを読んでみて結論をつけたいと思っています。

最後に

数値リテラルを基本的な部分から、普段は意識しないようなちょっとレイヤーの低いところまで見ることで、数値リテラルがコンパイラでどのように取り扱われているかを知ることができました。役に立つ役に立たない関係なく、数値リテラルの世界やちょっと低いレイヤーに興味を持ってもらえれば、私として嬉しいです。もしミス等が発見されましたら、お気軽に修正リクエストやコメントをお願いします。

最後の項目では、(まだ考察ですが)ちょっとリテラルに対してマイナスなイメージを持ってしまった方々もいるかも知れません。
ですが、数日前のSwiftアドカレにも書いてあるとおり、Swiftコンパイラに優しいコードを書けば、私が今回挙げた不思議な問題点も簡単に解決できると思っています(例えば、紹介したCGFloatの記事では明示的に数値リテラルを変換して、コンパイラの型チェックを円滑に行えるようにしています)。

本日はここまでです。皆様本当にありがとうございました!

明日はkateinoigakukunさんがメタデータ周りに付いて書いてくださるそうです。メタデータの話はあまり聞いたことが無いので、とても楽しみです。皆さんもお楽しみください!

追記解説

@omochimetaru さんからのコメントを紹介します。最後の章には特に大きく関わります。

Swiftの型推論において、オーバーロードされた関数があると、オーバーロードのうちのどれを選択すれば型推論が成功するのかを全て試さねばなりません。正解が複数ある場合はambiguousであるとしてエラー報告も必要です。ある式の中にk個オーバーロードされた関数がn個含まれているとき、k^nのパターンのチェックが必要になるわけです。演算子というのは見かけ上は中置していますが本質的にトップレベル関数なので、対応している型の数だけオーバーロードされているのと同じです。また、ある種類のリテラルからある型への暗黙変換も、変換先の型の数だけ候補があります。なので、演算子やリテラルを混ぜた式を作るとあっという間に組み合わせ爆発しまって推論が終わらなくなる、ということが起きているようです。

このことについて解説します。

型チェック時に、オーバーロードされた関数はすべてコンパイル可能か走査される

このことは、apple/swiftのdoc/Literals.rst でも言及されていました。

In order to make this actually happen, the type-checker has to do some fancy footwork. Remember, at this point all we have is a string literal and an expected type; if the function were overloaded, we would have to try all the types.

This algorithm can go forwards or backwards, since it's actually defined in terms of constraints, but it's easiest to understand as a linear process.

Swiftでは、コメントにもある通り演算子も関数です。最後の章の実験ではOriginalWrappingValueTypeで各四則演算の関数をオーバーロードしてましたね。

実際に、オーバーロードする四則演算を関数を増やすと、


  1. 0x0b.bのように.以下がアルファベットで始まる際は、Intのメンバを呼び出ししているようにコンパイラに認識されるので、別のエラーが出ます(例: error: value of type 'Int' has no member 'b') 。 10進数以外で.が使えない理由は、メンバの呼び出しかそうでないかの構文解析時の判断が難しいからかな、と考えています(これは私の憶測なのでご注意ください)。 

  2. Swift Intermediate Languageの略。Swiftの中間言語のことです。詳しくは https://blog.waft.me/2018/01/09/swift-sil-1/ のSIL (Swift Intermediate Language)という項目を見てください。この記事では主に、-emit-silgenで出力したrawSILを載せています。 

  3. 余談ですが、抽象構文木の時点では、指数表現があるリテラル以外はすべて10進数に変換されます。SILのときは指数部分も展開されますか、浮動小数点リテラルは先程も述べたとおり80bit浮動小数点数データで表記されます。 

  4. OriginalWrappingValueTypeをリネームしただけのAnotherOriginalWrappingValueTypeを別途用意したところ、最後の行の式でAnotherOriginalWrappingValueTypeが関連している演算が無いのにもかかわらず、構文木中の型の列挙にはAnotherOriginalWrappingValueTypeがありました 

26
11
2

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
26
11