
Tests vs. Types - charlieegan3
http://kevinmahoney.co.uk/articles/tests-vs-types/
======
dllthomas
Some nits to pick, on the Haskell:

    
    
        > No exceptions
        > 
        > Exceptions are also a side effect handled by the type 
        > system. We know our function is pure and won’t throw
        > an exception.
    

This isn't the case for Haskell.

    
    
        > The input can either be true or false. Once you have 
        > tested the result of these two possibilities, you
        > have the holy grail. No exceptions, no infinite
        > loops, no incorrect results, no errors.
    

Almost. The input can also be bottom (a thunk that, when forced, loops
infinitely or raises an exception). In that case, if the function tries to
inspect the input then the function will also be a thunk that, when forced,
loops infinitely or throws an exception. If the function does _not_ inspect
its input, it may still return a value (e.g. `const True`).

~~~
kpmah
Could you expand on your issue with exceptions? In this article I'm using the
distinction between exceptions and errors from
[https://wiki.haskell.org/Error_vs._Exception](https://wiki.haskell.org/Error_vs._Exception)

You're right about the bottom input. I'll add it to the article.

~~~
tazjin
Pure code can (unfortunately) throw exceptions, e.g.:

    
    
      Prelude> head []
      *** Exception: Prelude.head: empty list

~~~
cballard
Seems like a bug, shouldn't that return a Maybe? (the equivalent in Swift
returns an Optional)

~~~
chadaustin
There is a Haskell function called headMay [1] that returns a Maybe, but the
Prelude head function throws when given an empty list.

Either way, pure code can throw exceptions in Haskell. Consider division by
zero for example.

[1]
[https://hackage.haskell.org/package/safe-0.3.9/docs/Safe.htm...](https://hackage.haskell.org/package/safe-0.3.9/docs/Safe.html)

------
agentultra
What this article ignores is that in practice one must read the source code to
understand the function. You can be reasonably certain that the Python
function doesn't harm your grandmother that way. I understand that strong
functional programming is going to catch type errors at compile time and force
constraints on the values in your program but it's quite rare that I've
encountered a _type_ error in a Python program.

What I most often encounter are _logic_ errors. We don't write formal
specifications for safety-critical code often enough. Your program will still
have errors of this kind if you don't think clearly enough before you start
writing code.

There are nice things strong, static typing of the functional variety offers
certainly -- but it's not a hammer that is particularly useful at cutting
boards. There are plenty of cases where a dynamic type system is good enough
or even ideal.

~~~
nickbauman
Here's the thing. You're not going to forgo your tests, right? But testing in
a statically typed language is much harder to get right. But you still need
them. So the question of static or dynamic typing is moot.

~~~
chadaustin
Why is testing harder in a statically typed language? Seems about the same to
me.

~~~
phamilton
Monkey patching is often difficult in a statically types language, but is
quite useful in testing.

~~~
nrinaudo
While I see your point, if you ever find yourself wishing for monkey patching
while writing a test in a statically typed language, it's a code smell. You've
not abstracted things enough. If you need to modify things that way but can't,
you should consider abstracting over it and making it possible at runtime.

That way, not only do you get to write a _better_ test - a whole class of
errors is gone, just not possible anymore - you also have a more modular
system, the components of which you can test individually.

~~~
phamilton
DHH captures this very well
[http://david.heinemeierhansson.com/2012/dependency-
injection...](http://david.heinemeierhansson.com/2012/dependency-injection-is-
not-a-virtue.html)

I think his example of system time is a good one because an abstraction in
that case often only serves to enable testing. If testing is already possible,
then that abstraction is just unneeded complexity

~~~
nrinaudo
Without any sarcasm of any kind, I expect I must be missing the point of what
you linked and would love for you to clarify it.

What I understood was, changing the way time is handled _for your entire Ruby
process_ in order to test that some publish function sets the right date is
_good_ practice. As opposed to, say, have it expect a date argument, which
lets you test it without side effects that can impact absolutely everything
else, and gets you a more powerful feature, publishAt rather than just
publishNow.

If I understood this correctly (which I honestly doubt), I don't think our
conversation serves much purpose. You seem to regard as good practice what I
consider utter lunacy. I don't pretend that I'm right, just that there's
absolutely no chance that we'll agree on this.

That being said, your point about introducing abstraction for the sole purpose
of enabling tests is well made. My experience is that you'll often end up glad
you abstracted over whatever it was you needed for the tests and use it in
live code, but not 100% of the time.

But if the alternative is global mutable states, I'll consider un-needed
abstraction the lesser of two evils and gladly embrace it.

~~~
phamilton
I'm surprised at the shock and awe.

It's a test. It's not production code. It runs and is able to verify behavior.
It runs isolated, and no other code is running when it does.

Building publishAt is more powerful than publishNow, but building something
more powerful than what you need is hardly good practice. And you still end up
with the problem of how you test the code calling publishAt.

This isn't some whack job in the corner saying something he came up with is a
good idea. This is how the Ruby community thinks and writes software, with
great success. And it does in fact clash with other communities.

The point of this discussion isn't really whether one approach is better than
the other. The point is that our tools (specifically dynamic languages with
flexible metaprogramming) drastically influence our view of what is
acceptable. Your strong disagreement demonstrates the point.

------
dllthomas
> With dependent types, the line between tests and types becomes blurrier.

I came to the same realization a while back. In principle, in a sufficiently
expressive system one could encode any unit test as a type - although whether
that could ever be _useful_ is a separate question...

~~~
js8
Exactly. Well, mathematicians do that, they encode everything in the type,
which they call the theorem or lemma (while the program itself is the proof).
But mathematicians, it seems to me, tend to do a lot of proof copy-pasting,
unless they use category theory or something like that.

------
dllthomas
I understand the C bit to be mostly an aside, and I wouldn't really say
anything there is incorrect, but I think it's worth noting here:

In C, `typedef` only "defines a type" as far as the _parser_ is concerned.
Beyond that it is treated exactly the same as if you'd spelled it out. A
`typedef` can be useful for brevity, for documentation, and to DRY up usage of
the type, but it _does not get you nominal typing!_

That said, you _can_ get nominal typing (for a fairly low, but present,
syntactic cost) by wrapping primitives in a single-element struct. This keeps
the types distinct through the type checker, but is typically stripped away
long before code generation so there shouldn't be any run-time cost for the
additional safety.

As I've practiced, it looks like this:

    
    
        typedef struct price { int64_t v; } price;
        typedef struct bid { price v; } bid;
        typedef struct ask { price v; } ask;
    
        price price_difference(price p1, price p2) {
            price r = { p1.v - p2.v }
            return r;
        }
    
        price compute_spread(bid b, ask a) {
            return price_difference(a.v, b.v);
        }
    

Examples, of course, overly simplified for the sake of being examples. If all
the .v's are getting messy, I don't feel too bad about unwrapping locally -
but passing primitives between functions has become a bit of a smell.

------
pron
> If you’re writing software for Boeing or for an iron lung, you may want to
> consider writing proofs.

If you're writing software for Boeing for an iron lung, you may already be
using one of a few fully verifiable languages (like SCADE, Esterel) that don't
even require proofs -- just a specification -- and have been in common use in
safety-critical systems since the eighties. Dependent types are interesting,
but they're not widely used simply because there are much more efficient ways
of proving safety-critical systems 100% correct.

> using a modern type system will give you the most information and guarantees
> for the least amount of effort

I can agree with that one, but I don't agree with the implication that
_Haskell 's_ type system (or ML's) is the right one for this purpose of giving
the most information for the least effort.

