71
69

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Swift - 等しさとせつなさと型の強さと

Posted at

Swiftの関数の引数は、常に一つ

だとしたら、Swiftではカリー化は不要ということになってしまう。

もちろん、そんなことはない。

求めているのは、「n-tupleを引数とする関数を、1-tupleづつ引数を受け取り、n個受け取った時点ではじめて値を返す関数にすること」である。

例えばこんな風に。

import Darwin // for log(Double)
func logWithBase(base:Double)->Double->Double {
    return { x in log(x) / log(base) }
}
let log10 = logWithBase(10)
log10(100) // 2.0

そうすること、つまり

func f<T0,T1,...Tn,R>(a0:T0,a1:T1,...,an:Tn)->R

func f<T0,T1,...Tn,R>(a0:T0)->T1->T2->...->Tn->R

にするのが、カリー化(currying)というわけである。言葉にするとまだるっこしいが、関数の宣言として書くと直感的にわかる。

任意の関数をcurry化するには?

それでは、任意の関数を受け取ってそれをカリー化する関数は書けるのだろうか?

2-tupleの関数であれば、あっさり書ける。

func curry<A,B,R>(f:(A,B)->R)->A->B->R {
    return {a in {b in f(a,b)}}
}

自分で定義した関数のみならず、ビルトインはおろか演算子関数さえ受け付ける。

let plusplus = curry(+)(1)
plusplus(41) // 42

3-tupleでも

func curry<A,B,C,R>(f:(A,B,C)->R)->A->B->C->R {
    return {a in {b in {c in f(a,b,c)}}}
}

4-tupleでも

func curry<A,B,C,D,R>(f:(A,B,C,D)->R)->A->B->C->D->R {
    return {a in {b in {c in {d in f(a,b,c,d)}}}}
}

しかし、任意のtuple長の関数を、たった一つのcurry()で賄えるのか?

長さ変われば型変わる

ここでやっと本題に入れる。

できない、のである。

なぜか。

長さが変わると、型も変わってしまうから。

[0,1][0,1,2]はどちらも同じ[Int]型だが、(0,1)(0,1,2)は前者が(Int,Int)型で後者が(Int,Int,Int)型。任意の型の関数を受け取るところまでは、「Swiftの関数の引数は、常に一つ」で示した通りA->Rで行けるのだが、任意の型を返すのはジェネリクスをもってしても無理なのである。

これは、Pythonのtuple()のような、任意のSequenceから同じ長さのtupleを生成するような関数はSwiftでは実装できないことを意味する。

もっとも実行時も変数が自分の型を覚えている動的型のPythonのtupleと、コンパイルされたら(どうしても必要な場合を除いて)型が外される、静的型のSwiftやScalaなどのtupleは、ずいぶんと意味が異なる。むしろ静的型の言語から見れば、Pythonのそれはtupleというより単なるimmutable listに過ぎず、実際各要素へのアクセスは、

(0,0.0,"zero")[2] // "zero"

とlistとまるで変わらないのだし。

AnyObjectであれば、Pythonと同様のことをObjective-C的に実装することも出来なくはなさそうであるが、SwiftにおいてAnyObjectへのキャストは意味がまるで異なるので、「静的型の要素からなる任意の長さのSequenceをtuple化すること」は「出来ない」といい切ってもよいだろう。なお、AnyAnyObjectの違いについては、記事を改めてまた書く予定だ。

[0,1] == [0,1] が出来て (0,1) == (0,1) が出来ないなんて

Tupleをサポートする他の静的言語を使い慣れた人が、一番びっくりするのはこれかも知れない。

Screen Shot 2014-07-31 at 10.04.34 AM.png

なんと、tupleどおしの等しさを確認できないのだ。

(要素がEquatableな)配列どおしの等しさなら確認できるのに。

もちろん、これはすっぴん状態で、ということであり、以下のようにすれば 2-tuple (Doubleと呼びたいところだが倍精度浮動点小数にもう抑えられている)が即座に比較可能になる。

@infix func ==<A:Equatable,B:Equatable>(lhs:(A,B),rhs:(A,B))->Bool {
    return lhs.0 == rhs.0 && lhs.1 == rhs.1
}
(0,1) == (0,1) // true as it should be

これをtupleの長さごとに書くのは正直めんどくさすぎる。

せつなさの理由

とはいうものの、そうなっている理由を察せないわけでもない。

組み合わせ爆発だ。

Tupleの要素となりうる型がm種類あるとすると、要素数nまでのtupleの型の種類は、tupleのネストを認めなかった場合でさえ(m+1) ** nも存在することになる。

実際にはtupleの入れ子はSwiftでも認められている。そして(T,T,T)(T,(T,T))(((T,T),T)はそれぞれ別の型なのである。

型の増大を防ぐことができるのがメリットだと思うよ - http://qiita.com/f81@github/items/a8419532c316d190782d

ずいぶんな誤解である。「型定義の増大」であれば話はわかるのであるが。

Tupleの最大長は(C#は8?)、Scalaが22、Haskellが64。ずいぶんと短いのは、これが原因ではないか。

ちなみに現時点においてSwiftではtupleの最大長は無制限なようである。無制限な代わりに、すっぴんでは.0,.1...と、各要素のgetterしか持たない(ラベル付きのtupleではそのラベルも使えるが)。extensibleでもないので、

extension (Int, Int) : Equatable {}

のようなことも出来ない。

そういうことをしたければ、

struct Point2d: Equatable {
    var x = 0.0, y = 0.0
}
@infix func ==(lhs:Point2d, rhs:Point2d)->Bool {
    return lhs.x == rhs.y && lhs.y == rhs.y
}

といった具合にきちんと型を作りなさいというメッセージなのだろうか。そして「任意の長さのtuple」であれば、代わりに配列を使えというメッセージなのだろうか…

(Dan, Swift, Newbie)

71
69
1

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
71
69

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?