大規模なコードベースは、動的言語で書かれていると、維持が難しくなります。少なくとも、それがPlay FrameworkをLinkedInに持ち込んだ主任開発者のYevgeniy Brikman氏の発言 JaxConf 2013で記録されたビデオプレゼンテーション (44分)。
なぜ彼はこれを言うのですか?理由は何ですか?
動的言語は大きなコードベースを維持するのを難しくします
警告:私はプレゼンテーションを見ていません。
私はJavaScript(非常に動的な言語)、C#(ほとんどが静的な言語)、およびVisual Basic(静的と動的の両方)の設計委員会に参加しているため、このテーマについて多くの考えがあります。ここで答えに簡単に当てはめるには多すぎます。
大きなコードベース、ピリオドを維持するのは難しいと言いましょう。大きなコードは、どんなツールがあっても自由に作成するのは困難です。あなたの質問は、静的に型付けされた言語で大きなコードベースを維持することが「簡単」であることを意味しません。むしろ、質問は、静的言語よりも動的言語で大きなコードベースを維持することがさらに難しい問題であることを前提としています。とはいえ、動的言語で大規模なコードベースを維持するために費やされる労力が、静的に型付けされた言語に費やされる労力よりもいくらか大きい理由がある。この投稿では、それらのいくつかについて説明します。
しかし、私たちは自分より先を進んでいます。 「動的」言語の意味を明確に定義する必要があります。 「動的」言語とは、「静的」言語の反対を意味します。
「静的に型付けされた」言語は、プログラムの実行状態ではなく、ソースコードのみにアクセスできるツールによる自動正当性チェックを容易にするために設計された言語です。ツールによって推定される事実は「タイプ」と呼ばれます。言語設計者は、プログラムを「タイプセーフ」にするものについて一連のルールを作成し、ツールはプログラムがそれらのルールに従っていることを証明しようとします。そうでない場合は、型エラーが発生します。
対照的に、「動的に型付けされた」言語は、この種のチェックを容易にするようには設計されていません。特定の場所に格納されているデータの意味は、プログラムの実行中に検査することによってのみ簡単に判断できます。
(動的スコープ言語とレキシカルスコープ言語を区別することもできますが、ここでは説明を省略します。動的型付き言語を動的スコープにする必要はなく、静的型付き言語をレキシカルスコープにする必要はありませんが、多くの場合、2つの間の相関関係です。)
さて、ここでは用語を一通り説明したので、大規模なコードベースについて話しましょう。大規模なコードベースには、いくつかの共通の特徴がある傾向があります。
これらすべての特性は、コードを理解する上で障害となり、そのため、コードを正しく変更するための障害となります。つまり、時は金なりです。大規模なコードベースに適切な変更を加えることは、理解を妨げるこれらの障害の性質上、コストがかかります。
予算は有限であり、私たちが持っているリソースを最大限に活用したいので、大規模なコードベースのメンテナーは、これらの障害を軽減することにより、正しい変更を行うコストを削減しようとします。大規模なチームがこれらの障害を軽減する方法のいくつかは次のとおりです。
静的型付け言語は後者の例です。壊れたコードの変更をリポジトリにチェックインする前に、コンパイラ自体に型エラーを探して通知するデバイスを組み込みます。 明示的に入力された言語は、保管場所に何に関する事実で注釈を付ける必要があるそれらに入ることができます。
その理由だけで、動的に型付けされた言語は、大規模なコードベースを維持することを難しくします。なぜなら、コンパイラーによって「無料で」行われる作業は、現在youテストスイートを作成する形式で行う必要があります。変数の意味に注釈を付けたい場合、youはそのためのシステムを考え出す必要があり、新しいチームメンバーが誤って違反した場合は、コンパイラーではなく、コードレビューでキャッチする必要があります。
これが私が構築してきた重要なポイントです:動的に型付けされる言語と、他のすべての機能を欠いている言語との間には強い相関関係があり、大規模なコードベースを維持するコストの削減を容易にします =、およびthatは、動的言語で大きなコードベースを維持することがより困難になる主な理由です。同様に、静的に型付けされている言語と、より大きな言語でのプログラミングを容易にする機能を備えた言語の間には相関関係があります。
JavaScriptを例にとってみましょう。 (私は1996年から2001年までMicrosoftでJScriptの元のバージョンに取り組みました。)JavaScriptの仕様による目的は、サルの上にマウスを置くとサルを踊らせることでした。スクリプトは多くの場合1行でした。私たちは、10行のスクリプトはかなり普通であると考え、100行のスクリプトは巨大であると考えました。この言語は、大規模なプログラミング用に完全に設計されたものではなく、実装の決定、パフォーマンスの目標などは、その仮定に基づいています。
JavaScriptは、1人のユーザーが1つのページですべてを見ることができるプログラム用に特別に設計されているため、JavaScriptは動的に型付けされるだけでなく、大規模なプログラミングで一般的に使用される他の多くの機能が不足しています。
for-in
構造体で、プログラムのどの部分でも自由に変更できます。しかし、大規模なプログラミングを容易にするのは、設備の不足だけではありません。それを難しくする機能もあります。
JavaScriptのエラー管理システムは、スクリプトがWebページで実行されていること、その可能性が高いこと、失敗のコストが低いこと、そして失敗を目にしたユーザーがそれを修正できない人であることを前提に設計されています。コードの作成者ではなく、ブラウザのユーザー。したがって、できるだけ多くのエラーが静かに失敗し、プログラムは何とかしようとしています。言語の目標を考えると、これは妥当な特性ですが、テストケースの作成が難しくなるため、大規模なプログラミングは確実に難しくなります。何も失敗しないと、失敗を検出するテストを書くのが難しくなります。
コードは、eval
などの機能を介したユーザー入力に基づいて、または新しいscript
ブロックをブラウザーのDOMに動的に追加して、コード自体を変更できます。どの静的分析ツールでも、プログラムを構成するコードがわからない場合があります。
等々。
これらの障害を克服し、JavaScriptで大規模なプログラムを構築することは明らかに可能です。現在、多くの数百万行のJavaScriptプログラムが存在しています。しかし、それらのプログラムを構築する大規模なチームはツールを使用し、JavaScriptがあなたの方法でスローする障害を克服するための規律を持っています。
結論として、大きなコードベースを維持するコストを増加させるのは、タイピングの動的な性質だけではありません。それだけでもコストは増加しますが、それだけでは全体の話にはなりません。動的に型付けされたが、名前空間、モジュール、継承、ライブラリ、プライベートメンバーなどが含まれる言語を設計することもできます。実際、C#4はそのような言語です。このような言語は、動的かつ高度なものになります。大規模なプログラミングに適しています。
むしろ、大規模なコードベースでコストを増加させる動的言語から頻繁に欠落しているすべての他のものです。優れたテスト、モジュール化、再利用、カプセル化などの機能も含む動的言語は、大規模なプログラミングで実際にコストを削減できますが、頻繁に使用される多くの動的言語には、これらの機能が組み込まれていません。それら、それはコストを追加します。
それらは、プログラミング言語があなたがコードについて知っていることを主張するために提供するツールのいくつかを意図的に放棄しているからです。
これの最もよく知られている最も明白な例は、厳密/強力/必須/明示的なタイピングです(用語は非常に異議がありますが、ほとんどの人は、一部の言語が他の言語よりも厳しいことに同意します)。うまく使用すると、特定の場所で発生すると予想される値の種類についての永続的なアサーションとして機能します。これにより、考えられるケースが少ないという理由だけで、行、ルーチン、またはモジュールの可能な動作についての推論が容易になります。 。誰かの名前を文字列として扱うだけの場合、多くのコーダーは宣言を入力して、このルールに例外を設けず、指を滑らせたときに時々発生するコンパイルエラーを受け入れます(引用を忘れた)または脳の(この評価は分数を許可することになっていないことを忘れていた)。
他の人は、これがクリエイティブな表現力を制限し、開発を遅くし、コンパイラーが行うべき作業(例えば、型推論を介して)を導入する、またはまったく必要ない(文字列に固執することを忘れない)と考えています。これに関する問題の1つは、人々がどのようなエラーを起こすかを予測するのが非常に悪いということです。ほとんどすべての人が自分の能力を過大評価することがよくあります。もっと油断なく、問題はコードベースが大きくなるにつれて徐々に悪化します-ほとんどの人は実際に顧客名が文字列であることを覚えていますが、78の他のエンティティをすべてID付き、一部に名前付き、一部にシリアル付きで追加します'numbers'、いくつかは実際には数値です(それらを実行するために計算が必要です)いくつかは文字を格納する必要があります。しばらくすると、読み取っているフィールドが実際に保証されているかどうかを覚えるのがかなり難しくなります。 intかどうかを評価します。
したがって、迅速なプロトタイププロジェクトに適した多くの決定は、大規模なプロダクションプロジェクトではあまりうまく機能しません-多くの場合、転換点に気づかずに。これが、万能の言語、パラダイム、またはフレームワークがない理由です(そして、どの言語がより優れていると主張するのがばかげているのか)。
そのプレゼンテーションの作者に聞いてみませんか?それは彼の主張です、結局のところ、彼はそれをバックアップするべきです。
動的言語で開発された非常に大規模で非常に複雑で非常に成功したプロジェクトがたくさんあります。また、静的に型付けされた言語(FBI仮想ケースファイルなど)で記述されたプロジェクトには、多くの見事な失敗があります。
動的言語で記述されたプロジェクトは、静的型付け言語で記述されたプロジェクトよりも小さくなる傾向があることはおそらく本当ですが、それは赤いニシンです:静的型付け言語で記述されたほとんどのプロジェクトは、JavaまたはC、あまり表現力がない動的言語で書かれたほとんどのプロジェクトは、Scheme、CommonLisp、Clojure、Smalltalk、Ruby、Pythonなどの非常に表現力豊かな言語で書かれる傾向があります。
したがって、これらのプロジェクトが小さくなる理由は、あなたができない大規模なプロジェクトを動的言語で書くからではなく、必要としないだからです=大規模なプロジェクトを表現力豊かな言語で作成する…必要なコード行がはるかに少なく、より表現力豊かな言語で同じことを行うための複雑さがはるかに少なくて済みます。
たとえば、Haskellで作成されたプロジェクトも、かなり小さくなる傾向があります。 Haskellで大規模なシステムを書くことができないからではなく、単にhaveを書かないからです。
しかし、少なくとも、静的型システムが大規模なシステムを作成するために提供する必要があるものを見てみましょう。型システムは、特定のプログラムを作成することを妨げます。それがその仕事です。プログラムを作成して型チェッカーに提示すると、型チェッカーは「いいえ、それを書くことはできません。申し訳ありません」と言います。そして特に、型システムは、型チェッカーが「悪い」プログラムを書くことを防ぐように設計されています。エラーのあるプログラム。したがって、その意味では、はい、静的型システムは大規模システムの開発に役立ちます。
ただし、問題があります。停止問題、ライスの定理、およびその他の多くの不完全性定理があり、基本的に1つのことを教えています。プログラムがタイプセーフかどうかを常に判別できるタイプチェッカーを作成することは不可能です。型チェッカーが型セーフかどうかを判断できないプログラムは常に無限に存在します。そして、型チェッカーに対して行うべき常識は1つだけです。これらのプログラムを型セーフでないものとして拒否してください。そして、それらのプログラムの無限数willは、実際には、タイプセーフではありません。ただし、これらのプログラムの数もwillはタイプセーフです!そして、それらのいくつかはさらに役立つでしょう!そのため、型チェッカーは、型安全性を証明できないという理由だけで、有用な型保証プログラムを作成することを妨げてきました。
IOW:型システムの目的は、表現力を制限することです。
しかし、これらの拒否されたプログラムの1つが実際にエレガントで保守しやすい方法で問題を解決するとしたらどうでしょうか。そうすると、そのプログラムを書くことができません。
私はそれは基本的にギブアンドテイクだと思います:静的に型付けされた言語は、悪いプログラムを書くことを制限しますが、その代わりに、良いプログラムを書くことも妨げます。動的言語は、悪いプログラムを書くことを妨げないという犠牲を払って、良いプログラムを書くことを妨げません。
大規模で複雑なシステムを最初から作成する必要がないという理由だけで、大規模システムの保守性にとってより重要な側面は表現力です。
明示的な静的型は、広く理解されており、動的言語では利用できないドキュメントの正しい形式が保証されています。これが補正されない場合、動的コードは単純に読みにくくなり、理解しにくくなります。
データベースバインディングと豊富なテストスイートを含む大規模なコードベースを検討して、動的言語よりも静的言語の利点をいくつか取り上げます。 (いくつかの例は特異な場合があり、静的または動的言語には適用されません。)
一般的な考え方は、他の人が指摘したように、型システムはプログラムの「次元」であり、プログラムを処理する自動化ツール(コンパイラー、コード分析ツールなど)に情報を公開することです。動的言語では、この情報は基本的に取り除かれているため、利用できません。静的言語では、この情報を使用して正しいプログラムを作成できます。
バグを修正するときは、コンパイラーには良さそうに見えるが、ロジックに欠陥があるプログラムから始めます。バグを修正するときは、プログラムのロジックをローカルで修正する編集を行います(クラス内など)が、他の場所(たとえば、前のクラスと連携しているクラス)ではこのロジックを壊します。静的言語で書かれたプログラムは、動的言語で書かれたプログラムよりもはるかに多くの情報をコンパイラーに公開するため、コンパイラーは、動的言語のコンパイラーが行うよりもロジックが壊れている他の場所を見つけるのに役立ちます。これは、ローカルでの変更により、他の場所でのプログラムのタイプの正確性が損なわれ、プログラムを再度実行する前に、タイプの正確性をグローバルに修正する必要があるためです。
静的言語はプログラムの型の正確さを強制し、プログラムでの作業中に発生するすべての型エラーは、動的言語でのプログラムの仮想的な翻訳における実行時の失敗に対応すると想定できるため、前者の方がバグが少ない後者より。結果として、1つのWordで必要なカバレッジテスト、ユニットテスト、およびバグ修正が少なくなります保守が容易になります
もちろん、トレードオフがあります。型システムで多くの情報を公開し、信頼性の高いプログラムを作成する機会を得ることは可能ですが、これを柔軟なAPIと組み合わせるのは難しいかもしれません。
型システムでエンコードできる情報の例をいくつか示します。
—Const correctnessコンパイラは、値が「読み取り専用」でプロシージャに渡されることを保証できます。²
—データベーススキーマコンパイラは、プログラムをデータベースにバインドするコードがデータベース定義に対応していることを保証できます。これは、この定義が変更されたときに非常に役立ちます。 (メンテナンス!)
—システムリソースコンパイラは、システムリソースを使用するコードが、リソースが正しい状態にあるときにのみそれを実行することを保証できます。たとえば、型システム内のファイルの属性close
またはopen
をエンコードすることができます。
suchこのような違いがある場合、ここでコンパイラとインタープリタを区別することは役に立ちません。
静的型付けにより優れたツールが可能になり、プログラマーが既存の大規模なコードベースを理解、リファクタリング、または拡張しようとするときの生産性が向上します。
たとえば、大規模なプログラムでは、同じ名前のメソッドがいくつか存在する可能性があります。たとえば、セットに何かを追加するadd
メソッド、2つの整数を追加するメソッド、銀行口座にお金を預けるメソッドなどがあるとします。小さなプログラムでは、そのような名前の衝突は起こりそうにありません。数人が取り組む大規模なプログラムでは、自然に発生します。
静的に型付けされた言語では、そのようなメソッドは、操作対象の型によって区別できます。特に、開発環境では、メソッド呼び出し式ごとに、呼び出されているメソッドを検出し、そのメソッドのドキュメントでツールチップを表示したり、メソッドのすべての呼び出しサイトを見つけたり、リファクタリング(メソッドのインライン化など)をサポートしたりできます。メソッド名の変更、パラメータリストの変更、...).