------
MichaelBurge
I find that QuickCheck-style tests that randomly choose inputs can have value
approaching that of a proof. Code that's even heavily tested with test cases
tends to have minor bugs and inconsistencies that need resolving before a
proof will work. Code that passes a test with random inputs generally goes
through as-is(or with minor restructuring to make the proof easier).

------
fiatjaf
This is just Idris propaganda.

~~~
js8
Maybe that's where the things in programming are going?

I think eventually we (programmers) end up with some hybrid system, where lot
of stuff that's normally done by hand (refactoring, data transformations,
dependency injection, data structure choice, optimization for speed and memory
usage, parallelization, deduplication, unit testing) will be done by some
theorem prover, working alongside the programmer.

~~~
incepted
> Maybe that's where the things in programming are going?

Doubtful. Idris is probably the most visible current effort in that direction
and even it is extremely obscure and barely known beyond the Haskell
afficionado circles.

And for good reasons: even today, Idris can barely express any program more
complicated than the simplest mathematical formulas. Developers need languages
that help them express network requests and database queries safely and
efficiently, and Idris (and dependent languages in general) are not remotely
close to being able to express that.

------
tumdum_
unsafePerformIO invalidates any guarantees in Haskell :(

~~~
dllthomas
Less than you might think, in practice. I see unsafePerformIO used three
places.

First, debugging. While there are sometimes better approaches, this is
unobjectionable.

Second, I'll sometimes use it in throw-away scripts. Here I am knowingly
losing some guarantees, but in a sufficiently constrained portion of the
system that it's rarely a problem.

Third, where it's known to be safe and necessary. This one is the most
worrisome, but the community is pretty at expecting uses to 1) be _very_ rare,
and 2) have both safety and necessity justified.

Really, the fact that all values are inhabited by bottom is a bigger issue.
Still, IME, Haskell provides guarantees that are sufficiently reliable to be
tremendously useful.

~~~
15155
> where it's known to be safe and necessary

To give a valid, non-objectionable example: FFI that is known to be pure will
often be wrapped with unsafePerformIO to present a clean interface to Haskell
code.

~~~
chadaustin
Indeed. Consider [https://github.com/chadaustin/buffer-
builder/blob/master/src...](https://github.com/chadaustin/buffer-
builder/blob/master/src/Data/BufferBuilder.hs#L164)

The API is pure, but it's implemented with a high-performance effectful monad,
and none of the implementation details are exported. A perfect use of
unsafe[Dupable]PerformIO.

