書いて覚えるSwift入門

第2回総称関数!=関数?

総称関数の基本

連載第2回目の今回は、前回予告どおりSwiftにおける総称関数(Generic Functions)を詳しくみていきます。が、まず基本をおさらいしてみましょう。基本なので最も単純なケースから。最も単純な関数というと、引数をそのまま返す関数でしょう。

JavaScriptなら、

function identity(x) {
  return x;
}

Perlなら、

sub identity {
  shift;
}

Pythonなら

def identity(x):
  return x

Ruby なら、

def identity(x):
  x
end

どれも実質1行。簡単ですね。

ところが動的言語にとってこれほど簡単なことが、静的言語にとっては難しい。これをCでやってみようとすると……。

int int_identity(int x){
  return x;
}
double double_identity(double x){
  return x;
}
char *str_identity_s(char *x){
  return x;
}

要するに型の数だけ実装が必要になってしまうのです。中身が同じでも、型が違えば別物である以上、静的言語ではこれが当然でした。総称関数が登場するまでは。その総称関数で書くとどうなるのでしょうか?

こうなります。

func identity<T>(x:T)->T {
  return x
}

実際に動かして確認してみましょう図1⁠。

図1 総称関数を動かしてみる
図1 総称関数を動かしてみる
let i = identity(42)
let d = identity(42.195)
let s = identity("Marathon")

確かに動いています。

ここで<T>を取っ払ってみましょう。どうなりましたか? Use of undeclared type Tというエラーが出たはずです。これで<T>の役割がわかりました。Tは型の名前ではなく型の変数なので、コンパイル時に適切な型に置き換えた関数を作ってね」とSwiftにお願いしているわけです。

ここで問題です。上記のidentity(42)identityと、identity(42.195)identityは同じものでしょうか?

コンパイルされたコードを見てみればリスト1⁠、明らかです。

リスト1 identityの自分探し
% nm identity
0000000100000ee0 T __TF8identity8identityU__FQ_Q_
                 U __TFSSCfMSSFT21_builtinStringLiteralBp8byteSizeBw7isASCIIBi1__SS
                 U __TFSsa6C_ARGCVSs5Int32
                 U __TFSsa6C_ARGVGVSs20UnsafeMutablePointerGS_VSs4Int8__
                 U __TMdSS
                 U __TMdSd
                 U __TMdSi
0000000100001050 S __Tv8identity1dSd
0000000100001048 S __Tv8identity1iSi
0000000100001058 S __Tv8identity1sSS
0000000100000000 T __mh_execute_header
0000000100000f10 T _main
0000000100000e10 t _top_level_code
                 U dyld_stub_binder

_Tv8identity1までが同じで、後が異なるシンボルが3つ出てきました。プログラマが書いた1つのコードから、それぞれの型に対応する関数が3つ生成されたのです。

さらに、実際にidentityを使用している行をコメントアウトしてコンパイルしなおすと、シンボルはこうなりますリスト2⁠。

リスト2 identityのシンボル探し
% nm identity
0000000100000f00 T __TF8identity8identityU__FQ_Q_
                 U __TFSsa6C_ARGCVSs5Int32
                 U __TFSsa6C_ARGVGVSs20UnsafeMutablePointerGS_VSs4Int8__
0000000100000000 T __mh_execute_header
0000000100000f30 T _main
0000000100000ef0 t _top_level_code
                 U dyld_stub_binder

identityは完全に消えてしまいました。このことから、驚くべき結論が得られます。

総称関数は、関数ではないっ!

前回私はこう書きました。

「最低限文化的な関数型」の関数は第一級オブジェクト
  • 変数に代入できる
  • 関数の引数にできる
  • 関数を返す関数が書ける

総称関数は、この条件を満たしていないのです。

満たしているのであれば、次のようにも書けるはずですが、エラーになってしまいます。

let identity:<T>(T)->T = { x in
    return x
}

「関数は第1級オブジェクト」ということは、実体(instance)が存在するということですが、総称関数に実体はありません。クラスベースのオブジェクト指向言語におけるクラスに似ているといえば似ています。クラスはオブジェクトを生成するひな型ではあっても、オブジェクトそのものであるとは限らないという点において[1]⁠。

これで、キーワードfuncがいらない子でないことが証明されました。定義はできても代入はできないSwiftの総称関数には欠かせないのです。

型の型もやはり型

この総称関数は、⁠型が静的なのに動的言語のように書ける」Swiftの特長を実現するのに欠かせない機能となっています。たとえばsortを考えてみましょう。

let numbers = [3,2,1,0]
let strings = ["three","two","one","zero"]

let sorted_nums = numbers.sorted { $0 < $1 }
let sorted_strs = strings.sorted { $0 < $1 }

sorted_nums // [0, 1, 2, 3]
sorted_strs // ["one", "three", "two", "zero"]

