23

編集しました。私の質問は、静的型言語の合計型の代わりに、通常どのような慣用的なClojure構造が使用されるかということです。これまでのコンセンサス:動作を統一できる場合はプロトコルを使用し、そうでない場合はタグ付きペア/マップを使用し、必要なアサーションを事前条件と事後条件に配置します。

Clojureは、ベクトル、マップ、レコードなど、製品タイプを表現する多くの方法を提供しますが、タグ付き共用体やバリアントレコードとも呼ばれる合計タイプをどのように表現しますか?Either a bHaskellやEither[+A, +B]Scalaのようなもの。

最初に頭に浮かぶのは、特別なタグが付いたマップです{:tag :left :value a}。しかし、その後、すべてのコードが条件付きで汚染され、(:tag value)特別なケースがない場合は処理されます...私が確認したいのは次のとおりです。それ:tagは常に存在し、指定された値の1つのみを取ることができ、対応する値は一貫して同じタイプ/動作でありnil、存在することはできません。コード内のすべてのケースを処理したことを簡単に確認できます。

の行にマクロがあると考えることができますがdefrecord、合計タイプの場合は次のようになります。

; it creates a special record type and some helper functions
(defvariant Either
   left Foo
   right :bar)
; user.Either

(def x (left (Foo. "foo")))   ;; factory functions for every variant
; #user.Either{:variant :left :value #user.Foo{:name "foo"}}
(def y (right (Foo. "bar")))  ;; factory functions check types
; SomeException...
(def y (right ^{:type :bar} ()))
; #user.Either{:variant :right :value ()}

(variants x) ;; list of all possible options is intrinsic to the value
; [:left :right]

このようなものはすでに存在しますか?回答:いいえ)。

4

7 に答える 7

21

タグ付き共用体およびバリアントレコードとも呼ばれる合計タイプをどのように表現しますか?Either a bHaskellや Either[+A, +B]Scalaのようなもの。

Either2つの用途があります。2つのタイプのいずれかの値を返すか、タグに基づいて異なるセマンティクスを持つ必要がある同じタイプの2つの値を返すことです。

最初の使用は、静的型システムを使用する場合にのみ重要です。 EitherHaskell型システムの制約を考えると、基本的に可能な最小のソリューションです。動的型システムを使用すると、必要な任意の型の値を返すことができます。Either必要ありません。

2番目の使用法重要ですが、2つ(またはそれ以上)の方法で非常に簡単に実行できます。

  1. {:tag :left :value 123} {:tag :right :value "hello"}
  2. {:left 123} {:right "hello"}

私が確認したいのは、:tagは常に存在し、指定された値の1つのみを取ることができ、対応する値は一貫して同じタイプ/動作であり、nilにすることはできないということです。簡単な方法があります。コード内のすべてのケースを処理したことを確認してください。

これを静的に確認したい場合、Clojureはおそらくあなたの言語ではありません。理由は単純です。式は実行時まで、つまり値を返すまで型を持ちません。

マクロが機能しない理由は、マクロの展開時にランタイム値がな​​いため、つまりランタイムタイプがないためです。シンボル、アトム、sexpressionsなどのコンパイル時の構成があります。evalそれらは可能ですが、使用することevalは多くの理由で悪い習慣と見なされます。

ただし、実行時にかなり良い仕事をすることができます。

  • 私が確認したいのは、:tagが常に存在するということです。
  • そしてそれは指定された値の1つだけを取ることができます
  • 対応する値は一貫して同じタイプ/動作です
  • ゼロにすることはできません
  • そして、私がコード内のすべてのケースを処理したことを確認する簡単な方法があります。

私の戦略は、通常は静的なもの(Haskellの場合)をすべて実行時に変換することです。コードを書いてみましょう。

;; let us define a union "type" (static type to runtime value)
(def either-string-number {:left java.lang.String :right java.lang.Number})

;; a constructor for a given type
(defn mk-value-of-union [union-type tag value]  
  (assert (union-type tag)) ; tag is valid  
  (assert (instance? (union-type tag) value)) ; value is of correct type  
  (assert value)  
  {:tag tag :value value :union-type union-type}) 

