書いて覚えるSwift入門

第37回型進化論

型とは何か?

今回は型(type)の話をします。漢字の「型」だけみると、プログラミングに限定しても「モデル」⁠model)だったり「キャスト」⁠cast)だったりどの「型」なのか定まりませんが、本記事における「型」はデータ型(data type)を指します。

型がなければ意味がない

およそどんなプログラミング言語にも登場するこの型という概念ですが、まだプログラムを知らない子供に「型ってなあに」と問われたら読者のみなさんならどう答えますか?

筆者はこう答えます。⁠値(value)の意味(meaning⁠⁠」と。

Software Designの読者であれば100%ご存じのとおり、計算機が扱えるのは0と1からなる二進数の羅列だけです。ここであえて計算機と言ったのは、現代人にとっての「コンピュータ」は計算装置だけではなく、キーボード、マウス、マイクロフォン、タッチパネルといった入力装置、ディスプレイ、スピーカ、プリンタといった出力装置、Ethernet やWi-fi やLTE といった通信装置までまとめたものだから。しかしそういった装置を我々プログラマは直接操作できません。できるのは0 と1の羅列を別の0と1の羅列に変えるだけです。ここで32個の0と1の羅列をご覧いただきましょう。

01100011011011110110010001100101

これが何を意味するか、わかる読者はいらっしゃいますか?

フラグが32個並んでるだけかもしれませんし、整数の1668244581かもしれませんし、アドレス0x636f6465を指すポインタかもしれませんし、単精度浮動小数点の6.74221425242669e+22のことかもしれませんし、ASCII文字列のcodeのことかもしれませんし、色指定のrgba(99,111, 100, 0.39453125)のことかもしれません……。

つまり、わかるわけありませんよね? 型がわからなければ。コンピュータがただの計算機から万能情報処理機になれたのも、型の賜物(たまもの⁠⁠。ただの数値の羅列に文字どおり意味を与えるのが型なのです。

型はイイが型付けはイタい

コンピュータに意味ある仕事をさせるのがプログラマの仕事なのですから、ほとんどのプログラミング言語はvaluetypeを持たせています。しかしそのやり方はプログラミング言語ごとに異なります。筆者の見解では、プログラミング言語による差異が最も顕著なのがこの型の扱いです。

主な言語をざっと見てみると、二進数で10通りのやり方が存在します。型を必ず書かねばならない言語とそうでない言語。Javaは前者の代表的言語です。言わずと知れた⁠Hello,world!⁠を出力するプログラムはこんな感じ。

public class Hello {
    public static void main(String[] args){
        System.out.println("Hello, world!");
    }
}

型を指定しているpublic classも、public static voidも、mainの括弧のなかにあるString[]も省けません。そしてソースコードの名前はHello.javaでなければなりません。そして動かすにはTerminal.appから、

% javac Hello.java
% java Hello

とします。⁠Hello, world!⁠と出てきましたか?

ずいぶん面倒ですが、一度javacしたら次回以降はjava Helloだけで実行できます。Hello.javaをゴミ箱にポイしてもjava Helloで実行できます。lsしてみるとHello.classというファイルができています。これをJavaがインストールされた別のコンピュータにそのままアップロードしたうえでjava Helloと命じるとやはりそのまま動きます。まさにWrite once, run everywhereなわけです。

そして書かなくてもいいプログラミング言語として、JavaScript[1]もといECMAScriptが挙げられます。⁠Hello, world!⁠であれば、

console.log("Hello, world!");

とたった1行。動かす方法はいくつもあるのですが、ここではnode.jsを使うとして、

% node hello.js

javajavac相当は不要です。これくらい短いとソースコードすらファイルに落とさずとも、

% node -e 'console.log("Hello, world!");'

で動いてしまいます。紙幅制限ゆえ、ここではJavaとJa……ECMAScriptだけを取り上げますが、

  • ソースコードとは別の実行用ファイルを出力するタイプの言語では型を明記する。CやC++がこれに相当
  • ソースコードを直接実行するタイプの言語では型を省略できる。いわゆるスクリプト言語、Perl、PHP、Python、Rubyがこれに相当

という大まかな傾向には賢明な読者のみなさんはすでにお気づきと思います。

ここで本記事の主題であるSwiftがやっと登場します。Swiftはどっち?

ソースコードはこうです。

print("Hello, world!")

この1行をhello.swiftという名前のテキストファイルにセーブしたら、

% swift hello.swift

で実行できてしまいます。ところが、

% swiftc hello.swift

とするとhelloというファイルが生成されて、

./hello

でやはり実行できてしまいます。ただしJavaとは異なり、このファイルが実行できるのは同じOSのマシンだけで、Linuxで生成したものをmacOSにアップロードしても動きません。

つまりSwiftはスクリプト言語のように型を略記でき、スクリプト言語のように直接ソースファイルを実行できるにもかかわらず、実行ファイルexecutableを生成=コンパイルcompileすることもできるという意味で、両者のユースケース双方をカバーしようというアンビシャスな言語だということです。

名を持つ値の型

“Hello, world!⁠ではHello, world!という文字列の値をそのまま使っていましたが、より複雑なプログラムではデータに名前を付けて操作します。たとえばこんな感じ。

let hello = "Hello "
let world = "world!"
print(hello + world)

一度データを指定したらそのまま最後まで使うものを定数constant値を変更して使い回すものを変数variable両方合わせて識別子identifierと呼びます。数以外の型も使うので、筆者個人は「定値」⁠変値」のほうがよかったと思わぬでもありませんが、それはさておきSwiftの変数には1つの特徴があります。値は変えられても型は変えられないのです。

比較のためにRubyを見てみましょう。次のvは値だけではなく型(Ruby用語ではclass)も変わっていることが見てとれます。

irb(main):001:0> v = 42
=> 42
irb(main):002:0> v.class
=> Fixnum
irb(main):003:0> v = 42.195
=> 42.195
irb(main):004:0> v.class
=> Float

しかし、Swiftでは次のように怒られてしまいます。

% swift
Welcome to Apple Swift version 4.1
(swiftlang-902.0.48 clang-902.0.39.1). Type
:help for assistance.
  1> var v = 42
v: Int = 42
  2> v = 42.195
error: repl.swift:2:5: error: cannot assign
value of type 'Double' to type 'Int'
v = 42.195
  
    Int( )

Swiftでは、変数の値は変えられても型は変えられないのです。しかも、一度も値が変更されない場合varでなくてletにしたら」と警告さえします図1⁠。

図1 Swiftによる型の警告「varでなくてletにしたら」
図1 Swiftによる型の警告「varでなくてletにしたら」

型は値から推論される

本記事の主題は型なのに、Swiftのコードではまだ型を書いていません。Swiftには型推論type inferenceという機能があって、推論できる場合はそれを使ってくれるからです。Swiftは何から型を推論しているのでしょうか?

値、です。

let hello = "Hello "Helloというリテラルliteralが文字列型、Stringであることは一目瞭然です。42が整数Int42.195が浮動小数点数Doubleであることも。

それでは関数functionはどうでしょうか? どんな値が来てどんな値を返すかを決めるのはSwiftではなくプログラマですよね。というわけで2つの値を+する関数はこうなります。

func plus(_ l:Int, _ r:Int)->Int {
    return l + r
}

これでplus(40, 2)42になります。簡単ですね。しかし今度はplus(42.0, 0.195)としてみてください。cannot convert value of type 'Double' to expected argument type 'Int'というエラーになるはずです。

今度はさきほどのfunc plusを残したまま次を追加しています。

func plus(_ l:Double, _ r:Double)->Double {
    return l + r
}

今度はplus(40, 2)plus(42.0, 0.195)も期待どおりの答えを返したはずです。関数の名前が同じでも引数argument戻り値return valueの型が変わればSwiftは別物として扱ってくれるのです。

しかしこのやり方だと、型の種類だけ同じ名前の関数を用意しなければならなくなりそうでなんとも冗長です。まとめて1つで済ませる方法はあるのでしょうか?

あります。総称型genericsが。

今度は先ほどのfunc plusを両方消して、次を定義します。

func plus<T:Numeric>(_ l:T, _ r:T)->T {
    return l + r
}

どっちもうまく行ったことが確認できるでしょう。先ほどと違うのは<T:Numeric>という表記。これはNumericというプロトコルprotocol準拠complyする型Tという意味になります。そしてIntDoubleNumericというプロトコルに準拠しているので、これ1つで(Int,Int)->Int型の関数と(Double,Double)->Double型の関数を兼ねることができるわけです。

ところでSwiftは値から型を推論すると先ほど述べました。そして関数自体も値です。ということは名前のない関数リテラルに推論可能な引数を与えたら、関数の型も型推論できないのでしょうか?

それに気づいた読者は鋭い。

({$0 + $1})(40, 2) // 42:Int
({$0 + $1})(42.0, 0.195) // 42.195:Double

({$0 + $1})だけではダメで、引数があって初めて最初の({$0 + $1})(Int,Int)->Int(Double,Double)->Doubleだという推論が成立するので、型を書かずに済むのです。

これが配列Array辞書Dictionaryなど、同じ型の値がいくつも入ったコレクションCollectionの処理で威力を発揮します。たとえば総和(sum)を求めたかったら、

(1...100).reduce(0){ $0 + $1 } // 5050

と型を明示せずに書けますし、さらにここまで何の断りもなく使ってきた演算子(operator)+もその正体は総称関数なので、

(1...100).reduce(0,+)

とさらに簡単に書けます。実際に二項演算子+が関数であることは

(+)(40, 2)
(+)(42.0, 0.195)

のように確認できます。

まとめ

もう10年以上前にPerl 6をHaskellで実装して全スクリプト言語プログラマの度肝を抜き、現在は台湾の無任所大臣となった天才プログラマ、Audrey Tang(唐鳳)は次のような名言を残しています。

Type 😊

Typing 🙁

邦訳すると「型はイイが型付けはイタい」となりますが、型の痛みを伴わず型を縦横無尽に駆使するという意味で、Swiftは10年前の「人類には早すぎた」Perl 6やHaskellの野望を普通のプログラマにもたらしたと言えるのではないでしょうか。

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

おすすめ記事

記事・ニュース一覧