世界を動かす技術を、日本語で。

「Advent of Code」の解法を用いたAdaとRustの比較

概要

johnperry-mathAoC2023 プロジェクトに関する GitHub通知設定 について解説。 通知設定変更 には サインイン が必要。 Fork数Star数 も記載。 プロジェクトの公開状態 について説明。 操作手順 を簡潔にまとめ。

johnperry-math / AoC2023の通知設定

  • GitHub 上の AoC2023 リポジトリ公開状態
  • 通知設定 の変更には GitHubアカウントへのログイン が必須
  • Fork数:1件
  • Star数:20件
  • 通知設定 は画面右上の Bellアイコン から選択
  • 未ログインの場合、通知設定のボタンがグレーアウト表示
  • 通知の種類 :全通知、参加中のみ、無効化などの選択肢
  • リポジトリのフォークスター での関心度の確認
  • Publicリポジトリ で誰でも閲覧可能
  • 通知設定の変更手順
    • サインイン
    • リポジトリページ右上の 通知アイコン をクリック
    • 希望する通知タイプを選択

Fork・Star・公開設定について

  • Fork :他ユーザーによる リポジトリの複製数
  • Star :ユーザーの お気に入り登録数
  • Public全ユーザー閲覧可能な状態
  • 通知設定個別リポジトリ単位 で管理
  • アクティビティの追跡更新情報の受け取り に便利

Hackerたちの意見

Adaがオープンソースのコンパイラを持ってるのは面白いね。数年前に見たときは、プロプライエタリなコンパイラしかないと思ってたから、あんまり調べなかったんだ。今、もう一度見てみようかな。

GNATは30年も前からあるんだ。GPLランタイム例外なしでリリースされたバージョンがあったから、リンクするにはプログラムもGPLでリリースしなきゃいけなかったんだけど、それはもうずいぶん前の話だよ。

GNATは90年代からあって、GCCをベースにしてる。私の大学ではコンパイラに関する作業をして、リアルタイムプログラミングのような学部の授業で使ってた。確か、プログラミング入門の授業でAdaを使おうとした試みもあったけど、結局Pascalを選んで、最終的にはC++になったと思う。

Adaには本当に良いアイデアがあるのに、主に安全性が重要なコミュニティ以外ではあまり使われなかったのが残念だよね。範囲が制限された数値型を作れるのは、特定のバグのクラスにはすごく役立つんだ。Spark Adaは比較的学びやすい基準で、SIL 4に準拠したソフトウェアを開発するのに役立ったよ。最近は、何でもありの30年間を経て、また安全なソフトウェアを作ろうという流れがある気がする。数十年の安全性の教訓を基にして、もっと良い言語を作ることができたらいいな。良いアイデアがマイナーな言語に埋もれて忘れ去られちゃうのはもったいないよね。

範囲が制限された数値型を作れるのは、特定のバグのクラスにはすごく役立つんだ。そうだよね!RustにAdaの数値範囲機能があったら最高だな!

範囲が制限された数値型を作れる能力は、特定のバグのクラスにとって本当に役立つよ。これはC++でよく使う機能なんだ。標準ライブラリの一部ではないけど、現代のC++では範囲制限された数値型をプログラムで生成するのは簡単だよ。いくつかの安全チェックは実行時ではなくコンパイル時に行えることもあるし、プログラミング言語の標準機能にすべきだと思う。

30年以上前にAdaでプログラミングしてたけど、今も同じ気持ちで、何度も失望してきた。今回は違うかもしれないね。

NimはAdaとModulaにインスパイアされていて、サブレンジもあるんだ。[1]: type Age = range[0..200] let ageWorks = 200.Age let ageFails = 201.Age で、コンパイル時にエラーが出るよ: $ nim c main.nim エラー: 201はAgeに変換できません [1] https://nim-lang.org/docs/tut1.html#advanced-types-subranges

Adaには本当に良いアイデアがたくさんあるのに、安全性が重要なコミュニティ以外ではあまり使われなかったのが残念だね。範囲が制限された数値型を作れるのは、特定のバグのクラスにはすごく役立つ。pjmlpが別のコメントで言ってるように、Pascalはこの機能を最初から持ってたはずだし、初期のバージョンからあったと思うよ。最初のTurbo Pascalのバージョンよりも前にね。

