LoginSignup
53
53

More than 5 years have passed since last update.

Swift * Operators === Nirvana!

Last updated at Posted at 2014-07-24

Xcode6-Beta4 に続いて Yosemite Public Beta のダウンロードもはじまりました。Swiftいつやるか?今でしょ!って感じになってまいりました。

しかし言語なんていくらでもあるのにわざわざ新しいのをやるにはやはり理由も必要かも知れません。演算子なんていかがでしょう?

まずは "3" * 3 == "333" を実装してみる

文字列の「かけ算」は、PerlやRubyではおなじみかと思います。Swiftでも実装してみましょう。

func * (lhs:String, rhs:Int)->String {
    return "".join(Array(count:rhs, repeatedValue:lhs))
}

実際にPlaygroundで試してみてみましょう。

3 * 3
"3" * 3

それぞれ9,"333"と表示されればうまく行っています。「ふつう」の乗算も今までどおりできてます。楽勝ですね。

かけ算なら順序は関係ないよねっ?

ところで Perl の x 演算子も Ruby の String#:* も、繰り返し回数は後ろにつけます。しかしそうでなければならない理由ってあるのでしょうか?否否否否、断じて否!

というわけで3 * "3" == "333"も成立するようにしてみましょうか。

func * (lhs:Int, rhs:String)->String {
    return rhs * lhs
}

なんとこれだけ。これと再帰的に呼び出されているように見えますが違います。Swiftでは、名前だけではなく引数の型まで含めて関数のフルネームで、演算子もただの関数なので、先ほど定義した*(String, Int)を呼び出しているだけです。

折角ですから Ruby でも出来るようにしてみますか。

こんな感じ、ですかね。

class Fixnum
  alias __mul__ :*
  def *(rhs)
    puts "#{caller.first}: #{self} * #{rhs.inspect}"
    if rhs.is_a?(String)
      rhs.send(:*, self)
    else
      self.__mul__(rhs)
    end
  end
end

irbで試してみましょう。

irb(main):001:0> require "./overload.rb"
=> true
irb(main):002:0> 3*3
(irb):2:in `irb_binding': 3 * 3
=> 9
irb(main):003:0> 3*"3"
(irb):3:in `irb_binding': 3 * "3"
=> "333"

確かに期待通り動いてます。が、Rubyが孕む問題もこれで見えてきます。一度これをかましてしまうと、lhs * rhsrhsが何であろうが必ず新定義が呼ばれてしまうのです。

そのために上記のコードでは、まず旧定義をFixnum#__mul__に逃し、rhsのクラスを確認した上で振る舞いを切り替えています。つまり、演算子に新しい意味を与えようとする際には、元の意味を壊さないような配慮が必ず必要となってしまうのです。

そしてその配慮が、パフォーマンス面への悪影響を避けられないものともしています。数値*数値というのはかなり頻繁に行うオペレーションなのに、必ずクラス判定と再ディスパッチが発生してしまうというのはかなり痛い。Rubyをはじめ、二項演算子を左辺オブジェクトへのメッセージとして実装している言語において、演算子改革は必ず痛みを伴うのです。

これに対し、SwiftやC++の演算子オーバーロードではこの問題が全く発生しません。同じに見えても実は全く別の関数を呼び出しているだけなのですから。

#include <string>
#include <iostream>
using namespace std;
string operator*(const string str, int n) {
    string result = "";
    for (int i = 0; i < n; i++) {
        result += str;
    }
    return result;
}
string operator*(int n, const string str) {
    return str * n;
}
int main() {
    cout << string("3 * 3 == ")              << 3 * 3 << endl;
    cout << string("3 * \"3\" == ") + 3 * string("3") << endl;
}

余談ですが、Perl6はスクリプト言語でありながら引数の型による実装の振り分けをサポートしています(multi dispatch)。それ以前に速度的に実用になる実装がないのが残念ではありますが。

* がなければ *~ を使えばいいじゃない

ここで Haskell に登場してもらいましょう。3 * "3"はこんな感じでしょうか。

Prelude> let (*) n xs = concat $ take n $ repeat xs
Prelude> 3 * "3"
"333"

なんだかうまく行っているように見えますが…

Prelude> 3 * 3

<interactive>:4:5:
    No instance for (Num [a0]) arising from the literal `3'
    Possible fix: add an instance declaration for (Num [a0])
    In the second argument of `(*)', namely `3'
    In the expression: 3 * 3
    In an equation for `it': it = 3 * 3

駄目でした!一つの演算子に割り当てられる演算は一種類。しかも静的型ですから Ruby のように実行時に型を見てふるまいを変えるのも無理><

その代わり、Haskellではユーザーがいくらでも演算子を定義できるのです。

(*~) :: Int -> [a] -> [a]
(*~) n xs =  concat $ take n $ repeat xs
(~*) :: [a] -> Int -> [a]
(~*) xs n = n *~ xs
Prelude> :load overload.hs
[1 of 1] Compiling Main             ( overload.hs, interpreted )
Ok, modules loaded: Main.
*Main> 3 * 3
9
*Main> 3 *~ "3"
"333"
*Main> "3" ~* 3
"333"

二文字の弓矢

この Haskell の美点も、Swiftはあわせもっています。たとえば以下は私がよく使っている演算子。

infix operator => { associativity left precedence 95 }
func => <A,R> (lhs:A, rhs:A->R)->R {
    return rhs(lhs)
}

要はthird(second(first(arg)))というのをarg => first => second => thirdと呼べるようにするための構文糖衣的演算子で、Perl6では===>として定義されている演算子ですが、Swiftには fat comma がないのでこれに割り振っちゃってます。

正直手放せません。

println(...)

って書くより、

... => println

って書く方がずっと楽ですし、{}との相性も抜群です。

3   => { Array(count:$0, repeatedValue:"3") }
    => { join("", $0) }
    => println

playground.png

まとめ

Swiftでは、

  • 既存の演算子に新しい意味を持たせられます
  • 既存の演算子を壊しません
  • オレオレ演算子を定義できます
  • 実装のみならず、結合法則や優先順位もオレオレ出来ます。

Perl6でオレオレ演算子に目覚めた私にとって、Swiftの演算子は猫にマタタビもいいところです。

演算子募集中!

そんなこんなでSwiftのカスタム演算子を蒐集しています。

「こんなの作ったよ!」という方は是非本記事か同Wikiへご一報を。

演算子かわいいよ演算子

Dan the Smooth Operator

53
53
0

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
53
53