web-dev-qa-db-ja.com

Pythonのような動的に型付けされた言語でのみ可能なデザインパターンはありますか?

私は関連する質問を読みました Pythonのような動的言語では不要なデザインパターンはありますか? この引用を覚えています Wikiquote.orgで

動的型付けの素晴らしい点は、計算可能なものを表現できることです。また、タイプシステムはそうではありません。タイプシステムは通常決定可能であり、サブセットに制限されます。静的型システムを好む人は、「それは問題ありません。それで十分です。記述したいすべての興味深いプログラムはタイプとして機能します。しかし、それはばかげています。いったん型システムを作成すると、興味深いプログラムが何であるかさえわかりません。

---ソフトウェアエンジニアリングラジオエピソード140:Gilad BrachaによるNewspeakおよびPluggableタイプ

引用の定式化を使用して「タイプとして機能しない」という有用な設計パターンまたは戦略はあるのでしょうか。

30
user7610

ファーストクラスのタイプ

動的型付けとは、ファーストクラスの型があることを意味します。言語固有の型を含め、実行時に型を検査、作成、保存できます。また、valuesvariablesではなくと入力されます。

静的に型付けされた言語は、メソッドのディスパッチ、型クラスなどの動的な型にも依存するコードを生成する可能性がありますが、通常はランタイムからは見えません。せいぜい、イントロスペクションを実行するためのいくつかの方法を提供します。あるいは、型を値としてシミュレートすることもできますが、その場合はアドホック動的型システムがあります。

ただし、動的型システムが(== --- ==)のみのファーストクラス型を持つことはほとんどありません。あなたは、ファーストクラスのシンボル、ファーストクラスのパッケージ、ファーストクラス...すべてのものを持つことができます。これは、静的に型付けされた言語におけるコンパイラの言語とランタイム言語の厳密な分離とは対照的です。コンパイラーまたはインタープリターがランタイムで実行できることも可能です。

ここで、型推論は良いことであり、コードを実行する前にコードをチェックすることに同意します。ただし、実行時にコードを生成してコンパイルできることも気に入っています。また、コンパイル時にも事前計算するのが大好きです。動的に型付けされた言語では、これは同じ言語で行われます。 OCamlには、プリプロセッサ言語とは異なるメインの型システムとは異なるモジュール/ファンクターの型システムがあります。 C++では、メイン言語とは何の関係もないテンプレート言語があり、一般に実行中の型は無視されます。そして、それらの言語ではfineです。彼らはそれ以上提供したくないからです。

最終的に、開発できるソフトウェアの種類whatは実際には変わりませんが、表現力はhowそれらを開発し、それが難しいかどうか。

パターン

動的型に依存するパターンは、動的環境に関係するパターンです。オープンクラス、ディスパッチ、オブジェクトのメモリ内データベース、シリアライゼーションなどです。ジェネリックコンテナーのような単純なものは、実行時にベクターが保持するオブジェクトの型について忘れないため、機能します(パラメトリックタイプは必要ありません)。

Common LISPでコードを評価する多くの方法と、可能な静的分析の例(これはSBCLです)を紹介しようとしました。サンドボックスの例は、別のファイルからフェッチされたLISPコードの小さなサブセットをコンパイルします。合理的に安全にするために、私は読み取り可能テーブルを変更し、標準シンボルのサブセットのみを許可し、タイムアウトでラップします。

;;
;; Fetching systems, installing them, etc. 
;; ASDF and QL provide provide resp. a Make-like facility 
;; and system management inside the runtime: those are
;; not distinct programs.
;; Reflexivity allows to develop dedicated tools: for example,
;; being able to find the transitive reduction of dependencies
;; to parallelize builds. 
;; https://gitlab.common-LISP.net/xcvb/asdf-dependency-grovel
;;
(ql:quickload 'trivial-timeout)

;;
;; Readtables are part of the runtime.
;; See also NAMED-READTABLES.
;;
(defparameter *safe-readtable* (copy-readtable *readtable*))
(set-macro-character #\# nil t *safe-readtable*)
(set-macro-character #\: (lambda (&rest args)
                           (declare (ignore args))
                           (error "Colon character disabled."))
                     nil
                     *safe-readtable*)

;; eval-when is necessary when compiling the whole file.
;; This makes the result of the form available in the compile-time
;; environment. 
(eval-when (:compile-toplevel :load-toplevel :execute)
  (defvar +WHITELISTED-LISP-SYMBOLS+ 
    '(+ - * / lambda labels mod rem expt round 
      truncate floor ceiling values multiple-value-bind)))