Ada、少なくともGNATは、コンパイル時の次元解析(単位チェック)もサポートしてるよ。私はエンジニアリングアプリケーションを主に扱ってるから偏見があるかもしれないけど、他の言語ではなぜそれがサードパーティのライブラリに委ねられているのか理解できないな。 https://docs.adacore.com/gnat_ugn-docs/html/gnat_ugn/gnat_ug...

C#はAdaやRustと同じ領域では競争していないけど、似たような範囲属性があるよ。ただ、唯一の欠点は、ASP.NETのように特定のタイミングで自動的に行わない限り、手動で検証関数を呼び出さなきゃいけないことだね。

個人的な経験から言うと、安全性だけじゃなくて、生成されるものの信頼性も大事な要素だよ。型システムで制約を表現できると、より高品質なコードが生まれるんだ。RustとGolangの経験からの簡単な例としては、ミューテックスの扱いがあるね。Rustはガードハンドルを漏らさせないけど、Golangはデッドロックに突っ込むことを許しちゃう。

ちょっと関連するけど、最近3Dプリントの分野で見た中で面白いプロジェクトの一つがPruntだよ。プリンターの制御基板とファームウェアで、後者はAdaで開発されてるんだ。https://prunt3d.com/ https://github.com/Prunt3D/prunt ちょっとエソテリックな選択だけど、「ああ、これはコンセプト的には悪くないな」と思ったよ。

ここに選んだ理由について書いたよ: https://news.ycombinator.com/item?id=42319962

著者がRustはデフォルトで並行プログラミングをサポートしてないって言ってるのはちょっと変だと思った。彼は別のコメントにリンクしてて、非同期にTokioは必要ないって指摘してるけど(それはその通り)、それを除いても非同期だけが並行性を実現する方法じゃないよ。スレッドは言語に組み込まれてるし、非同期より使いやすいんだ。スレッドを大量に生成する必要があってリソースの問題が出る場合を除けば、ほとんどのプログラムではスレッドが良い選択だと思う。