;; "conditional" to ensure that all the cases are handled  
;; take a value and a map of tags to functions of one argument
;; if calls the function mapped to the appropriate tag
(defn union-case-fn [union-value tag-fn]
  ;; assert that we handle all cases
  (assert (= (set (keys tag-fn))
             (set (keys (:union-type union-value)))))
  ((tag-fn (:tag union-value)) (:value union-value)))

;; extra points for wrapping this in a macro

;; example
(def j (mk-value-of-union either-string-number :right 2))

(union-case-fn j {:left #(println "left: " %) :right #(println "right: " %)})
=> right: 2

(union-case-fn j {:left #(println "left: " %)})
=> AssertionError Assert failed: (= (set (keys tag-fn)) (set (keys (:union-type union-value))))

このコードは、次の慣用的なClojureコンストラクトを使用します。

  • データ駆動型プログラミング:「タイプ」を表すデータ構造を作成します。この値は不変でファーストクラスであり、ロジックを実装するために使用できる言語全体があります。これは、Haskellができるとは思わないことです。実行時に型を操作します。
  • マップを使用して値を表す。
  • 高次プログラミング:fnsのマップを別の関数に渡します。

Eitherポリモーフィズムに使用している場合は、オプションでプロトコルを使用できます。それ以外の場合、タグに興味がある場合は、形式の何かが{:tag :left :value 123}最も慣用的です。あなたはしばしばこのようなものを見るでしょう:

;; let's say we have a function that may generate an error or succeed
(defn somefunction []
  ...
  (if (some error condition)
    {:status :error :message "Really bad error occurred."}
    {:status :success :result [1 2 3]}))

;; then you can check the status
(let [r (somefunction)]
  (case (:status r)
    :error
    (println "Error: " (:message r))
    :success
    (do-something-else (:result r))
    ;; default
    (println "Don't know what to do!")))
于 2012-06-08T14:40:49.347 に答える
6

一般に、動的型付け言語の合計型は次のように表されます。

  • タグ付きペア(例:コンストラクターを表すタグ付きの製品タイプ)
  • ディスパッチを実行するための実行時のタグのケース分析

Either静的に型付けされた言語では、ほとんどの値は型によって区別されます。つまり、またはを持っているかどうかを知るためにランタイムタグ分析を行う必要はありません。Maybeしたがって、タグを見て、それがまたはであるかどうかを知るだけLeftですRight

動的型付けの設定では、最初にランタイム型分析を実行し(どのタイプの値を使用しているかを確認)、次にコンストラクターのケース分析を実行する必要があります(どのフレーバーの値を使用しているかを確認します)。

1つの方法は、すべてのタイプのすべてのコンストラクターに一意のタグを割り当てることです。

ある意味で、動的型付けは、すべての値を単一の合計型に入れ、すべての型分析を実行時テストに延期することと考えることができます。


私が確認したいのは、:tagは常に存在し、指定された値の1つのみを取ることができ、対応する値は一貫して同じタイプ/動作であり、nilにすることはできないということです。簡単な方法があります。コード内のすべてのケースを処理したことを確認してください。

余談ですが、これは静的型システムが何をするかについての説明です。

于 2012-06-08T13:10:02.600 に答える
4

タイプされたclojureのような驚くべきものが完成しなければ、アサーションの実行時チェックを回避することはできないと思います。

実行時チェックに確実に役立つclojureによって提供されるあまり知られていない機能の1つは、事前条件と事後条件の実装です(http://clojure.org/special_formsおよびfogusによるブログ投稿を参照)。事前条件と事後条件を備えた単一の高階ラッパー関数を使用して、関連するコードのすべてのアサーションをチェックすることもできると思います。これにより、ランタイムチェックの「汚染問題」を非常にうまく回避できます。

于 2012-06-14T14:44:03.693 に答える
4

これが一部の言語で非常にうまく機能する理由は、結果に対して(通常はタイプごとに)ディスパッチするためです。つまり、結果のプロパティ(通常はタイプ)を使用して、次に何をするかを決定します。

したがって、clojureでディスパッチがどのように発生するかを確認する必要があります。

  1. nil特殊ケース-nil値はさまざまな場所で特殊ケースになっており、「Maybe」の「None」部分として使用できます。たとえば、if-let非常に便利です。

  2. パターンマッチング-ベースclojureは、シーケンスを破棄することを除けば、これをあまりサポートしていませんが、サポートしているさまざまなライブラリがあります。ADTとパターンマッチングのClojureの置き換えを参照してください。[更新:コメントの中で、mnickyはそれが時代遅れであり、core.matchを使用する必要があると言っています]

  3. OOを使用したタイプ別-メソッドはタイプ別に選択されます。そのため、親のさまざまなサブクラスを返し、オーバーロードされたメソッドを呼び出して、必要なさまざまな操作を実行できます。あなたが非常に奇妙で不器用に感じる機能的なバックグラウンドから来ている場合、それはオプションです。

  4. 手作業によるタグ-最後に、明示的なタグを使用するcaseか、使用することができます。condさらに便利なことに、それらを希望どおりに機能するある種のマクロでラップすることができます。

于 2012-06-08T12:56:41.573 に答える
4

動的に型付けされた言語であるため、Clojureでは、Haskell /Scalaよりも型の関連性/重要性がやや低くなります。それらを明示的に定義する必要はありません。たとえば、タイプAまたはタイプBのいずれかの値を変数にすでに格納できます。

したがって、これらの合計タイプで何をしようとしているのかによって異なります。タイプに基づくポリモーフィックな動作に本当に関心がある可能性があります。その場合、合計タイプのポリモーフィックな動作を一緒に与えるプロトコルと2つの異なるレコードタイプを定義することが理にかなっている場合があります。

(defprotocol Fooable
  (foo [x]))

(defrecord AType [avalue]
  Fooable 
    (foo [x]
      (println (str "A value: " (:avalue x)))))

(defrecord BType [bvalue]
  Fooable 
    (foo [x]
      (println (str "B value: " (:bvalue x)))))

(foo (AType. "AAAAAA"))

=> A value: AAAAAA

これにより、合計タイプから得られる可能性のあるほぼすべてのメリットが得られると思います。

このアプローチの他の素晴らしい利点:

  • Clojureではレコードとプロトコルは非常に慣用的です
  • 優れたパフォーマンス(プロトコルディスパッチが大幅に最適化されているため)
  • プロトコルにnilの処理を追加できます(経由extend-protocol
于 2012-06-14T09:11:23.050 に答える
4

タグ付きのベクトルをベクトルの最初の要素として使用し、core.matchを使用してタグ付きデータを非構造化します。したがって、上記の例では、「どちらかの」データは次のようにエンコードされます。

[:left 123]
[:right "hello"]

次に構造を解除するには、 core.matchを参照して次を使用する必要があります。

(match either
  [:left num-val] (do-something-to-num num-val)
  [:right str-val] (do-something-to-str str-val))

これは他の答えよりも簡潔です。

このYouTubeトークでは、マップ上でバリアントをエンコードするためにベクトルが望ましい理由について、より詳細に説明しています。私の要約では、マップを使用してバリアントをエンコードすることは問題があります。これは、マップが通常のマップではなく「タグ付きマップ」であることを覚えておく必要があるためです。「タグ付きマップ」を適切に使用するには、常に2段階のルックアップを実行する必要があります。最初にタグ、次にタグに基づくデータです。マップでエンコードされたバリアントでタグをルックアップするのを忘れた場合、またはタグまたはデータのキールックアップを間違えた場合は、追跡が困難なnullポインター例外が発生します。

ビデオでは、ベクトルエンコードされたバリアントのこれらの側面についても説明しています。

  • 不正なタグをトラップします。
  • 必要に応じて、TypedClojureを使用して静的チェックを追加します。
  • このデータをDatomicに保存します。
于 2016-11-30T12:57:24.183 に答える
2

いいえ、現時点ではclojureにはそのようなものはありません。実装することはできますが、IMOは静的に型付けされた言語に適しているようで、clojureのような動的な環境ではあまりメリットがありません。

于 2012-06-08T12:00:02.420 に答える