書いて覚えるSwift入門

第3回演算子

演算子=へんな関数

連載第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 二項演算子の実行結果
図1 二項演算子の実行結果

二項演算子(binary operators)の場合、普通の関数と同じように定義できてしまいます。関数名は、演算子そのもの。簡単ですね。

前置と後置の場合は?

二項演算子の場合、位置に曖昧さはないのでこれで十分なのですが、単項演算子(unary operators)の場合、前置(prefi x)と後置(postfi x)の2つが考えられます。たとえば++は前置と後置両方ありますよね。

前述のPoint++したいケースというのはあまり考えられないのですが、ここでは便宜上xyそれぞれ++するということにして、前置と後置を書き分けられるかを試してみることにしましょう。

前置のほうは簡単です。引数を上書きするので、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 前置と後置の実行結果
図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 オレオレ演算子の実行結果
図3 オレオレ演算子の実行結果

演算子に関しては、⁠こんなのどうよ」というのをGitHubにまとめてあるのでよろしければご確認を。

The (Un)?documented Feature

それではどのような記号が演算子として使えるのか。公式ドキュメントにはこう書いてあります図4⁠。

図4 公式ドキュメントでのU+の羅列?
図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の実行
図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の遺産を活用します。

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の実行環境(基礎編)

おすすめ記事

記事・ニュース一覧