(Rustを使ったことがない人からの素朴な質問。)Rustのスレッドと非同期のキャンセルの話はどう違うの?他の言語の非同期と比べてどう?特に違う理由はないと思うけど、私の経験(C++、Python、C#)では、キャンセルはシンプルなスレッドやブロッキングコールよりも非同期の方がずっと良いんだ。多くの言語でブロッキングコールを使っていると、ソケットのシャットダウンを整理するのはほぼ不可能だよ。標準の読み取りスレッドと書き込みスレッドを前提にすると、ソケットスレッドを中断する信頼できる方法はソケットを閉じることだけで、それが望んでいることじゃない場合もあるし、原則的にファイルハンドルの再利用バグに対して脆弱になる可能性もある。非同期のキャンセルは、言語によっては達成可能だけど難しい(すでに改善されてるけど)か、素晴らしいものだよ。Trio [1]を使うと、比較されていないソケット操作が完了するか、影響を与えないことが保証されるんだ。Rustのスレッドやブロッキングコールでは、これがもっと良くなってるのかな?私の無知な理解では、キャンセルをキャッチして処理する方法がないから、非同期の方が他の言語よりも実際には悪いと思ってる。Pythonみたいに例外を使って処理するわけじゃないからね。Adaでも状況が良くなってるとは思えないけど、それについてもぜひ教えてほしいな。

それか、プラットフォームがスレッドを簡単にサポートしていない場合 - WASMや組み込み(Embassy)とかね。正直、100k人が一度に私のアニメブログを訪れるっていう通常の理由より、非同期を使う動機としてはこっちの方が良いと思う。

Tokioみたいなワークスティーリングスケジューラが、単にスレッドをたくさん作って仕事をするよりも明らかに速くなる境界線ってどこなんだろう?それと、スレッドを作ることで深刻な問題が起きるハードカットオフはどこなのかも気になる。たぶん、結構小さいかもね。マップの例で言うと、5つの要素を入れるだけなら、超シンプルなVecMapで十分だけど、25個になるとVecMapは実際のハッシュテーブルよりちょっと悪くなるし、100個だとVecMapは笑えるほどひどい結果になるから。

理論的には正しいかもしれないけど、実際にはAsyncをスレッドの代わりに使う理由は、使いたいクレートがasyncだからってことが多いよ。Reqwestはいい例で、Tokioなしでは使えない。Ureqは存在していて、ちゃんと動くけどね。私はすごく高レベルのアプリケーションプロジェクトをやったことがあって、全てのasyncを避けようとしたけど、ある時から逆流しているように感じ始めたよ。

著者は明らかな違いをいくつか指摘してるけど、Adaには正式な仕様があってRustにはないっていうのがその一つだね。rustcは流動的で、リファレンス実装でもあるみたい。新しいコンパイラやアナライザーを書く場合には重要かもしれないけど、最も明白な違い、ユーザーにとって多分最も重要なことは言及されてないんだ。それは採用状況やエコシステム、ツール、ライブラリ、コミュニティなんだよね。Adaは航空宇宙やライフセーフティでの成功の歴史があるけど、実際のプロジェクト、つまり分散システムやOSコンポーネントに取り組むときは、現代のデータフォーマットやプロトコル、IDE、そして人々とのインターフェースが選択に影響するから、初日からそれを考慮しないといけないよ。

Rustには、Ferroceneから寄付された仕様があるんだ。この仕様スタイルはAdaの仕様に影響を受けてる。今は公開されていて、https://rust-lang.github.io/fls/ で見られるよ。これはFerroceneが安全証明書コンパイラを提供するための取り組みの一環なんだ。もう利用可能になってるよ。

RustもAdaも、定理証明器が使えるような形式的な仕様ではないよ。私の知る限り、Ada Sparkの場合、もちろんSparkに組み込まれた言語の意味に関する仮定があるけど、それが本当に形式的(機械可読)な仕様として一貫して書かれているわけではないね。

年間10億人を安全に輸送しているフライトコントロールソフトウェアのプログラマーたちは、君の「現実のプロジェクト」を見て、「そうだね、出力があまり重要でないソフトウェアを書いているなら、私たちのプロセスは過剰だよ」って返すと思うよ :p

この書き込みは、AdaがRustに比べて文化的や型に関する不利な点があるかもしれないけど、一般的には可読性のコンテストではAdaが勝っているように見えるってことを示してるね。比較に欠けているのはコンパイラの速度で、Adaはかつて複雑な言語と見なされていたけど、Rustと比べるとそうでもないかもしれない。いずれにせよ、投稿ありがとう。リアルなプロジェクトでAdaを使ってみたくなったよ。

ケーススタディ2の最後の方に「クライアントがSIDE_LENGTHを知る必要があるなら、その値を返す関数を追加できます」と書いてあるね。確かにそうだけど、これは定数だから、実装ではその関数のようにもっと直接的に書けるよ:pub const SIDE_LENGTH: usize = ROW_LENGTH;

AdaとRustの文字列について。AdaのデザインはUnicodeよりも前(1980年代初頭 vs 1991年)だから、AdaのStringは基本的にchar配列なんだ。一方、Rustの文字列はUnicodeテキスト型。これが、AdaのStringsがバイトの配列でインデックスできるのに対して、Rustの文字列はUTF-8でエンコードされたバッファでテキストとして扱うべきだからインデックスできない理由だね。Rustの実装はここでバイト配列を使うこともできたかもしれない。

AdaのStringは基本的にchar配列 さらに悪いことに、組み込みのUnicode文字列はUnicodeスカラーの配列で、一般的にはUTF-32なんだ。AFAIK、UTF-8の文字列リテラルを書く適切な方法はないし、リテラルに応じて8ビット、16ビット、または32ビットの文字の配列から変換する必要があるよ。

Rustの文字列にインデックスを付けることはできるけど、たぶんそれはあまり望ましくないよね。「Clown」[2..5] // は「own」だし。これが範囲だってことに注意して。Rustの文字列スライスタイプは自分を単なる配列だとは考えてないから、整数インデックスをそのまま使うことはできないんだ。インデックスはサブ文字列の開始と終了を指定する整数の範囲なんだよ。Unicode文字の真ん中を指定するとコードがパニックになるから、そういうのはやめてね。そうだね、AoCはいつもASCIIを使うから、通常は&[u8](バイトスライスへの参照)を使うのが理にかなってるし、実際にstr::as_bytesメソッドを使うと、必要なバイトスライスがそのまま得られるよ。

両方の言語がスタック中心のプログラミングイディオムを促進しているとは思わないな。Adaは静的な割り当てを推奨してるし。

値に依存する型のことを「依存型」って言うんだ。例えば、サイズNのリストや、範囲内の数値は、全部依存型って呼ばれるものだよ。Idrisは見た目はHaskellに似てるし、Leanや他のいくつかの言語もこの機能を持ってるよ。https://en.wikipedia.org/wiki/Dependent_type