この並べ替えを決める関数ブロック、同じ姿をしていますがそれぞれ別物です。前者は(Int,Int)->Bool後者は(String,String)->Bool。なのに同じように書けるのは、配列の方も総称的(generic)だから。⁠数値の配列」「文字列の配列」はそれぞれ別の型ですが、それぞれの型ごとにコードを書き下ろしているのではなく、総称的な型Array<T>が1つだけ定義してあって、そこからSwiftが適宜Array<Int>Array<String>を生成しているのです[2]⁠。もちろん「配列の配列」も可能で、その場合の型はArray<Array<T>>となるわけです。

型を型にはめるプロトコル

この総称型の「型変数」は、基本的にどんな型でも受け入れます。しかしそれでは困る状況も少なくありません。たとえば、次のコードを見てみましょう。

struct Point {
    var x:Int
    var y:Int
}
var origin = Point(x:0, y:0)
println(origin)

ここで何がprintlnされるでしょう? (x:0,y:0)とか(0, 0)とかとはなりません。Playgroundでは__lldb_expr_XXX.Pointswiftcでコンパイルされたコードではproto.Pointとかと、あまり意味のない文字列が出力されます。それもそのはず。Pointは自分がどうプリントされるべきかを知らないのです。どうやってPoint型にそれを教えてあげればいいのでしょうか?

まず、最初のstruct Pointの後ろに:Printableとつけてみてください。するとSwiftはType'Point' does not conform to protocol'Printable'と文句を言ってくるはずです。次に、Pointの定義の中でvar description:Stringを定義してみてください。まとめるとこんなふうに。

struct Point:Printable {
    var x:Int
    var y:Int
    var description: String {
        return "Point(x:\(x), y:\(y))"
    }
}
var origin = Point(x:0, y:0)
println(origin)

これをコンパイルすると、確かにPoint(x:0,y:0)と出てきます。

Playgroundだと、__lldb_expr_XXX.Pointのママなのですが、これはXcodeのバグですね:-p

この、型に施す制約のことを、Swiftではプロトコル(protocol)と呼びます。struct PointPrintableプロトコルに準拠している。なぜならdescriptionプロパティを持つからだ⁠⁠。プロトコルという言葉は本誌の読者であれば毎号必ず目にしているかと思いますが、本来の意味はなんだったでしょうか?辞書を引くとはじめに「外交儀礼。国際儀礼」と出てきます。国際会議を上手に進めるには、どこで会議をするか、何語で会議をするかといったことを会議の前に決めておかなければなりません。たとえば講話条約を、敵国の首都で敵国語で進めるとなったら無条件降伏でもないかぎり会議に出席する気にもならないでしょう。何を進めるかは未定でも、どう進めるかが決まっていれば進めることはできるのです。Swiftにおける「プロトコル」という言葉はその意味において正しい選択だと思います。

上記のPointでは、ユーザ定義の型を特定のプロトコルに準拠する方法を見ましたが、総称型や総称関数の方で特定のプロトコルに準拠した型だけ受け付けるよう指定することも当然できます。たとえば辞書、Dictionaryは次のように定義されています[3]⁠。

struct Dictionary<Key:Hashable, Value>

「辞書の値Valueはどんな型でもOKだけれども、KeyHashableプロトコルに準拠したもののみですよ」ということです。実際、先ほどのPoint

var fromto = [origin:origin]

と書くと、Type 'Point' does not conform toprotocol 'Hashable'と文句を言ってきます。

それでは、PointHashableプロトコルに準拠させるにはどうしたらよいでしょうか? Printableの時と同様に、var hashValue:Intを定義すればOKかと思いきや、今度はType 'Point'does not conform to protocol 'Equatable'と文句を言ってきます図2⁠。Equatable? 要は等号が定義されていればいいの?

図2 protocolの実行例
図2 protocolの実行例

そうなんです。実は==の再定義も可能なんですよ。Swiftならね。

おわりに

次回はいよいよ私の一番お気に入りのSwiftの機能、ユーザ演算子定義を見ていきます。乞うご期待!

Software Design

本誌最新号をチェック!
Software Design 2022年9月号

2022年8月18日発売
B5判/192ページ
定価1,342円
(本体1,220円+税10%)

  • 第1特集
    MySQL アプリ開発者の必修5科目
    不意なトラブルに困らないためのRDB基礎知識
  • 第2特集
    「知りたい」⁠使いたい」⁠発信したい」をかなえる
    OSSソースコードリーディングのススメ
  • 特別企画
    企業のシステムを支えるOSとエコシステムの全貌
    [特別企画]Red Hat Enterprise Linux 9最新ガイド
  • 短期連載
    今さら聞けないSSH
    [前編]リモートログインとコマンドの実行
  • 短期連載
    MySQLで学ぶ文字コード
    [最終回]文字コードのハマりどころTips集
  • 短期連載
    新生「Ansible」徹底解説
    [4]Playbookの実行環境(基礎編)

おすすめ記事

記事・ニュース一覧