;;
;; Read-time evaluation #.+WHITELISTED-LISP-SYMBOLS+
;; The same language is used to control the reader.
;;
(defpackage :sandbox
  (:import-from
   :common-LISP . #.+WHITELISTED-LISP-SYMBOLS+)
  (:export . #.+WHITELISTED-LISP-SYMBOLS+))

(declaim (inline read-sandbox))

(defun read-sandbox (stream &key (timeout 3))
  (declare (type (integer 0 10) timeout))
  (trivial-timeout:with-timeout (timeout)
    (let ((*read-eval* nil)
          (*readtable* *safe-readtable*)
          ;;
          ;; Packages are first-class: no possible name collision.
          ;;
          (package (make-package (gensym "SANDBOX") :use '(:sandbox))))
      (unwind-protect
           (let ((*package* package))
             (loop
                with stop = (gensym)
                for read = (read stream nil stop)
                until (eq read stop)
                ;;
                ;; Eval at runtime
                ;;
                for value = (eval read)
                ;;
                ;; Type checking
                ;;
                unless (functionp value)
                do (error "Not a function")
                ;; 
                ;; Compile at run-time
                ;;
                collect (compile nil value)))
        (delete-package package)))))

;;
;; Static type checking.
;; warning: Constant 50 conflicts with its asserted type (MOD 11)
;;
(defun read-sandbox-file (file)
  (with-open-file (in file)
    (read-sandbox in :timeout 50)))

;; get it right, this time
(defun read-sandbox-file (file)
  (with-open-file (in file)
    (read-sandbox in)))

#| /tmp/plugin.LISP
(lambda (x) (+ (* 3 x) 100))
(lambda (a b c) (* a b))
|#

(read-sandbox-file #P"/tmp/plugin.LISP")

;; 
;; caught COMMON-LISP:STYLE-WARNING:
;;   The variable C is defined but never used.
;;

(#<FUNCTION (LAMBDA (#:X)) {10068B008B}>
 #<FUNCTION (LAMBDA (#:A #:B #:C)) {10068D484B}>)

上記のものは、他の言語で行うことが「不可能」ではありません。 Blender、音楽ソフトウェア、またはオンザフライで再コンパイルを行う静的にコンパイルされた言語用のIDEなどのプラグインアプローチ。外部ツールの代わりに、動的言語はすでに存在する情報を利用するツールを支持します。 FOOの既知の呼び出し元すべて? BARのすべてのサブクラス?クラスZOTに特化したすべてのメソッド?これは内部化されたデータです。タイプは、これのもう1つの側面です。


(参照: [〜#〜] cffi [〜#〜]

4
coredump

短い答え:いいえ、チューリング同値なので。

長い答え:この男はトロールです。型システムが「サブセットに制限する」ことは事実ですが、サブセットの外のものは、定義上、機能しないものです。

任意のチューリング完全プログラミング言語(汎用プログラミング用に設計された言語に加えて、そうでないもので十分な言語)で実行できることはすべて、クリアするのがかなり低く、システムがチューリングになる例がいくつかあります-意図せずに完了する)。他のチューリング完全プログラミング言語で実行できます。これは「チューリング同値性」と呼ばれ、それはまさにそれが言うことを意味するだけです。重要なのは、それが他の言語で同じように簡単に他のことを実行できることを意味するわけではありません。それは、最初から新しいプログラミング言語を作成することの全体的なポイントであると主張する人もいます。既存の言語が苦手なもの.

たとえば、動的型システムは、すべての変数、パラメーター、および戻り値を基本Object型として宣言するだけで、静的OO型システムの上にエミュレートできます。次に、リフレクションを使用して内部の特定のデータにアクセスします。これに気づくと、動的言語では静的言語では実行できないことは文字通り何もできないことがわかります。しかし、そのように実行するのは非常に面倒です。もちろん。

引用によると、静的型が実行できることを制限することは正しいですが、それは重要な機能であり、問​​題ではありません。道路上の線は車でできることを制限しますが、制限があると思いますか、それとも役に立ちますか? (私は、反対方向に行く車が自分の側に留まって、私が運転しているところに来ないように指示しない何もない忙しい、複雑な道路を運転したくないことを知っています!)何を明確に示すルールを設定することによって無効な動作と見なされ、それが発生しないようにすることで、厄介なクラッシュが発生する可能性を大幅に低減できます。

また、彼は反対側を誤解しています。それは「書きたいすべての興味深いプログラムが型として機能する」ということではなく、「書きたいすべての興味深いプログラムがrequire型になる」 」特定のレベルの複雑さを超えると、2つの理由から、型システムを使用せずにコードベースを維持し続けることが非常に難しくなります。

まず、型注釈のないコードは読みにくいからです。次のPythonを考えてみます。

_def sendData(self, value):
   self.connection.send(serialize(value.someProperty))
_

接続の反対側のシステムが受信するデータのように見えると思いますか?そして、それが完全に間違っているように見えるものを受け取っている場合、何が起こっているのかをどのように理解しますか?

それはすべて_value.someProperty_の構造に依存します。しかし、それはどのように見えますか?良い質問! sendData()とは何ですか?それは何を通過していますか?その変数はどのように見えますか?それはどこから来たの?ローカルでない場合は、valueの履歴全体をトレースして、何が起こっているのかを追跡する必要があります。多分あなたはsomePropertyプロパティも持っている他のものを渡していますが、それはあなたが思っていることをしていませんか?

非常によく似た構文を使用しているが静的に型付けされているBoo言語でわかるように、型注釈でそれを見てみましょう。

_def SendData(value as MyDataType):
   self.Connection.Send(Serialize(value.SomeProperty))
_

何か問題が発生している場合、デバッグ作業が突然簡単になりました。MyDataType!の定義を調べてください。さらに、同じ名前のプロパティを持つ互換性のない型を渡したために不正な動作が発生する可能性は突然ゼロになります。これは、型システムがそのミスを許さないためです。

2番目の理由は、1番目の理由に基づいています。大規模で複雑なプロジェクトでは、おそらく複数の貢献者がいます。 (そうでない場合は、長い時間をかけて自分で構築しています。これは基本的に同じことです。信じられない場合は、3年前に書いたコードを読んでみてください!)これは、何があったのかわからないことを意味します。あなたがそこにいなかったか、ずっと前に自分のコードだったか覚えていないので、彼らが書いたときにコードのほとんどすべての特定の部分を書いた人の頭をくぐります。型宣言があると、コードの目的が何であるかを理解するのに役立ちます。

引用の男のような人々は、静的型付けの利点を、ほぼ無制限のハードウェアリソースによって年ごとの関連性が低くなる世界で「コンパイラを支援する」または「すべてを効率化する」ことであると誤解しがちです。しかし、すでに示したように、これらの利点は確かに存在しますが、主な利点は人的要因、特にコードの可読性と保守性にあります。 (追加された効率は確かに素晴らしいボーナスです!)

39
Mason Wheeler

「パターン」の部分を回避するつもりです。パターンとは何か、またはパターンではないものの定義に展開するものだと思うので、その議論への興味を失っています。私が言うことは、ある言語ではできることが他の言語ではできない言語があるということです。はっきりさせておきますが、私はではありません解決できる問題解決できない1つの言語であると言います別の。メイソンはすでにチューリング完全性を指摘している。

たとえば、pythonでクラスを記述しましたが、これはXML DOM要素をラップし、それを最初のクラスオブジェクトにします。つまり、次のコードを記述できます。

doc.header.status.text()

解析されたXMLオブジェクトからそのパスのコンテンツを取得します。きちんと整頓された、IMO。そして、ヘッドノードがない場合は、ダミーオブジェクトのみを含むダミーオブジェクトを返します(カメはずっと下にあります)。たとえば、Javaでこれを行う実際の方法はありません。 XMLの構造に関するある程度の知識に基づいて、事前にクラスをコンパイルしておく必要があります。これが良いアイデアかどうかはさておき、この種のことは、動的言語で問題を解決する方法を本当に変えます。ただし、常に良い方法で変化するとは言っていません。動的なアプローチにはいくつかの明確なコストがあり、メイソンの答えはまともな概要を示しています。それらが良い選択であるかどうかは、多くの要因に依存します。

ちなみに、あなたはcan Javaでこれを行う JavaのPythonインタプリタ を作成できるためです。特定の問題を解決するという事実与えられた言語の問題は、通訳者やそれに似たものを作ることを意味するかもしれません。

27
JimmyJames

引用は正しいですが、本当に不誠実です。それを分解して、その理由を見てみましょう。

動的型付けの素晴らしい点は、計算可能なものを表現できることです。

まあ、かなり。 languageと動的型付けを使用すると、 Turing complete である限り、ほとんどのものを表現できます。型システム自体では、すべてを表現することはできません。ここで彼に疑いの恩恵を与えましょう。

また、タイプシステムはそうではありません。タイプシステムは通常決定可能であり、サブセットに制限されます。

これは本当ですが、type systemが許可するものについて固く話していることに注意してください。タイプを使用するlanguageについてではありませんシステムが許可します。型システムを使用してコンパイル時にデータを計算することは可能ですが、これは一般に(型システムは一般に決定可能であるため)完全なチューリングではありませんが、ほとんどすべての静的型付き言語は、ランタイムで完全にチューリングします(依存型付き言語はそうではありませんが、ここではそれらについて話しているとは思いません)。

静的型システムを好む人は、「それは問題ありません。それで十分です。記述したいすべての興味深いプログラムはタイプとして機能します。しかし、それはばかげています。いったん型システムを作成すると、興味深いプログラムが何であるかさえわかりません。

問題は、動的型言語には静的型があることです。時にはすべてが文字列であり、より一般的にはすべてのものがプロパティのバッグまたはintやdoubleのような値のいずれかであるタグ付きユニオンがあります。問題は、静的言語でもこれを実行できることです。これまで、これを行うのは少し不格好でしたが、最近の静的型付き言語では、動的型言語を使用するのと同じくらい簡単に実行できるため、どのように違いがあるのでしょうか。プログラマーが興味深いプログラムとして見ることができるものは何ですか?静的言語は、他の型と同じように、まったく同じタグ付き共用体を持っています。

タイトルの質問に答えるには、いいえ。静的型付け言語で実装できないデザインパターンはありません。それらを取得するのに十分な動的システムを常に実装できるからです。動的言語で「無料」で入手できるパターンがあるかもしれません。 [〜#〜] ymmv [〜#〜] の場合、これらの言語の欠点を我慢する価値があるかもしれません。

10
jk.

動的に型付けされた言語でしかできないことは確かにあります。しかし、それらは必ずしもgoodデザインではありません。

最初に整数5、次に文字列_'five'_、またはCatオブジェクトを同じ変数に割り当てます。しかし、コードの読者が何が起こっているのか、すべての変数の目的が何であるのかを理解することを難しくしているだけです。

ライブラリに新しいメソッドを追加する可能性がありますRubyクラスとそのプライベートフィールドにアクセスします。そのようなハッキングが役立つ場合がありますが、これはカプセル化違反になります(私はしません)パブリックインターフェイスのみに依存するメソッドを追加することはできますが、それは静的に型指定されたC#拡張メソッドでは実行できないことではありません。)

新しいフィールドを他の誰かのクラスのオブジェクトに追加して、追加のデータを渡すことができます。ただし、新しい構造を作成するか、元のタイプを拡張する方が良い設計です。

一般的に、コードを整理しておくと、型定義を動的に変更したり、同じ型の変数に異なる型の値を割り当てたりできるというメリットが少なくなります。ただし、コードは静的型付け言語で実現できるものと変わりません。

動的言語が得意とするのは構文糖です。たとえば、逆シリアル化されたJSONオブジェクトを読み取る場合、ネストされた値を単に_obj.data.article[0].content_と呼ぶ場合があります-obj.getJSONObject("data").getJSONArray("article").getJSONObject(0).getString("content")と言うよりもはるかに簡潔です。

Ruby開発者は、特に_method_missing_を実装することで実現できる魔法について詳しく話すことができます。これは、宣言されていないメソッドへの呼び出しを処理できるようにするメソッドです。たとえば、ActiveRecord ORMはこれを使用して、_find_by_email_メソッドを宣言せずにUser.find_by_email('[email protected]')を呼び出すことができるようにします。もちろん、静的に型付けされた言語でUserRepository.FindBy("email", "[email protected]")として達成できなかったものは何もありませんが、それをきちんと否定することはできません。

4
kamilk

動的プロキシパターンは、プロキシする必要があるタイプごとに1つのクラスを必要とせずにプロキシオブジェクトを実装するためのショートカットです。

_class Proxy(object):
    def __init__(self, obj):
        self.__target = obj

    def __getattr__(self, attr):
        return getattr(self.__target, attr)
_

これを使用して、Proxy(someObject)someObjectと同じように動作する新しいオブジェクトを作成します。もちろん、何らかの機能を追加する必要もありますが、これは最初から役立つベースです。完全な静的言語では、プロキシするタイプごとに1つのProxyクラスを作成するか、動的コード生成を使用する必要があります(これは確かに、多くの静的言語の標準ライブラリに含まれています。この原因を実行できない問題)。

動的言語のもう1つの使用例は、いわゆる「モンキーパッチ」です。多くの点で、これはパターンではなくアンチパターンですが、慎重に行うとcanを便利な方法で使用できます。そして理論モンキーパッチが静的言語で実装できなかった理由はありませんが、実際にそれを持っているものを見たことはありません。

4
Jules

はい、動的に型付けされた言語でのみ可能である多くのパターンとテクニックがあります。

Monkey patchingは、プロパティまたはメソッドが実行時にオブジェクトまたはクラスに追加される手法です。この手法は、静的型付け言語では不可能です。これは、コンパイル時に型と操作を検証できないためです。別の言い方をすると、モンキーパッチをサポートする言語は、動的言語である定義です。

言語がモンキーパッチ(または実行時に型を変更するための同様の手法)をサポートしている場合、静的に型チェックできないことが証明されています。したがって、これは現在存在する言語の単なる制限ではなく、静的型付けの基本的な制限です。

したがって、引用は間違いなく正しいです-静的型付き言語よりも動的言語でより多くのことが可能です。一方、特定の種類の分析は、静的型付け言語でのみ可能です。たとえば、特定の型で許可されている操作を常に知っているため、コンパイル型での不正な操作を検出できます。実行時に操作を追加または削除できる動的言語では、そのような検証は不可能です。

これが、静的言語と動的言語の競合に明らかな「最善」がない理由です。静的言語は、コンパイル時に異なる種類の能力と引き換えに、実行時に特定の能力を放棄します。これにより、バグの数が減り、開発が容易になります。トレードオフに価値があると考える人もいれば、そうしない人もいます。

他の回答は、チューリング同等性とは、1つの言語で可能なことはすべての言語で可能であることを意味すると主張しています。しかし、これは従いません。静的言語でのモンキーパッチなどをサポートするには、基本的に静的言語内に動的サブ言語を実装する必要があります。もちろんこれは可能ですが、ホスト言語に存在する静的な型チェックも失われるため、埋め込み動的言語でプログラミングしていると私は主張します。

バージョン4以降のC#では、動的に型付けされたオブジェクトがサポートされています。言語設計者は明らかに、両方のタイプのタイピングを利用できることの利点を理解しています。しかし、それはまた、あなたがケーキを食べて私を食べることができないことも示しています:C#で動的オブジェクトを使用すると、モンキーパッチなどの機能を実行できますが、これらのオブジェクトとの相互作用の静的型検証も失われます。

3
JacquesB

引用の定式化を使用して「タイプとして機能しない」という有用な設計パターンまたは戦略はあるのでしょうか。

はいといいえ。

プログラマーがコンパイラーよりも精度の高い変数の型を知っている状況があります。コンパイラは何かがオブジェクトであることを知っているかもしれませんが、プログラマはそれが実際には文字列であることを(プログラムの不変により)知っています。

この例をいくつか示します。

_Map<Class<?>, Function<?, String>> someMap;
someMap.get(object.getClass()).apply(object);
_

SomeMapの作成方法により、someMap.get(T.class)が_Function<T, String>_を返すことを知っています。しかし、Javaは、関数があることを確信しているだけです。

もう一つの例:

_data = parseJSON(someJson)
validate(data, someJsonSchema);
print(data.properties.rowCount);
_

Data.properties.rowCountは、スキーマに対してデータを検証したため、有効な参照と整数になることを知っています。そのフィールドがないと、例外がスローされます。しかし、コンパイラーは、それが例外をスローしているか、ある種の汎用JSONValueを返すかを知っているだけです。

もう一つの例:

_x, y, z = struct.unpack("II6s", data)
_

「II6」は、データが3つの変数をエンコードする方法を定義します。フォーマットを指定したので、返されるタイプがわかります。コンパイラは、それがタプルを返すことだけを知っています。

これらすべての例の統一テーマは、プログラマが型を知っていることですが、Java=レベルの型システムはそれを反映できません。コンパイラは型を知らないため、静的に型付けされた言語では呼び出せませんが、動的に型付けされた言語では呼び出せます。

それはそれが得ている元の引用です:

動的型付けの素晴らしい点は、計算可能なものを表現できることです。また、タイプシステムはそうではありません。タイプシステムは通常決定可能であり、サブセットに制限されます。

動的型付けを使用する場合、私の言語の型システムが知っている最も派生した型だけでなく、私が知っている最も派生した型を使用できます。上記のすべてのケースで、意味的に正しいコードがありますが、静的タイピングシステムによって拒否されます。

ただし、質問に戻るには:

引用の定式化を使用して「タイプとして機能しない」という有用な設計パターンまたは戦略はあるのでしょうか。

上記の例のいずれか、および実際に動的型付けの例は、適切なキャストを追加することにより、静的型付けで有効にすることができます。コンパイラが知らない型がわかっている場合は、値をキャストしてコンパイラに伝えます。したがって、あるレベルでは、動的型付けを使用して追加のパターンを取得することはできません。静的に型付けされたコードを機能させるには、さらにキャストする必要があるかもしれません。

動的型付けの利点は、型システムにその有効性を納得させるのが難しいという事実に悩まされることなく、これらのパターンを簡単に使用できることです。使用可能なパターンは変更されません。型システムにパターンを認識させる方法や型システムを破壊するキャストを追加する方法を理解する必要がないため、実装が簡単になる可能性があります。

2
Winston Ewert

以下は、C++(静的型付け)では不可能なObjective-C(動的型付け)の例です。

  • いくつかの異なるクラスのオブジェクトを同じコンテナに配置します。
    もちろん、これにはコンテナーのコンテンツを後で解釈するためのランタイム型検査が必要であり、静的型付けのほとんどの友人は、最初にこれを行うべきではないことに反対します。しかし、私は、宗教的な議論を超えて、これが重宝することを発見しました。

  • サブクラス化せずにクラスを拡張する。
    Objective-Cでは、NSStringなどの言語で定義されたものを含め、既存のクラスの新しいメンバー関数を定義できます。たとえば、メソッドstripPrefixIfPresent:を追加して、[@"foo/bar/baz" stripPrefixIfPresent:@"foo/"]と言うことができます(NSSringリテラル@""の使用に注意してください)。

  • オブジェクト指向のコールバックの使用。
    JavaおよびC++のような静的に型付けされた言語では、ライブラリがユーザー指定のオブジェクトの任意のメンバーを呼び出すことができるようにするには、かなりの長さにする必要があります。Javaでは、回避策は、インターフェイス/アダプターのペアと匿名クラスです。C++では、通常、回避策はテンプレートベースです。これは、ライブラリコードをユーザーコードに公開する必要があることを意味します。Objective-Cでは、オブジェクト参照とセレクターのセレクターを渡すだけです。メソッドをライブラリに追加すると、ライブラリはコールバックを簡単かつ直接呼び出すことができます。