4 ms·
> Please note I didn't throw out add(3,4)==7, but instead pointed out it's terribly inadequate as a test. Additional testing tools must be employed; unit testin
by owensd 12y ago
> Please note I didn't throw out add(3,4)==7, but instead pointed out it's terribly inadequate as a test. Additional testing tools must be employed; unit testing alone of this kind is not enough.
I don't follow what you are saying here at all... It's an API test with no external integration points, what other kind of tests besides unit tests would you have? Your `add(x, y) = add(y, x)` are still unit tests.
Also, you said that you would "rule out testing specific values such as 3+4=7". You have to test specific values; the contract of the function is:
f(x, y) = z
Where z is the mathematical sum of x and y.
Specific value testing is the only way you can verify that claim for a given set of inputs.
>> The only thing that static typing provides, in regards to test cases, is this [example]
> This assertion is wrong. Static typing done well provides a lot of things "for free", such as restricting incorrect behavior.
This was taken out of context; this was in reference your to "generator" of test values for X and Y. The type signature alone is inadequate to generate test values.
Regardless,
f :: [a] -> a
Is no harder to test in a dynamic language.
let r = f([...])
assert(r, correct_value)
assert(r.type, correct_type)
It's up to the contract of the function to determine what, if any, validation needs to be done on the input. This is true regardless of type system. The only question is this: do you also check the type.
Again, type is only _one_ of the constraints that get applied to parameters. In the add function example, the other constraints are:
1. x <= TYPE_MAX (for free in a statically typed system)
2. x <= TYPE_MAX - y
3. y <= TYPE_MAX (for free in a statically typed system)
4. y <= TYPE_MAX - x
Plus the similar for TYPE_MIN. In the `addbase2` example, additional constraints are:
1. x power of 2
2. y power of 2
In this example, we still need to add verification for two-thirds of the constraints.
On the flip-side, with generics, especially with the type of generics we see in Swift (using the `f :: [a] -> a` example), you'll probably need to model constraints of the collection, the element type of the collection, and the type of indexer that is being used if you wish to make your function actually work.
And then your implementation only works for those that rigidly adhere to the type conformance, where as the dynamic one can work for any type that conforms to the protocol, whether loosely or explicitly.
This flexibility is very powerful, is not hard to code safely around, and requires significant less code gymnastics before you can even get your code compiling.
- the_af 12y agoI think you are underestimating the power of statically typed generics when using a language with a decent type system. In your example: let r = f([...]) assert(r, correct_value) assert(r.type, correct_type) This doesn't test everything we need to know. For example, the following function passes your asserts (in pseudocode): f(a_list): if (a_list instanceof List[Int]): return 0 else ... other stuff ... whereas the original, statically typed version of the function with signature f :: [a] -> a cannot ever do that. This is a profound insight. It cannot return zero "in the case of a list of ints". It doesn't know anything about its input if you don't tell it. And you shouldn't tell it, either, unless you have a very specific reason to do so. Also, in programming language with decent static typing (that is, not Java or C++; I wouldn't know about Swift to comment), there is a huge additional difference between the two functions: I can promise you my function doesn't write to disk, doesn't output to the screen, etc. You cannot promise the same with your function. Your function might work when it has access to the disk, as in your test environment, but fail on production where it does not. Ok, so you inspect the code to make sure your function (or any function it calls) don't do I/O. But I don't have to do this, because the type system tells me my function is side-effect free. So now you have some pretty powerful assurances in favor of my statically typed function: - It doesn't perform side effects. I don't know about the dynamic function. - It doesn't produce any value out of thin air; it must work with the list I passed it, because it doesn't know anything else. It doesn't know how to create new values. - As a consequence of the above, there are fewer possible implementations of my function than of yours, excluding no-ops. This is a kind of testing "for free" that you don't have with dynamically typed languages. And it is pretty powerful. Yes, you can cover a lot of cases with unit tests in a dynamic language, but why not let the computer do the boring work for you? It's what computers are there for. Focus on the interesting test cases instead.
- owensd 12y ago> I can promise you my function doesn't write to disk, doesn't output to the screen, etc. You cannot promise the same with your function. WHAT?! Your type signatures have absolutely no assurances with regards to side effects. They cannot even make a claim that the function is thread safe, let alone that it doesn't write to disk our output to the screen. I'm baffled at why you think that is true: int foo(int bar): // network call here // write a log to disk here // change a global value here return happy_int And you are woefully mistaken about about this claim as well: "it must work with the list I passed it, because it doesn't know anything else." Many languages that actually have good generic type systems allow for type specialization. That means that I can provide different implementations for different types. So in the contrived example of you doing something completely different with my list of ints in the dynamic version is completely possible in many statically typed languages too.