演算子=へんな関数
連載第4回目の今回は、前回 予告どおりSwiftにおける演算子(operators)を詳しくみていきます。
その前に、演算子とは一体なんでしょうか?
なんだか難しそうな定義がありそうな気がしますが、Perlの父、Larry Wallが単純にして明快な定義を示してくれています。
> operators are just funny looking function calls [1]
「ただの変な見た目の関数呼び出し」 。たしかに言われてみると、演算子がなくとも一人前な言語はいくらでもあります。とくによく知られているのはLisp でしょう。1 + 1
ではなく(+1 1)
と書けば事足りますし、3 + 2 * 4
を計算したければ必ず(+ 3 (* 2 4))
と書くので優先順位の問題もありません。
実際、「 へんな関数」がないおかげでLisp の実装というのはずいぶんと楽で、Lisp in
ほげほげでほげほげ言語を検索すると、誰かが手なぐさみに実装したLispがいくつも見つかりますし、簡単なものは大学の学部生の課題として取り上げられたりします(私はSchemeでSchemeを書かされました。もう20年前) 。Swiftの母体ともなったLLVMがChris Lattnerの博士号の課題だったのと比べるとその難易度の差も感じられようというものです。
しかし、演算子がない言語は実装しやすい代わりに使いにくい。E = m * c ** 2
と書ける言語と、(setq E (* m (** c))
[2] と書かねばならない言語では、やはり前者の方が人気者。CもC++もJavaもJavaScriptもPerlもPHPもPythonも、本誌が好んで取り上げる言語はほとんど演算子を持っています。
しかし演算子を再定義したり、ましてや新演算子を定義できたりする言語となると、格段に減ります。Swiftはその数少ない例外。C++のような演算子オーバーロードはもちろん、Haskellのような優先順位定義までサポートしています。これはやるっきゃないでしょう。
二項演算子
前口上はこれくらいにして、実際に使ってみましょう。前回は、
struct Point {
var x:Int
var y:Int
}
をPrintable
にしたところで終わっていました。これをEquatable
にしてみます。equatableということは、等号があるということ。つまり==
を実装すればいいわけです。こういうふうに。
func ==(p:Point, q:Point)->Bool {
return p.x == q.x && p.y == q.y
}
こんな簡単でいいのでしょうか?
いいみたいです(図1 ) 。
図1 二項演算子の実行結果
二項演算子(binary operators)の場合、普通の関数と同じように定義できてしまいます。関数名は、演算子そのもの。簡単ですね。
前置と後置の場合は?
二項演算子の場合、位置に曖昧さはないのでこれで十分なのですが、単項演算子(unary operators)の場合、前置(prefi x)と後置(postfi x)の2つが考えられます。たとえば++
は前置と後置両方ありますよね。
前述のPoint
を++
したいケースというのはあまり考えられないのですが、ここでは便宜上x
とy
それぞれ++
するということにして、前置と後置を書き分けられるかを試してみることにしましょう。
前置のほうは簡単です。引数を上書きするので、inout
がついている点に注意してください。
prefix func ++(inout p:Point)->Point{
p.x++; p.y++; return p
}
後置のほうはちょっと工夫がいります。返り値は++
する前の値なので、前の値を一時変数に格納したうえでそれを返しています。
postfix func ++(inout p:Point)->Point {
var q = p
p.x++; p.y++; return q
}
それではうまくいくでしょうか。うまくいったみたいです(図2 ) 。引数がvar
ではなくlet
で宣言された定数の場合、きちんとエラーになっているところも含めて。
図2 前置と後置の実行結果
オレオレ演算子の書き方
ここまでは、既存の演算子をユーザが定義した型に適用していました。いわゆる演算子オーバーロードで、ここまでであればできる言語は少なくありません。しかしここから先はSwiftのほかにはHaskellぐらいしかできない領域、オレオレ演算子の世界です。
たとえばこんなのはいかがでしょう?
infix operator => { associativity left precedence 95 }
func => <A,R> (lhs:A, rhs:A->R)->R {
return rhs(lhs)
}
f(a)
の代わりに、a => f
と書けるようになります。
これの何がうれしいかというと、たとえばprintf
デバッグならぬprintln
デバッグの時。Swiftにはplaygroundがあるのでprintlnデバッグしなければならないケースは他より減るとはいえ、コマンドラインでのテストなどにやはり重宝します。こういう時にわざわざprintln(
と書いて)
で閉じるより、そのままうしろに=> println
と付け加えた方が楽ですし直感的でもあります。
見てのとおり、ユーザが演算子を定義する際には、(pre¦in¦post)fix operator
で演算子を定義しておく必要があります。続く{}
の中で、結合の方向と優先順位を定義します。ここでは左結合、優先順位95なので、=>
の重ね打ちも図3 のような感じでできたりします。
図3 オレオレ演算子の実行結果
演算子に関しては、「 こんなのどうよ」というのをGitHub にまとめてあるのでよろしければご確認を。
The (Un)?documented Feature
それではどのような記号が演算子として使えるのか。公式ドキュメントにはこう書いてあります(図4 ) 。
図4 公式ドキュメントでのU+の羅列?
U+の羅列、なんのこっちゃという感じですが、これ、Unicodeの記号文字です。ということは……、リスト1 のコードが動くということではありませんか? playgroundで実行してみてください(図5 ) 。
リスト1 公然の秘密のサンプルコード
import Foundation
prefix operator √ {}
prefix func √(x:Double)->Double {
return sqrt(x)
}
√2
// U+2211, N-ARY SUMMATION
// not U+3A3, GREEK CAPITAL LETTER SIGMA
infix operator Σ { }
func Σ (r:Range<Int>, f:Double->Double)->Double {
return Array(r).map{f(Double($0))}.reduce(0,+)
}
let s = 1...10 Σ {$0}
s
// U+220F, N-ARY PRODUCT
// not U+3A0, GREEK CAPITAL LETTER PI
infix operator Π { }
func Π (r:Range<Int>, f:Double->Double)->Double {
return Array(r).map{f(Double($0))}.reduce(1,*)
}
let p = 1...10 Π {$0}
p
図5 リスト1の実行
1つ注意したいのは、使えるのはあくまで記号であって、ギリシャ文字などは記号ではなく普通の文字として扱われること。図5のΣはU+3A3(GREEK CAPITAL LETTER SIGMA)ではなくU+2211(N-ARY SUMMATION) 、ΠもU+3A0(GREEK CAPITAL LETTER PI)ではなくU+220F(N-ARY PRODUCT)です。
記号が使えることはかつては公然の秘密でしたが、Swift 1.1現在では公式化されています。
さらに次のコードを追記してみてください。
for n in 1...16 {
let e_1 = 1...n Σ {1/(1...Int($0) Π {$0})}
println( exp(1) - e_1 )
}
どんどん1
に近づいています。どこかで見たことがありませんか? そう、ネイピア数、e
の定義です。
指数関数exp(x)はテイラー展開で
と書けますが、1...n Σ {1/(1...Int($0) Π {$0})}
はexp(1) - 1
に相当するわけです。なぜexp(1)そのものではないかというと、階乗計算に使っているΠ
は、上の定義では0!
が扱えないから。第0項がない分、1少ないわけです。
ここまでくるといっそ後置の!
を定義してn!
とかやりたい誘惑に駆られますが、残念ながらn!
はImplicitly Unwrapped Optionalsのために予約されていて再定義不可能です。
まとめ
Lispを見ればわかるとおり、演算子というのはプログラミング言語に必須の機能ではありません。が、やはりLispを見ればわかるとおり、演算子というのは一定以上の人気を得るのに欠かせない機能でもあるようです。この点において、Swiftに比肩するのはHaskellぐらいしか見当たりません。
そして一定以上の人気を得るのに欠かせないいまひとつの機能が、過去の遺産の継承。次回はSwiftからCやObjective-Cの遺産を活用します。
第1特集
MySQL アプリ開発者の必修5科目
不意なトラブルに困らないためのRDB基礎知識
第2特集
「知りたい」「 使いたい」「 発信したい」をかなえる
OSSソースコードリーディングのススメ
特別企画
企業のシステムを支えるOSとエコシステムの全貌
[特別企画]Red Hat Enterprise Linux 9最新ガイド
短期連載
今さら聞けないSSH
[前編]リモートログインとコマンドの実行
短期連載
MySQLで学ぶ文字コード
[最終回]文字コードのハマりどころTips集
短期連載
新生「Ansible」徹底解説
[4]Playbookの実行環境(基礎編)