総称関数の基本
連載第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 総称関数を動かしてみる
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.Point
、swiftcでコンパイルされたコードでは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 Point
はPrintable
プロトコルに準拠している。なぜならdescription
プロパティを持つからだ」 。プロトコルという言葉は本誌の読者であれば毎号必ず目にしているかと思いますが、本来の意味はなんだったでしょうか?辞書を引くとはじめに「外交儀礼。国際儀礼」と出てきます。国際会議を上手に進めるには、どこで会議をするか、何語で会議をするかといったことを会議の前に決めておかなければなりません。たとえば講話条約を、敵国の首都で敵国語で進めるとなったら無条件降伏でもないかぎり会議に出席する気にもならないでしょう。何を進めるかは未定でも、どう進めるかが決まっていれば進めることはできるのです。Swiftにおける「プロトコル」という言葉はその意味において正しい選択だと思います。
上記のPoint
では、ユーザ定義の型を特定のプロトコルに準拠する方法を見ましたが、総称型や総称関数の方で特定のプロトコルに準拠した型だけ受け付けるよう指定することも当然できます。たとえば辞書、Dictionary
は次のように定義されています[3] 。
struct Dictionary<Key:Hashable, Value>
「辞書の値Value
はどんな型でもOKだけれども、Key
はHashable
プロトコルに準拠したもののみですよ」ということです。実際、先ほどのPoint
を
var fromto = [origin:origin]
と書くと、Type 'Point' does not conform toprotocol 'Hashable'
と文句を言ってきます。
それでは、Point
をHashable
プロトコルに準拠させるにはどうしたらよいでしょうか? Printable
の時と同様に、var hashValue:Int
を定義すればOKかと思いきや、今度はType 'Point'does not conform to protocol 'Equatable'
と文句を言ってきます(図2 ) 。Equatable? 要は等号が定義されていればいいの?
図2 protocolの実行例
そうなんです。実は==
の再定義も可能なんですよ。Swiftならね。
[3] 配列同様、辞書はとてもよく使われる方なので、[Key, Value]という表記も定義されています。
おわりに
次回はいよいよ私の一番お気に入りのSwiftの機能、ユーザ演算子定義を見ていきます。乞うご期待!
第1特集
MySQL アプリ開発者の必修5科目
不意なトラブルに困らないためのRDB基礎知識
第2特集
「知りたい」「 使いたい」「 発信したい」をかなえる
OSSソースコードリーディングのススメ
特別企画
企業のシステムを支えるOSとエコシステムの全貌
[特別企画]Red Hat Enterprise Linux 9最新ガイド
短期連載
今さら聞けないSSH
[前編]リモートログインとコマンドの実行
短期連載
MySQLで学ぶ文字コード
[最終回]文字コードのハマりどころTips集
短期連載
新生「Ansible」徹底解説
[4]Playbookの実行環境(基礎編)