web-dev-qa-db-ja.com

宣言型コードと命令型コードの混合(暗黙の「ユニットテスト」?)

私は専門家ではありませんが、学生として、言語とそのデザインパターン/目標に興味があります。

次の例で見逃している点があるかどうか、このような手法が一般的な言語で広く使用されていない理由を知りたいのですが、実際のプログラムで明示的に行う方がよいのはなぜですか?いくつかの宣言型コードを使用する代わりに、すべての単体テストを定義します。言語が一般的にテストおよび実装としていくつかの宣言を使用しないのはなぜですか?なぜこれらを2つの部分に分割するのですか?

次の単体テストがあるとします。

assertRaises(avg([]), ValueError)
assertEquals(avg([1], 1)
assertEquals(avg([1, -2]), -.5)
assertEquals(avg([1, 2]), .5)

この擬似コードを取る場合、

def avg(items):
    if len(items) == 0:
        raise ValueError
    else if len(items) == 1:
        return items[0]
    else:
        return sum(items)/len(items)

avgのほとんどの部分は何もしないか、いくつかの単体テストに合格する方法を宣言するだけのようです。言い換えれば、私にとっては、コードの重複のように感じます。特に、この例では最初のテストは不要のようです。

この方法でいくつかのユニットテストを取り除くのは良い設計になるのだろうかと思います。

def avg(items) {
    len(items) == 0 => raises ValueError
    len(items) == 1 => items[0]
    len(items) > 1 => sum(items)/len(items)
    (test) items == [1, -2] => $avg == -.5
    (test) items == [1, 2] => $avg == .5
    (test) avg(items) <= sum(items)
}

この例では、(test)は、特定のテストケースが有用な定義ではない場合を示します(ただし、まれに最適化に使用される場合もあります)。もちろん、この擬似コードは最初の実装よりも読みにくいため、うまく設計されていませんが、単体テストを宣言するためのより便利な方法だと思います。実際、ユニットテストは定義をテストするだけなので、ここでは間違っているようです。そして、最後のテストを実行するのは簡単ではありません。ただし、たとえば、avgを含むコードをテストする別のテストを実行するときに実行できるため、有用なテストパラメーターのセットが提供される可能性があります。また、たとえば、最後の単体テストにも2番目の役割があります。私がこのようなコードを持っている場合:

if avg(a) <= sum(a)

実行時にテストケースの1つが直接それを暗示しているため、複雑な定義を評価する必要はまったくありません。 OK、この場合、それは実際の最適化ではありません(IDEはユーザーに通知できますが、式は常に真であるため便利です)。しかし、私は想像できます。は多くの複雑な例であり、開発者が関連するすべての場所で特別なケースを明示的にテストすることなく、この方法で最適化できます。

たぶん、この最適化のより良い例

def power(a, n) {
    n == 0 => 1
    n == 1 => a
    n > 1 => a * power(a, n - 1)
    (test) for any (x) power(a, n) % x == power(a % x, n) % x
}

賢いコンパイラが多くの場合にそれを使用でき、賢いインタプリタ/ジャストインタイムコンパイラがはるかに多くの場合にこれらを使用できることは秘密ではないと思います。

テストと言語をよりよく理解するために、私の考えが役立つかどうか、そうでない場合は何が欠けているのか、もしそうなら、なぜ人気のある言語がそのようなパターンを実装しないのか興味があります。

1
kdani

「十分に高度なコンパイラ」は、プログラミング言語について話すときの一般的なジョークになっています。一部のコンパイラは実際には驚くべき機能を備えていますが、多くの場合、これは、ずさんな言語設計の言い訳として、またはないそのような高度なコンパイラを備えた特定の動的言語のパフォーマンスの言い訳として使用されます。

関数を定義した方法は、パターンマッチングを思い出させます。これは、MLのような関数型言語で最もよく見られる機能です。の線に沿って

_let avg xs =
  match xs with
  | []      => throw (ValueError "array must contain at least one element")
  | x :: [] => x
  | xs      => (fold (+) xs) / (array_length xs)
_

一部の言語のセマンティクスを一連の書き換えルールとして指定できるため、これは興味深いことです。このような関数定義は、_avg [1, 2, 3]_の形式のアプリケーションを書き直す方法を指定します。確かに、ソリューションのこの宣言型の言い回しは、最適化の適用を容易にします。しかし、Cに似た形式を考えると、それははるかに難しいでしょうか?

_Num avg(Num[] xs) {
    if (xs.length == 0) throw new ValueError();
    if (xs.length == 1) return xs[0];
    return sum(xs)/xs.length;
}
_

言語のイディオムを認識しているコンパイラーは、そのような表現を推論するのがこれ以上難しいとは思わないでしょう。


さて、あなたの主な質問は、ユニットテストです。テストの値はこの繰り返しです。理想的には、テストと実装は2人の異なる人によって書かれ、バグが1つの表現から別の表現にコピーアンドペーストされることを回避します。言い換えれば、twoの人々が問題を理解できなかった、または同じタイプミスをした可能性は何ですか? (たとえば、avg([-1, -3]) <= sum([-1, -3])は正しくありません)。

このテストでは、特定のインターフェイスが尊重されることを表明し、代表的な入力をチェックして、実装が期待どおりに機能することを確信します。ここに示されている単純なコードの場合、多くのテストを作成しなくても、明らかに正しい可能性があります。ただし、実装の内部動作を変更したい場合があります。リフレクションやその他の最適化できないものを使用した非常識な実装で、が明らかに正しくない可能性がある場合でも、変更後もコードが期待どおりに機能することを確認したいと思います。したがって、実装を同時にテストすることはできません。また、別の2番目の方法でインターフェイスをエンコードするためにhaveします。これを実行すると、下位互換性を維持するのが非常に簡単になります。ユーザーはあなたに感謝します。

あなたが与えた例では、テストは一行で書くことができます。他の場合では、これは最も確実に当てはまりません。今日、私は100行のテストファイルを開いて、単純な文法を設定しました。これはすべて、レクサーの実装がこの文法と正しくインターフェイスできることを確認する1つのテストのためだけのものです。このような詳細なテストを実装内に配置すると、すべての中断のためにactualコードが非常に読みにくくなります。テストを別のファイルに配置すると、読みやすさの問題がなくなり、選択したテストツールが実装の正確さを「 prove 」にするのが簡単になります。

3
amon