最近、Web界隈で使われているいろいろなプログラミング言語を見ていて、段々と「型」の話が増えているな、と感じています。
ほんの一例ですが、
など、型のある言語や、既存言語に型を付ける、等の動きが活発です。
そこで、今回は、上記のようなトレンドの話をしつつ、将来的に入るかもしれない「型」に関する技術について紹介したいと思います。
注意事項1:自分は学者ではないです
まず、自分は趣味で、型を勉強しているので、細かい間違えや詳しくは説明できない部分もあるかもしれません。ただ、逆に広く分かりやすい紹介の仕方ができるのでは、と思っています。
ツッコミ等はWelcomeです!
注意事項2:今回話さないこと
今回、型の話をすると、よく出てくるような以下のことは詳しく紹介や説明しません。
- モナド
- モナドは毎日「モナドを理解した」記事が出てくるぐらい取っ付きにくい考え方です。これだけで記事をいくつも賭けそうなボリュームになるので今回は直接モナドの紹介はしません。
- 型理論(Curry-Howard同型、等)ギリシャ文字やら矢印やらが出始めると読者が一気に離れると思っています 😁
- 圏論こちらも理論的な内容をし始めると読者が一気に離れると思っています 😁
もちろん、どれも面白く、より深く知ろうとすると、すぐに出てくるような内容ですが、今回はエンジニアの方々に広く型に興味を持ってもらうよう、上記の話は紹介しません(そして、自分がそれほど深く理解できていない、というのも正直あります)。
そもそも、なぜ型を使うのか?
では、まず実際の話に入る前に「なぜ型を使うのか、大事なのか」という点について軽く説明させてください。
いろいろあるかとは思いますが、自分の中で思う代表的なものを挙げると…(順不同です)
- バグを実行時ではなく、それより手前の段階(コンパイル時、等)で早く見つけられる
- バグを自動的に見つけられる
- プログラムの実行を速くする
- コードが分かりやすくなる
などでしょうか。
ここで注目してほしいのは「コードが分かりやすくなる」以外はコードを解釈するプログラムに対するものであることです。型を使って、バグを早く見つけるのも、実行を速くするのも、実際にはコンパイラなどのプログラムであり、僕らエンジニアがプログラムへ付加情報を与えている構図になります(「助言」とか「補助」と読み替えても良いかもしれません)。
これが型を使う理由であると自分は理解していますし、この後に説明する様々な新しい技術もこの考えが根底にあると思っていると理解しやすいと思います。
なぜ、型の話が増えているのか?
冒頭に書いたように型に関する情報や技術は日進月歩で増えているのを感じています。
その背景としては
開発の大規模化、複雑化
が大きな要因と考えています。
ソースコード検索サービスを提供するSourcegraphの調査によると
- 51%のエンジニアが10年前に比べて、コード量が100倍になっている
- 57%のエンジニアが以前より、コードの変更が行いにくくなっている
という回答されたようです。
(余談:⬆️ の資料ですが、他にもとても面白い内容でしたので、オススメです)
みなさんも例えば10人が1年以上開発してきたソフトウェアで型とか何もなかったら怖いですよね?
自分は怖すぎて、そのコード触れません。
型の基本から
まずは一応基本だけ紹介します。
型といえば
- int : 整数型
- string: 文字列型
- float: 浮動小数点型
- (int) -> string: 整数を受け取り、文字列を返すような関数の型
とかありますね(なお、上記の記述は特定の言語の記述とかではなく、あくまで概念的なものです)。
少し発展した例としては
function firstElement<T>(arr: T[]): T {
return arr[0];
}
とかもありますね。
ここまでは見たことがある人が多いでしょうし、型を使わない言語を利用されている方でも概念的に理解しやすいと思うので以上にします。
最近のトレンド
さて、ここから最近見かけることが多くなった(≒対応している言語が増えた)、と感じている型の技術を紹介したいと思います。
Optional
いわゆる「ヌル値になるかもしれない」という型です。
Swift の例です。
let num: Optional<Int> = may_return_nil(); // ここでnilが返るかもしれない。
if let a = num { // Optionalの中のIntを取り出します。
print(a + 1) // 取り出しているので、Intとして使えます。
} else {
print("nilでした")
}
let addOne = num + 1
// ^^^^^^^ そのまま使おうとするとエラーになります。
変数が明示的にヌル値になるかどうかを指定することにより、その変数に依存しているコードがヌルチェックを行わずに使おうとしていないか、というのをコンピュータがチェックできます。
- Javaのjava.lang.NullPointerException"
- JavaScriptだと "
Uncaught TypeError: a is null"
などを見なくて済むようになるって訳ですね。
Promise、Async/Await
こちらは非同期処理を表すための型(および文法)になります。
特にTypeScriptなどで利用している方も多いのではないでしょうか?
TypeScriptの例です。
function delayedPrint(msg: string, delay: number): Promise<void> {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(msg);
resolve();
}, delay);
});
}
async function main() {
console.log("イチと出力します。");
delayedPrint("イチ", 1000);
console.log("ニと出力します。");
await delayedPrint("ニ", 1000);
console.log("サンと出力します。こちらは「ニ」を待ってから出力します。")
delayedPrint("サン", 1000);
}
main();
上記を実行すると、以下のようになります。
イチと出力します。
ニと出力します。
イチ
ニ
サンと出力します。こちらは「ニ」を待ってから出力します。
サン
「イチ」が出る前に、「ニと出力します」が出ており、非同期に実行されています。
反対に次の「サン」はawaitを使って、 「ニ」が出力されるのを待ってから、処理されています。
このようにPromiseやAsync/Awaitにより、より直感的でシンプルな記述で非同期処理が実装できるようになりました。以前のコールバックによる複雑さ(通称:コールバック地獄)を知っている方にとって、嬉しい機能ではないでしょうか。
この機能、マイクロソフトが開発したF#という言語では2007年頃に実装されており、その後、同じくマイクロソフトのC#に2011〜2012年頃に導入されております。その後、TypeScript、Kotlin、Swiftなどにも入り、更にC++にも最近入りました。
いかにエンジニアのみなさんが便利と感じているのか分かりますね!
余談
上記のF#は Don Syme というマイクロソフトの研究者の天才的なエンジニア(一部ではC++を作ったBjarne Stroustrup以来の大物新人という言われ方も)が開発されたのですが、実はこのDon Syme、C#にジェネリクスを導入した方でもあったりします。
関数型やオブジェクト指向などパラダイムも異なるプログラミング言語ですが、核にある考え方や理論は近いんだなぁ、と感じた小話でした。
ここまでのまとめ
型の技術がいろいろな言語に広まっている雰囲気が感じられたでしょうか?
この種の技術はに日進月歩なので、来年には多くの人が知らないような技術が入ってくることも十二分にありえると思います。
そこで、ここからは本題の「これから来るかも」な型の技術を少し紹介させてください。
Algebraic Data Types(代数的データ型)
日本語でも英語でも仰々しい名前なのですが、実はそれほど難しい話ではありません。
簡単に言うと
- 複数の型をまとめた PairやStruct
- どちらかの型 を取れるUnion
のことです。
複数の型をまとめたPairやStruct
まずは1のPairなのですが、こちらは分かりやすいと思います。以下、例です。
type Hour = number;
type Minute = number;
type Time = [Hour, Minute]; // <-- コレのこと
const time: Time = [3, 'a'];
// ^^^ コンパイルエラー
const time2: Time = [17, 30, 5];
// ^^^^^ コンパイルエラー
このように指定された型と異なる内容が指定されるとコンパイル時にエラー判定できます。
これだけではなく、構造体などの形でもPairに対応している表現ができれば対応しているので、見たことがある方も多いと思います。
どちらかの型を取れるUnion
こちらは字面だけでは分かりにくいので、少し詳しく説明します。
TypeScriptで例を挙げると、こういうやつです。
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
type Shape = Square | Rectangle; // <-- コレ
上記、Shapeのように、Unionのどれか(この例ではSquareかRectangle)であることが保証されているような型ということになります(それ以外のやつが入り込む余地がない)
さて、何が嬉しいのか、という説明も必要だと思うので、上の例を使った小さいプログラムを書いてみました。
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
type Shape = Square | Rectangle;
function non_exhaustive(s: Shape) {
switch (s.kind) {
case "square":
return s.size * s.size;
default:
const _tmp: never = s
// ^^^^ コンパイルエラー!
}
return 0;
}
function not_in_union(s: Shape) {
switch (s.kind) {
case "square":
return s.size * s.size;
case "rectangle":
return s.height * s.width;
case "circle":
// ^^^^^^ コンパイルエラー!
}
}
見ていただいて分かるように、
- Unionの取り得る型に対する処理を書ききっていない場合
- Unionに無いような型に当てはめようとした場合
などをコンパイル時にチェックすることが可能です。
特に前者は「場合分けの処理し忘れ」というよくやりがちなバグを潰せるので効果として大きいと思います。
これって新しいのか、と思う方もいらっしゃるかもしれませんが、実は対応していない言語も意外とあります。
例えば PHPもまだ提案段階 ですし、Kotlinもコンパイル時にすべてのパターンを網羅したかの確認(先程のnon_exhaustiveの例)は これから入る予定 です。
「代数的」の意味
なぜ「代数的」と呼ばれているか、なのですが、話が長くなる&自分がきちんと説明できる自信がないので、割愛します。PairやUnionをそれぞれ掛け算と足し算に見立てると型に入る種類の数と一致して、数学の代数と関係が深いね、ぐらいに思っててください。
興味のある方は以下の記事がオススメです。
Dependent Types(依存型)
さて、次の話題はDependent Typesです。名前からだとイメージ沸かないやつですね。
今までの型の話は値が型に依存しているというのが前提です。
let n: number = 3; // 3はnumberだからOKと値は型に依存している。
let str: string = "hello"; // "hello"はstringだから同様。
ここで紹介するDependent Typesはそれを逆転させて型が値に依存するというものになります。
ちょっと意味不明ですね😅
例を挙げて説明します。
なお、Dependent Typesをサポートしている言語がAgda 、 Idrisなど、あまり馴染みのない言語だと思うので、TypeScriptっぽい言語で表現します。
Dependent Typesがない場合に困るケースとして、「偶数を引数に期待しているような関数」を実装する場合を考えます。
特に意識せずに書くと、こんな感じになると思います。
function f(n: number) {
if (n % 2 != 0) // 偶数であることを確認。
throw "n is not even!"
} else {
// do something with n
:
}
}
f(1) // 奇数だから実行時にエラー!
これで間違ってはいないのですが、最後の行のように人が読めば絶対にエラーするのは分かるのを防げないのが残念ですよね。
次にDependent Typesみたいなものが入っているとどうなるかと言うと…
(なお、下の記法は僕が勝手に作ったもので実際にはTypeScriptでは動きません)
function f(n: { int | n % 2 == 0 }) {
// ^^^^^^^^^^ コンパイル時にチェックされます!
// do something with n
:
}
f(1) // コンパイル時にエラー!
というように型に値が入っており、それを使ってコンパイル時のチェックができます。
で、長い説明でようやく「依存」の部分なのですが、見ていただくと
n: { int | n % 2 == 0 }
という型指定が2という値に依存した型になっていることが分かるかと思います。
何が嬉しいのか?
これは今までのデータの形や種類が型に合っているか、ぐらいの確認だったのが、そのときに取り得る値も見てくれるためコンパイラによる検証範囲が広がり、より多くバグなどを検知できます。
Linear Types(線形型)
最後に紹介したいのがLinear Typesです。
値を一直線で使いたいってことなのかな、と思っていただくとイメージしやすいかもしれません。
今回は解決したい問題から考えたいと思います。
問題提起:変数の使用回数を制限したい
例を挙げます。
- 一度freeしたメモリを指している変数を使わせたくない。
- 脆弱性あるあるのdouble freeとかuse after freeとかの原因ですね。C/C++をご存じの方なら、よく分かると思います。
- 一度しか使えないリソースを再度使わせたくない。
- ネットワークからデータを読むときなどは一度しか読めないので、間違って複数回読めるようにしたくない、という感じですね。
その解決策として、Linear Typesは
必ず一度使わないといけない。そして、一度しか使えない。
ような型になります。
例えば、2点目のリソースに関して、以下のような実装があったとします。
function readFromNetwork(socket: Socket): byte[] {
// ソケットからデータを読む関数
}
function doSomething(socket: Socket) {
const data = readFromNetwork(socket);
: // 間に100行ぐらいあったりして…
const tmp_data = readFromNetwork(socket); // <- このロジックが怪しい
}
はコンパイラ的には問題ないですが、同じネットワークソケットから2回データを読み出しているの、ちょっと怪しいですよね?
無論、そういう実装はありえるのですが、バグの可能性も十分あります。2回目のreadFromNetwork
では、仮にLinear Typesがあったとしたら、どうなるでしょうか?
(※ またまた存在しない言語仕様です。実際にはTypeScriptでは通りません。)
linearfunction readFromNetwork(socket: Socket): byte[] {
// ソケットからデータを読む関数
}
linearfunction doSomething(socket) {
const data = readFromNetwork(socket);
: // 間に100行ぐらいあったりして…
const tmp_data = readFromNetwork(socket); // <- コンパイルエラー!
:
}
この例では各関数をLinear Typesとして定義しており、変数は一度しか使えない、という制限を加えているため、コンパイラで検知することができます!🎉
Linear Types、それほどメジャーにはなっていませんが、Haskellに最近追加されており、有用性が証明されれば、今後、他の言語にも入ってくるかもしれません。
まとめ
いかがでしたでしょうか?もっとサクっと書くつもりだったのですが、紹介し始めると長い記事になってしまいました。
紹介した型の技術自体は難しさや書きにくさなどのデメリットと捉えられる可能性もあるため、本当に広まるかは未知数ですが、今後も型周りで機能が増えていくことは間違いないかと思います。
次に広まっていく型の機能は何でしょうね?想像するだけでワクワクします。