有能ではあるが万能には足りない
前回 の予告どおり、今回はSwiftへの不満と将来への期待を取り上げます。
公式サイトのAbout Swift にはこうあります。
The goal of the Swift project is to create the best available language for uses ranging from systems programming, to mobile and desktop apps, scaling up to cloud services.
「Swift プロジェクトのゴールは、システムプログラムからモバイルおよびデスクトップアプリケーション、そしてクラウドサービスに至るまで最良の言語を創ることである」 。裏を返すと、Swiftへの不満はこのゴールに足りないものということになります。
RustにあってSwiftにないもの=RustになくてSwiftにあるもの
現時点において、Swiftはシステムプログラミングにほとんど用いられていません。つまりカーネル(kernel)やデバイスドライバ(device drivers)はSwiftでは書かれていないということです。これはSwiftの「先輩言語」の1つであり、Swiftともっとも見た目が似ているRust 言語とは対象的です。
RustにはすでにRustで実装されたRedox というOSがあります(図1 ) 。それも仮想マシンでただ起動してコンソールが表示されるというレベルではなく、実マシンで動作してGUIも装備しているというレベルです。すごいですね。
図1 Redoxのデスクトップ画面
それに対し、Swiftは「故郷」のAppleにおいてすらアプリケーションの開発にしか用いられていません。この違いはどこから来るのでしょうか?
筆者の答えは、過去の遺産の有無です。SwiftはObjective-Cの遺産を継承することが義務付けられましたが、Rustにはそれがありません。SwiftはObjective-CとともにiOSとmacOSの開発者コミュニティをも継承することで、Version 1どころかベータ段階から100万人単位でユーザを確保することに成功しましたが、Objective-Cの「技術的負債」も一部背負うことになりました。
たとえばメモリ管理。SwiftもObjective-C同様、参照カウンタ(reference counter)でメモリ管理を実現しており、それが言語レベルで密結合しています(ARC:AutomaticReferenceCounting ) 。参照カウンタはプログラミング言語における代表的なメモリ管理技術で、JavaScriptやPerlやPythonなどでも用いられてきましたが、それが唯一の手法でないことは賢明な読者のみなさんはご存じのとおりです。
しかし問題なのは参照カウンタ方式であることそのものより、それが言語仕様と密結合していることにあります。密結合しているおかげで循環参照の解消などをプログラマが制御できるのですが、密結合しているということは別の手法を導入したり、メモリ管理そのものをSwiftで実装したりすることが難しいということでもあります。
これに対しRustは、Ownershipという概念を導入したことで言語自体にメモリ管理を密結合させることを極力避けたうえで、参照カウンタなどの機能はRcなどライブラリとして分離されています。これはRustが難しい言語になっていることの一因(というか最大の理由)にもなっているのですが、システムプログラミング言語として頭1つ抜けている理由でもあると言えるでしょう。
Ownershipがいかに優れた概念であるかは、Swiftにも導入の動きがある ことからも伝わってきます。
SwiftはSwiftで書かれていない
Swift 4.2現在、SwiftはSwiftではなくおもにC++で実装されています。別の言い方をするとSwiftはセルフホストされていないということです。JavaScriptからRubyまで、アプリケーション言語の多くはセルフホストされておらず、そのこと自体が問題というわけではありませんが、Cをはじめ、システムプログラムに用いられる言語の多くはその言語自身で実装されています。Swiftのスローガンは“ Safe, Fast & Expressive” ですが、自分自身を表現するというのは表現力を提示する最良の方法だけに、SwiftがSwiftで書かれていないことは物足りなく感じます。
総称関数が関数(オブジェクト)でない
システムプログラミングが「下」に対する不満なら、「 上」に対する不満ももちろんあります。筆者の一番のそれは、Swiftのジェネリクス(generics)です。
本連載でも以前指摘したとおり、Swiftの関数(function)は第一級オブジェクト(first-class object)ですが、総称関数(generic function)はそうではありません。
たとえば、
func idInt(_ i:Int)->Int{ return i }
と、
let idInt = { (i:Int)->Int in i }
や
let idInt:(Int)->Int{ i in i }
や
let idInt:(Int)->Int{ $0 }
は等価で、
var f = idInt
f(42) // 42
と変数に代入することも可能ですが、
func id<T>(_ a:T)->T {
return a
}
に、
let id = <T>{ (i:T)->T in i }
や
let id<T>:(T)->T { i in i }
や
let id<T>:(T)->T { $0 }
という表現はありえず、もちろん、
var f = id
とは書けません。Swiftが静的型言語である以上これは仕方がないことで、嫌ならJavaScriptなど動的言語を使えばいいと思われるかもしれませんが、ちょっと待った!
それができる静的型言語はすでに存在するのです。それもSwiftの先輩言語に。
リスト1 のコードにchurchNum.hs
と名付けて保存します。
リスト1 churchNum.hs
zero = \ f x -> x
one = \ f x -> f x
two = \ f x -> f $ f x
three = \ f x -> f $ f $ f x
succ = \ n f x -> f $ n f x
add = \ m n f x -> m f $ n f x
mul = \ m n f -> m $ n f
次にHaskellのデファクト標準実装であるghci
で実行してみます。
% ghci
GHCi, version 8.4.3: http://www.haskell.org/ghc/:? for help
Prelude> :l churchNum.hs
[1 of 1] Compiling Main ( churchNum.hs, interpreted )
Ok, one module loaded.
*Main> (add two three) (+1) 0
5
*Main> (mul two three) (+1) 0
6
これはチャーチ数(Church numerals)と言って、整数n
は初期値x
に関数f
をn
回かける関数として定義するもので、ソースコードというデータをプログラムとして実行するチューリングマシーン(turing machine) 、つまり一般的なデジタルコンピュータとは逆に、関数というプログラムでデータを表現する手法です。詳しくはラムダ計算で検索してもらうとして、実際「twoとthreeをadd」した結果の関数に、初期値0
と+1
する関数を入れると6
になっています。「 twoとthreeをadd」した結果も総称関数であることは、
*Main> (add two three) ((++) "!") ""
"!!!!!"
*Main> (mul two three) ((++) "!") ""
"!!!!!!"
として容易に確認できますし、その型がHaskellでは総称的に推論されていることも次のように確認できます。
*Main> :t add
add :: (t1 -> t2 -> t3) -> (t1 -> t4 -> t2) -> t1 -> t4 -> t3
*Main> :t mul
mul :: (t1 -> t2) -> (t3 -> t1) -> t3 -> t2
当然のことながら、チャーチ数の実装は動的型言語でも容易です。リスト2 はJavaScriptの例です。
リスト2 チャーチ数の実装(JavaScript)
onst zero = f => x => x;
const one = f => x => f(x);
const two = f => x => f(f(x));
const three = f => x => f(f(f(x)));
const succ = n => f => x => f(n(f)(x));
const add = m => n => f => x => n(f)(m(f)(x));
const mul = m => n => f => x => n(m(f))(x);
console.log(add(two)(three)(x=>x+1)(0)); // 5
console.log(mul(two)(three)(x=>x+1)(0)); // 6
それではSwiftではどうでしょう? リスト3 の結果をご覧ください。
リスト3 Swiftでのコード
typealias F<A,R> = (A)->R
typealias C<N> = F<N,N>
func zero<N> (_ f:@escaping C<N>)->C<N> {
return {x in x}
}
func one<N> (_ f:@escaping C<N>)->C<N> {
return {x in f(x)}
}
func two<N> (_ f:@escaping C<N>)->C<N> {
return {x in f(f(x))}
}
func three<N>(_ f:@escaping C<N>)->C<N> {
return {x in f(f(f(x)))}
}
// SUCC := λn.λf.λx.f (n f x)
func succ<S,Z,N>(
_ n: @escaping (@escaping F<S,N>)->F<Z,S>
)-> (@escaping F<S,N>)->F<Z,N>
{
return {f in {x in f(n(f)(x))}}
}
// ADD := λm.λn.λf.λx.m f (n f x)
func add<S,Z,N>(
_ m: @escaping F<Z,F<S,N>>
)-> (@escaping F<Z,F<N,S>>)->F<Z,C<N>>
{
return {n in {f in {x in m(f)(n(f)(x))}}}
}
// MUL := λm.λn.λf.m (n f)
func mul<S,Z,N>(
_ m: @escaping F<S,Z>
)-> (@escaping F<N,S>)->F<N,Z>
{
return {n in {f in m(n(f))}}
}
print( mul(two)(three)({x in x+1})(0) )
print( add(two)(three)({x in x+1})(0) )
ずいぶんとまだるっこしいものになってしまいました。実はこれ、昔のSwiftではもう少し楽に 書けました。昔は@escaping
は不要だったのです。その一方typealias
で総称型が使えるようになったので、3歩下がって2歩進んだと言ったところでしょうか。
Swiftの物語はこれからだ!
ひねくれた言い方をすると、Swiftは後出しじゃんけん言語です。ほかの言語の失敗に学び、いいとこどりばかりしてきたという点で。しかし後出しなのに負けている点も、上記のとおり見受けられます。
とはいえSwiftはまだ5才。ほかの主要言語は主要言語とみなされるまで10年以上かかるのが普通です。拡張の余地もまだあります。''
(シングルクォート)はいまだに丸々未使用ですし、Ownershipも後付け不可能ではありません。
そもそも万能言語が必要かという疑問もあります。Cはなんでも書けますが、なんでもCで書く人はほとんどいません。何でもできるものは往々にして何でも不得意なもので、年々その数を減らしている自然言語とは逆に、プログラミング言語の数が増えているのは適材適所をユーザが求め続けてきた結果でもあるでしょう。
それでもなお万能言語を希求してしまうのもまた、プログラマの性でもあります。誕生して以来、Swiftはその候補としてずっといい位置にいますし、ユースケースを着実に広げています。Swiftはまだ始まったばかりなのです。
第1特集
MySQL アプリ開発者の必修5科目
不意なトラブルに困らないためのRDB基礎知識
第2特集
「知りたい」「 使いたい」「 発信したい」をかなえる
OSSソースコードリーディングのススメ
特別企画
企業のシステムを支えるOSとエコシステムの全貌
[特別企画]Red Hat Enterprise Linux 9最新ガイド
短期連載
今さら聞けないSSH
[前編]リモートログインとコマンドの実行
短期連載
MySQLで学ぶ文字コード
[最終回]文字コードのハマりどころTips集
短期連載
新生「Ansible」徹底解説
[4]Playbookの実行環境(基礎編)