One aspect of unit tests is to have the specs in runnable form.
However when employing a declarative style, that directly maps the
formalized specs to language semantics, is it even actually possible
to express the specs in runnable form in a separate way, that adds
value?
If you have specs which can be mapped directly to function declarations - fine. But typically those are two completely different levels of abstractions. Unit tests are intended to test single pieces of code, written as white box tests by the same developer who is working on the function. Specs normally look like "when I enter this value here and press on this button, this and that should happen". Typically such a spec leads to far more than one function to be developed and tested.
However a simple mistake like using x
instead of y (both being coordinates) in your code cannot be covered.
However such a mistake could also arise while writing the test code,
so I am not sure whether it is worth the effort.
Your misconception is here that unit tests are actually for finding bugs in your code first hand - that's not true, at least, it is only partially true. They are made to prevent you from introducing bugs later when your code evolves. So when you first had your function tested and your unit test working (with "x" and "y" properly in place), and then while refactoring you use x instead of y, then the unit test will show that up to you.
Unit tests do introduce redundancy, which means that when requirements
change, the code implementing them and the tests covering this code
must both be changed. This overhead of course is about constant, so
one could argue, that it doesn't really matter. In fact, in languages
like Ruby it really doesn't compared to the benefits, but given how
statically typed functional programming covers a lot of the ground
unit tests are intended for, it feels like it's a constant overhead
one can simply reduce without penalty.
In engineering, most safety systems rely on redundancy. For example, two breaks in a car, a redundant parachute for a sky diver etc. The same idea applies to unit tests. Of course, having more code to change when the requirements change can be a disadvantage. So especially in unit tests it is important to keep them DRY (follow the "Dont Repeat Yourself" principle). In a statically typed language, you may have to write some less unit tests than in a weakly typed language. Especially "formal" tests may be not necessary - which is a good thing, since it gives you more time to work on the important unit tests which test substantial things. And don't think just because you static types, you don't need unit tests, there is still plenty of room to introduce bugs while refactoring.
There are several terms for the concept. Deconstructing is what I believe is common in Haskell circles, it is what Real World Haskell calls it. I believe the term destructuring(or destructuring bind) is common in Lisp circles.
Best Answer
All these terms - expressions, values, and "class" - are general PL concepts that have no specific ties to Haskell, and are best understood under a more general framework. To keep things brief, I will only describe these ideas informally, although it is important to realize they can all be rigorously defined within a formal logical framework.
Expressions
Expressions are the basic units of programming; in some sense, programs are expressions. Here are some examples of expressions (in a small made-up language):
1 + 3 * 3
concat("hello", "world")
let x = pow(2, 2) in pow(x, x)
lambda x. x
Notice that
lambda x. x
(the identity function) is an expression in this language. It can be used interchangeably in any context in which an expression is expected; for example, instead of1 + 1
we can write1 + lambda x. x
*. In particular, since the arguments to a function are expressions, and functions themselves are expressions, we may pass functions to functions as arguments, such asmap(lambdax. x, [1, 2, 3])
.Thus, higher-order functions are but a consequence of treating functions as expressions. In contrast, in a language that does not do so, like C, such an expression is not even a program in that language.
* This is valid according to the abstract syntax of the language, but the code will not type-check. More on this later.
Dynamics and Values
Expressions are static. It is the job of the dynamics of a language to tell us how expressions are to be evaluated during run-time. The (operational) dynamics consists of a set of simple transition rules for transforming one form of expression into another. For example, our dynamics may have a rule that, informally, says "
n1 + 0
transitions ton1
".The values in a language are a subset of expressions that we consider to be fully evaluated; we write programs (expressions) to compute values. The expressions given above evaluate to:
7
"hello world"
256
lambda x. x
Tangent: It should be the case that a value cannot transition to another expression, but the converse does not generally hold; there are some expressions (e.g.
7 + "hello world"
) that cannot be evaluated further, yet are not values. The purpose of a type system is to avoid such situations.Thus, to declare that "functions are values" we must a priori insist that functions be expressions. In our language, we do consider functions to be values; thus,
lambda x. x
is a value, andmap(lambda x. x, [1 2 3])
is a valid expression.As far as I know, it would be useless to create a language in which functions are expressions but not values.