プログラミングの変化勉強会
2023-02-10
箇条書きでまとめた上記ページは1万PV以上見られている
(先週Kozanebaにつけた手軽に線を引く機能の動作テストを兼ねて)
今日はそれをざっくり解説してから重要なポイントについてコードを交えて解説する
まずは全体像
https://gyazo.com/69c8676477b96050ae696d51b7206796
これが本当の全体像
https://gyazo.com/b4784d52c52797355c8566cf07125941
少しズーム
当初「この10年の間にあったプログラミング言語の仕様変更で大きなものってなんですかねー」という問いでスタートした
が、集まった意見を見るとどうも「プログラミング言語の仕様」という枠組みに収まらないものがある
一番大きなもの: ブラウザ
他には機械学習やコンテナ技術がこの10年でプログラミングというアクティビティに大きな影響を与えた
プログラミング言語の仕様の話に入る前にまずこれをざっと見る
ブラウザ
https://gyazo.com/93b2e5ee8df55544778ed45a0f64b700
10年前の2013年にiPhone5Sが出た
その後10年でスマートフォンが普及した
スマートフォンアプリを作る仕事が増えた
一方でiOS向けとAndroid向けを個別に作るのは高コスト
WebView(ブラウザコンポーネント)でやろうという動きが生まれる
時を同じくしてデスクトップ環境にも似たことが起こっていた
2013年、Electron登場
GUIアプリケーションを、Windows、Mac、Linuxと作り分けるのは高コスト
ChromiumレンダリングエンジンとNode.jsで実現しようという動きが生まれる
これが現実的な選択肢であることがElectronベースで作られているMicrosoftのIDE「Visual Studio Code」(2015)で如実に示された
IDEのような複雑なものがまともなパフォーマンスで動く、ならばこれでいいじゃないか、となった
この10年で起こったこと
ブラウザの技術的進歩
これが「Webサービスの提供」という狭い範囲ではなく
「GUIの提供」という広い範囲に影響を与えた
Q&A
human.icon相対的にOSの重要性が下がった?
nishio.icon「オペレーションをするシステム」としてのOSの「オペレーション」をブラウザがだいぶ奪っていった
最近、InstaChordって電子楽器を買ったんですけど、これのファームアップデートの時にもうネイティブアプリなんかいらなくて、ブラウザからUSB接続した周辺機器と直接シリアル通信するの
Web Serial API は、ウェブサイトがシリアルデバイスからデータを受信したり、シリアルデバイスにデータを送信したりする方法を提供します。対象は、シリアルポートで接続されたデバイスのことも、シリアルポートとして振る舞う USB や Bluetooth のデバイスのこともあります。MDN 10年前なら「Windowsでこのexeファイルを実行してください」とか言われてたようなことがもうOSと無関係にできちゃう時代
視線を戻す
https://gyazo.com/2b3acb8b22ae82979a0687e63a677081
大きなトピック3つある
やっばり型が欲しい
async/await
破壊的更新を避ける
破壊的更新を避けるプログラミングスタイルに関してはさらに様々な要素が絡んでいる
この講義資料を作ってる段階ではまだ整理し切れてないが、ざっくり見ていこう
(注: 図では「脱オブジェクト指向」が大きく表示されているが、これは「オブジェクト指向」という言葉でイメージするものが人によってマチマチな曖昧なフレーズなので避けることにした)
GUI開発と破壊的更新を避ける動き
破壊的更新を避けるプログラミングスタイルの話はGUIの話と結構関連している
https://gyazo.com/770ae9733b7c4e687a5bdd3a71a8d588
かつてGUIはオブジェクト指向の重要な応用領域だった
個別のボタンは、ボタンクラスのインスタンス、個別のラベルは、ラベルクラスのインスタンス、…
GUIはオブジェクト指向のメリットを素直に理解しやすい応用ドメインだった
しかし10年前の時点でその使い方には変化の兆しがあった
Java 周辺には「実装の再利用を目的とした継承」を避ける方 向への動きが見られます。たとえばグラフィックツールキットの実装がそ うです。1995年にリリースされたAbstract Window Toolkit(AWT)では「継承して各種メソッドをオーバーライドして使う」ルールでした。しかし、今 Eclipseなどで使われているStandard Widget Toolkit(SWT)は「継承してはいけない」というルールです。
代わりに発達したのが「委譲」の概念です
(余談: 文章に「今」と書いたのは失敗だったな、2023年の今の読者はEclipseがわからないかも…)
2019年、React「Hooks API」
プログラマが状態のあるGUIコンポーネントを定義する際に、クラスを使わない
代わりに状態を持たない関数を使う
この関数コンポーネントが、クラスコンポーネントと同等のことができるようにする仕組みがHooks API
関数コンポーネントの方を標準的な使い方とみなしていく
Hooks APIと関数コンポーネント
状態のあるGUIコンポーネント
「例えばクリックするとオンオフが切り替わるスイッチ」を考えてみよう
これは「オンオフ」が状態
クラスでコンポーネントを定義する場合、素朴に考えるとコンポーネントの状態はメンバ変数として持つことになる
関数コンポーネントでは、状態をReact側が持つ
Hooks APIと呼ばれる仕組みによって、状態の現在の値とsetterを取得する
https://gyazo.com/a16e89398f997015f385a40f8fed22ef
状態を更新する時にはそのsetterを呼ぶ
状態の変更をReactに任せることで、コンポーネントの側が状態を持たなくなった、なので関数で良くなった
これの何が嬉しいのか
Reactの側が「状態が更新されたこと」を知ることができる
クラスがメンバ変数を書き換える実装スタイルだとフレームワークが変化を察知しにくい
特に過去のオブジェクト指向の考え方だと「メンバーをprivateにして外から見えないようにしよう」となりがち
Reactは「その状態を参照するコンポーネント」も知っている
その状態をReadしてきたコンポーネントだけがその状態に依存した振る舞いをする
再描画が必要なのはこのコンポーネントだけ
https://gyazo.com/34daca8ccd87549b9ce7fa42076fabbd
対象の関数コンポーネントをReactが呼び出し、その結果を使って再描画をする
Q: 状態を管理する責任を負わず、状態遷移の方法だけ記述する?
A: まあそうですね。状態管理をあちこちのオブジェクトが各自でやってるとカオスなわけです。なのでReactの一箇所にまとめる的な話
関数コンポーネントはHooks APIによって最新の状態を得てレンダリング結果(仮想DOM)を返す、状態を持たない関数として設計できるようになる
プログラマがちゃんと書けば副作用を持たない関数になる
つまり状態の更新回数と同じ回数呼ぶ必要がない
状態の更新と同期的である必要もない
例えばボタンを押すと101回オンオフが切り替わるコードを書いたとしても、レンダリングは1回だけ実行される、などということが可能になる
これがパフォーマンスに対して重要、これができるようにするためにみんな関数コンポーネントを使おうね、そうするともっと速くなるよ、という流れ
human.iconQ: Webアプリは状態を持たずに、状態はデータベースにある的な作り方も一般的ですね
nishio.iconA: うーん、それは「状態」という言葉で指してるものが違うような。
状態にも「永続化する状態」と「しない状態」がある
GUIにおける「状態」はどのテキストボックスにフォーカスが当たってるかとかも含んでいる
ブラウザ上のすべての状態をデータベースに送るのは現実的ではない
ブラウザ上にしかない状態も存在しているはず
human.iconReactのモデルでも関数コンポーネントの中にローカル変数があって細かい状態はそこで持っていると言える
nishio.icon関数のローカル変数は関数を抜けるとアクセスできなくなるわけなので、それを状態とはあまり考えないのでは
Reactの状態の一部に関して更新をウォッチして、更新があったらデータベースに書き込みにいくという画面に表示されないコンポーネントがあってもいい
状態が更新されたときに、そのどれをデータベースに保存してどれを保存しないかはプログラマーのチョイス
Reactにすべての状態が集まっているから、その中の「永続化したい状態」が更新されたときに、それをデータベースに書き込みにいくって設計は、コンポーネントがバラバラに状態を持っているよりも実装しやすくなるだろう
具体例を追加
Kozanebaではドラッグドロップでの要素の移動は、ドロップのタイミングで全部データベースに保存している
Kozanebaでは要素をクリックするとコンテキストメニューが開く
この「どのメニューがどこに開いているか」もReactが管理してGUIのレンダリングに影響する「状態」
保存すれば、データベースの書き込みコストと引き換えに、次回の起動時に復元できる
これをデータベースに保存すべきだとは思わない。コストに見合うメリットがないから
human.iconQ2: 状態を持たない方が好ましいのは再利用のためではなく応用が広がるからか?
nishio.iconA: 疎結合にしたい
状態が変われば再描画は必要
しかしそこが同期的呼び出しでは密結合すぎる
実際のところその密結合が必要か?
人間の目の時間解像度が低いからブラウザ上での表示の更新ディレイが多少あっても問題ない
非同期にすることで必要なくなる更新もある
だから「状態の書き換え」と「状態の書き換えにトリガーされた再描画」を切り離したかった
これがHooks APIを入れていく最大のモチベーションだったのではないかと僕は思う
リアクティブプログラミング系の流れ
https://gyazo.com/fa53b2b0babf79eda645d5b80af0f684
名前がまちまち、概念にも揺れがある
説明できるほど詳しくないことに気づいたので今回は事実をざっと眺めるだけにする
2007 C# LINQ(Language INtegrated Query)
code:c#
// Specify the data source.
int[] scores = { 97, 92, 81, 60 };
// Define the query expression.
IEnumerable<int> scoreQuery =
from score in scores
where score > 80
select score;
// Execute the query.
foreach (int i in scoreQuery)
{
Console.Write(i + " ");
}
// Output: 97 92 81
配列などのコレクションに対してもSQLのようにクエリランゲージを書ける
2009~2011 C# Rx(ReactiveExtensions)
LINQの考え方が非同期処理に応用される
データベースと配列が「似たもの」として統一的に扱われるようになった流れの延長で、ストリームの概念を使って非同期処理を記述するようになった
2014 Java Stream API
code:java
int sum = widgets.stream()
.filter(w -> w.getColor() == RED)
.mapToInt(w -> w.getWeight())
.sum();
もちろん、各メソッドの呼び出しで大きな一時オブジェクトが作られたりはしない
2020 Java / C#: Record型
code:c#
public record Person(string FirstName, string LastName);
code:java
record Rectangle(double length, double width) { }
レコード・クラスは一連のフィールドを宣言し、適切なアクセッサ、コンストラクタ、equals、hashCodeおよびtoStringメソッドが自動的に作成されます。このクラスは単純な「データキャリア」として機能することを意図しているため、フィールドはfinalです。
この簡潔な宣言は、次の標準クラスと同等です:
code:java
public final class Rectangle {
private final double length;
private final double width;
public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
double length() { return this.length; }
double width() { return this.width; }
// Implementation of equals() and hashCode(), which specify
// that two record objects are equal if they
// are of the same type and contain equal field values.
public boolean equals...
public int hashCode...
// An implementation of toString() that returns a string
// representation of all the record class's fields,
// including their names.
public String toString() {...}
}
Q: すごく単純なstruct?
A: 破壊的更新が禁止されてるところがstructとの大きな違い
Q: 先に代入することができない?単純にデータをトランスファーするオブジェクト?
A: そう。そういうオブジェクトを作る手段が今まではたくさんコードを書かないと実現できなかった。
2009年とかの発展の過程で「ここを伝わるオブジェクトが破壊可能なのはおかしいよね」「破壊不可能なオブジェクトが手軽にかける手段が必要だよね」ということでJavaとC#が変わった
Q2: const dataってことですか
A2: そう言っていいと思います
Q3: ミューテックスだけは可変に持ちたいという欲が出てくると思う
A: ミューテックス周りのことは追いかけてないですね、わかりません
先ほどのReactで関数コンポーネントが仮想DOMを返していたのも同じ
ブラウザの生のDOMを人間が破壊的に更新することを避ける
仮想DOMを返す=新しいオブジェクトを作って返す
それを生のDOMに反映させる破壊的更新の作業は人間ではなくReactがやる
ざっくりまとめ
書き換え不能な(immutableな, finalな)「データキャリア」オブジェクトを受け取って、新しいオブジェクトを返す関数のつらなりとして物事を記述する
オブジェクトを破壊的に書き換えるのではなく、そういう方法を使おうという流れが生まれた
それを楽に記述するための方法が言語の側に入って行った
非同期処理関連ではこれが大きい
https://gyazo.com/aeda341c5e9ddd54bb6bffe639b64f32
async関数はPromiseを返し、awaitはPromiseが解決されるまでブロックする
Promise(またはfuture)は古くからある考え方
1997年のE言語で今使われているような実装になった
この10年の間にC#、Python、ECMAScript、Rustなどが相次いでこのやり方を採用した
Promiseとはなんなのか
非同期処理を実行したタイミングでブロッキングせず即座に値が返される、これがPromise
「後で値を提供しますね」という約束
比喩より定義が見たい人にはこれ
An open standard for sound, interoperable JavaScript promises—by implementers, for implementers.
Promiseは3つの状態を持つ
https://gyazo.com/607ca5a38fb90ddbb4ef57eae202abb8
thenメソッド
code:js
promise2 = promise1.then(onFulfilled, onRejected);
https://gyazo.com/8834e1646f07cd1ee5ce56e1196a27cb
非同期処理の表現方法には他にはコールバックを渡すとか、イベントハンドラを登録するとかがあった
code:js
// callback style
do_something(params, on_success, on_error);
code:js
r = new Request(params);
r.on_success = on_success;
r.on_error = on_error;
r.send()
かつてのJavaScriptでは他のサービスのAPIを叩いたりするのにXMLHttpRequestを使っていた、これはイベントハンドラを使うスタイルだった
これが面倒だったのでサードパーティライブラリが試行錯誤し、Promiseを使うスタイルがデファクトスタンダードになった
そのあとでPromiseを使うスタイルのAPIが標準化された
何が面倒だったか?色々あるが:
非同期処理をした後にやりたいことをソースコード上で先に書くのは思考の流れが混乱する
非同期処理1をした後、その結果を使って非同期処理2をして…と続けて行った場合のエラー処理
If onRejected is not a function and promise1 is rejected, promise2 must be rejected with the same reason as promise1.
コールバックやイベントハンドラのスタイルでは、名前付きの関数にした上であちこちのon_errorにそれを指定することになる
https://gyazo.com/6ca93fa1196d2124f7c7ecc0a484c065
動的型付けの言語が「やっぱり型が欲しい」と言い出して漸進的型付け(Gradual Typing)を導入した 例
code:python
def foo(x: int):
y: str = x # NG
error: Incompatible types in assignment (expression has type "int", variable has type "str") [assignment]
全部に型宣言を書きたくない
では型宣言の書いてないものは何か?
暗黙のany型
any型ってJavaのjava.lang.Objectみたいな型?
違います
code:python
def foo(x: int):
y: object = x # OK
code:python
def foo(x: object):
y: str = x # NG
error: Incompatible types in assignment (expression has type "object", variable has type "str") [assignment]
Object型の値をString型の値に代入しようとすること(暗黙のダウンキャスト)はNG
これが普通の型の振る舞い
anyがアップキャストもダウンキャストも暗黙に行える特殊な型ということ
https://gyazo.com/8e18254db69518ccc099cf724fdb1f86https://gyazo.com/802836aae4333668c25a40bf1f6b5120
TypeScriptではこの二つの挙動に対応する型が unknownとany
code:ts
const take_number = (x: number): void => {};
{
let x: any;
take_number(x); // OK
}
{
let x: unknown;
take_number(x); // NG
}
Argument of type 'unknown' is not assignable to parameter of type 'number'.
現状のPython(mypy)では返り値の型を推測しない
code:python
def foo(x: int):
return x
print(typing.reveal_type(foo))
note: Revealed type is "def (x: builtins.int) -> Any"
これはTypeScriptとは異なる挙動
https://gyazo.com/485b1234b2e40fa0839bf9379e8ddce3
人間がこう明示することが期待されている
code:python
def foo(x: int) -> int:
return x
print(typing.reveal_type(foo))
note: Revealed type is "def (x: builtins.int) -> builtins.int"
名前的型付けと構造的型付け
https://gyazo.com/dbb2e686e58f68db832dcc6ab1d0ad05
https://gyazo.com/df9ab60204aa68fff5318dccb0c6a6da
メモリに0と1が並んでいるだけでは、それが整数なのか浮動小数点数なのかわからない。
そこでint n;などと宣言することで「これは整数だよ」という情報を変数につけるのが静的型付け
「この値は整数だよ」という情報を値につけるのが動的型付け
この「変数にメタ情報をつけるアプローチ」に二つある
構造を使って判断する派
名前を使って判断する派
https://gyazo.com/d5e7ab2fc66dd5cecc55a95e5667d199
1985年に生まれたC++や1995年に生まれたJavaは名前的型システムを採用していた。
名前が異なるものには互換性がない
extendsやimplementsで部分型関係の宣言をする
この2つの言語はとてもメジャーになったので、このやり方に慣れ親しんだ人も多いだろう。
一方、型理論の研究においては構造的型システムの方が主流だった。
その結果、JavaScriptを生成するための静的型付け言語を設計する際に、Javaなどを参考に名前的型システムを採用した言語(Dartなど)と、型理論を参考に構造的型システムを採用する言語(TypeScriptなど)が混在することになってしまった。
TypeScriptでの例
code:TypeScript
type T1 = {
x : number
}
type T2 = {
x : number
}
let a: T1;
let b: T2 = {x: 1};
a = b; // OK
このコードのlet b:T2 = {x: 1}の、{x: 1}の部分は名前のないオブジェクト型である。
これはT1ともT2とも同じ形をしているので、エラーになることなく代入できる。
構造の同じ型に違う名前をつけて、区別されることを期待してしまった事例
Rust: 所有権・ライフタイム
Rustは2010年にリリースされた
2015年にRust 1.0がリリースされた
2016年から2020年にかけてStack Overflowによる「開発者がもっとも愛するプログラミング言語」でRustが1位を取り続けた
Rustは所有権やライフタイムの概念が特徴的とされるが、所有権はRust特有の概念ではない
C++11でstd::unique_ptrが採用されている
これはつまり「確保したメモリを唯一指しているポインタ」
このポインタがスコープを抜ける時にはそのメモリを参照する手段がなくなるのだから解放してよい
C++は過去のコードとの互換性のために、従来通りのシンプルな記法は従来通りの挙動、新しい挙動には新しい記法、という縛りがある
Rustは違う言語になることでこの互換性の縛りから逃れる
code:rust
let x = String::from("hello");
let y = x;
println!("x: {}", x); // NG
println!("y: {}", y);
error: borrow of moved value: x
xやyがC++でいうところのunique_ptrになっていて、y = xでムーブが行われている
C++では普通は参照のコピーをしようとしてしまう
「コピーではなくムーブを意図しているんだ」と表現するためにstd::moveを使う
これを既存のコードを壊さずに実現するために「右辺値/左辺値」の区別がC++11で追加された
「ある値が右辺値であるなら、今後使わないのでコピーではなくムーブでよい」ということ
こうして右辺値参照を受け取るムーブコンストラクタが追加された
std::moveは右辺値参照にキャストすることでムーブが行われるようにする
C++においては「有効でないポインタをデリファレンスしないように気をつける」はコンパイラの仕事ではない
人間が気をつけるか、静的解析ツールでチェックする
Rustではコンパイラがそれをチェックする
上記のRustコードと同じことをC++でやろうとしたらSegmentation faultになってしまった
code:cpp
int main () {
std::unique_ptr<std::string> x = std::make_unique<std::string>(std::string("abc"));
std::unique_ptr<std::string> y;
y = std::move(x);
std::cout << *y << std::endl;
std::cout << *x << std::endl; // here
}
code:output
abc
Segmentation fault
このコードでは、間違って読んだり解放したりしてしまわないようにxはnullになっている
「スコープを抜けた時に解放されるのはRAIIと同じでは?何が珍しいの?」と思ってた
重要なのは「シンプルな書き方をした時にmove」というデフォルト挙動の変化の方だと思う
例
code:rust
let x = String::from("hello");
foo(x); // move
println!("x: {}", x); // NG
この例では関数呼び出しのところでxがムーブしているからそれ以降使えない
代入によるムーブや関数呼び出しによるムーブをたどっていって「ムーブせずにスコープを抜けたところ」で解放をするようにコンパイラがやってくれる
ライフタイムの概念
参照が有効であるスコープ
スコープという言葉の今を多くの人がソースコード上のブロックと関連付けて考えるので(字句的スコープだと解釈するようになったので)スコープと呼ぶと混乱が起きる
呼び出し元で作られた参照が呼び出し先でも有効である特徴はPerlなどにあった動的スコープに似ている
code:rust
let x;
let y;
// println!("x: {}", x); // NG
// errorE0381: used binding x is possibly-uninitialized x = String::from("hello");
println!("x: {}", x); // OK
y = x;
// println!("x: {}", x); // NG
// errorE0382: borrow of moved value: x xの初期化前とムーブ後の利用がコンパイルエラーになることがわかる
ライフタイム指定子
ライフタイムをコンパイラが推測できない時に教えてやる記法
独特な記法に戸惑う人も多いみたい
これは変数に対してメタデータを付与する行為なので広い意味で「型」
ライフタイム指定子が「名前をつけなければいけない」ので慣習的に'aとつけるのは、ジェネリクスで型変数にTと名前をつけるのと同じ
名前自体に意味があるのではなく同じ名前が複数箇所で使われることによって、その要素の間の関係を表現する
例1
複数の参照に同じ名前のライフタイム指定子をつけると、その共通部分集合がライフタイムになる
code:rust
fn longest(x: &str, y: &str) -> &str { // NG
if x.len() > y.len() {
x
} else {
y
}
}
code:error
errorE0106: missing lifetime specifier --> src/main.rs:14:33
|
14 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from x or y
help: consider introducing a named lifetime parameter
|
14 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
例2
staticライフタイム
あらかじめシステムによって名前が付いているライフタイム指定子
「プログラム全体にわたるライフタイム」を表現している
code:rust
fn dangle() -> &String {
let s = String::from("hello");
&s
}
code:error
errorE0106: missing lifetime specifier --> src/main.rs:22:16
|
22 | fn dangle() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the 'static lifetime
|
22 | fn dangle() -> &'static String {
| +++++++
これは本来はダメ
関数を抜ける時にsが解放されるから
なんだけどコンパイラが「定数に変えたらプログラム全体をライフタイムにできるな」と判断してる
その結果2歩先を行くエラーメッセージになってる
コンパイラが定数にできるかわからないようにしてやれば素直なエラーメッセージになる
code:rust
fn dangle(x: &String) -> &String {
let s = x.clone();
&s
}
code:error
errorE0515: cannot return reference to local variable s --> src/main.rs:31:5
|
31 | &s
| ^^ returns a reference to data owned by the current function