Python from Wise Guy's Viewpoint

L

Lex Spoon

It sounds unbelievable, but it really works.
This kind of claim comes is usually just a misunderstanding.
For example, the above claim indeed holds for HM typing - for the
right definitions of "never wrong" and "never has an error".

HM typing "is never wrong and never has a run-time error" in the
following sense: the algorithm will never allow an ill-typed program
to pass, and there will never be a type error at run-time. However,
people tend to overlook the "type" bit in the "type error" term, at
which point the discussion quickly degenerates into discourses of
general correctness.


It is misleading to make this claim without a lot of qualification.
It requires careful, technical definitions of "type" and "type error"
that are different from what an unprepared audience will expect.


For example, it is not considered a type error if you get the wrong
branch of a datatype. So if you define:

datatype sexp = Atom of string | Cons of (sexp, sexp)

fun car (Cons (a,b)) = a

then the following would not be considered a "type error" :

car (Atom "hello")



To add to the situation, HM flags extra errors, too, that many people
would not consider "type errors" but which are for HM's purposes. For
example, it is considered a type error if two branches of an "if" do
not match, even if one branch is impossible or if the later code can
remember which branch was followed. For example:

val myint : int =
if true
then 0
else "hello"


or more interestingly:

val (tag, thingie) =
if (whatever)
then (0, 1)
else (1, 1.0)

val myotherstuff =
if tag = 0
then (tofloat thingie) + 1.5
else thingie + 1.5


In common parlance, as opposed to the formal definitions of "type
error", HM both overlooks some type errors and adds some others. It
is extremely misleading to claim, in a non-technical discussion, that
HM rejects precisely those programs that have a type error. The
statement is actually false if you use the expected meanings of "type
error" and "type".


All this said, I agree that HM type inference is a beautiful thing and
that it has significant benefits. But the benefit of removing type
errors is a red herring--both in practice and, as described in this
post, in theory as well.

-Lex
 
D

Dirk Thierbach

Lex Spoon said:
To add to the situation, HM flags extra errors, too, that many people
would not consider "type errors" but which are for HM's purposes. For
example, it is considered a type error if two branches of an "if" do
not match, even if one branch is impossible or if the later code can
remember which branch was followed. [...]
val (tag, thingie) =
if (whatever)
then (0, 1)
else (1, 1.0)

val myotherstuff =
if tag = 0
then (tofloat thingie) + 1.5
else thingie + 1.5

The point here is of course that you "glue together" the tag
and the value, with the additional side effect that this documents
your intention. So you would write in this case

data Thingie = Tag0 Integer | Tag1 Float

and then you can write

myfirststuff whatever = if whatever then Tag0 1 else Tag1 1.0

myotherstuff (Tag0 thingie) = (fromInteger thingie) + 1.5
myotherstuff (Tag1 thingie) = thingie + 1.5

Then the type checker will happily infer that

myfirststuff :: Bool -> Thingie and
myotherstuff :: Thingie -> Float

So you indeed have to express your tags a bit differently. Is this
asking too much? Is that so inconvient, especially when you get a
good documention of your intentions for free?

- Dirk
 
J

Joachim Durchholz

Neel Krishnaswami had a wonderful explanation in article
<[email protected]>

Sorry, that link doesn't work for me, I don't know the proper syntax for
links, and I couldn't type one in even if I knew it.

Anybody got a full reference? This thread is too long for searching...

Regards,
Jo
 
F

Fergus Henderson

Pascal Costanza said:
- The important thing here is that the EMLOYED mixin works on any class,
even one that is added later on to a running program. So even if you
want to hire martians some time in the future you can still do this.

What happens if the existing class defines a slot named "salary" or "company",
but with a different meaning? Are slot names global, or is there some sort
of namespace control to prevent this kind of accidental name capture?

Anyway, regarding how to write this example in a statically typed
language: you can do this in a quite straight-forward manner,
by just keeping a separate table of employees.
For example, here it is in Java.

import java.util.*;
public class Employed {
static String default_company = "constanz-inc";

static class Employee {
public Object obj;
public String company;
public int salary;
public Employee(Object o, String c, int s) {
company = c;
salary = s;
obj = o;
}
}

static Hashtable employees = new Hashtable();

static void hire(Object obj, int salary) {
hire(obj, salary, default_company);
}
static void hire(Object obj, int salary, String company) {
employees.put(obj, new Employee(obj, company, salary));
}
static void fire(Object obj) {
employees.remove(obj);
}

static void test_employed() {
class Person {
public String name;
Person(String n) { name = n; }
};
Person joe = new Person("joe");
System.out.println("-> hire joe");
hire(joe, 60000);
System.out.println("name: " + joe.name);
System.out.println("class: "
+ joe.getClass().getName());
Employee e = (Employee) employees.get(joe);
System.out.println("employed: " +
(e != null ? "yes" : "no"));
System.out.println("company: " + e.company);
System.out.println("salary: " + e.salary);
System.out.println("-> fire joe");
fire(joe);
if (employees.contains(joe)) {
System.out.println("joe is still employed.");
} else {
System.out.println(
"joe is not employed anymore.");
}
}

public static void main(String args[]) {
test_employed();
}
}

As you can see, there's no need here for dynamically changing the types of
objects at runtime or for creating classes at runtime. But you can employ
Martians or any other object.

This example makes use of one dynamic cast; that's because the Java
type system doesn't support generics / parametric polymorphism. It would
be a little nicer to do this in a language which supported generics, then
we could use `Hashtable<Object, Employee>' rather than just `Hashtable',
and there wouldn't be any need for the dynamic cast to `(Employee)'.
 
D

Dirk Thierbach

Neel Krishnaswami had a wonderful explanation in article
<[email protected]>

And note that his example for practical relevance includes a datatype.

Haskell doesn't support recursive types directly, but a recursive
datatype for lists is easy:

data List a = Nil | Cons a (List a)

Since Haskell is lazy, that's already a lazy list, but even if Haskell
was eager, you could write a similar datatype for lazy lists.
(And of course Haskell lists are essentially just given by the above
datatype, together with some syntactic sugar).

It's very natural for a relevant application of recursive types to
be coupled with data.

- Dirk
 
P

Pascal Costanza

Fergus said:
What happens if the existing class defines a slot named "salary" or "company",
but with a different meaning? Are slot names global, or is there some sort
of namespace control to prevent this kind of accidental name capture?

Sure, that's what Common Lisp's packages are there for. Define the
EMPLOYED mixin in its own package - done! You won't ever need to worry
about name clashes.
Anyway, regarding how to write this example in a statically typed
language: you can do this in a quite straight-forward manner,
by just keeping a separate table of employees.
Yuck.

static void test_employed() {
class Person {
public String name;
Person(String n) { name = n; }
};
Person joe = new Person("joe");
System.out.println("-> hire joe");
hire(joe, 60000);
System.out.println("name: " + joe.name);
System.out.println("class: "
+ joe.getClass().getName());
Employee e = (Employee) employees.get(joe);
^^^^^^^^^^^^^^^^^^
This part is not domain-specific, but shows that your abstraction leaks.
I.e. the client of your interface has to remember how the employee
abstraction is implemented in order to use it correctly.

In my original example, I was able to just call (company joe) and
(salary joe) (or, in Java syntax, this would be joe.salary and
joe.company). I.e., I don't have to know anything about the internal
implementation.

You can't implement unanticipated optional features in a statically
typed language that doesn't involve leaking abstractions.
As you can see, there's no need here for dynamically changing the types of
objects at runtime or for creating classes at runtime. But you can employ
Martians or any other object.

Sure. You also don't need functions and parameter passing. You also
don't need GOSUB and RETURN. So why don't we just program in assembler
again?
This example makes use of one dynamic cast; that's because the Java
type system doesn't support generics / parametric polymorphism. It would
be a little nicer to do this in a language which supported generics, then
we could use `Hashtable<Object, Employee>' rather than just `Hashtable',
and there wouldn't be any need for the dynamic cast to `(Employee)'.

I would still need to remember what features happen to be kept external
from my objects and what not.


Pascal

P.S.: Your implementation of default_company doesn't match mine. Yours
is not dynamically scoped. But maybe that's nitpicking...
 
F

Fergus Henderson

Fergus Henderson said:
Anyway, regarding how to write this example in a statically typed
language: you can do this in a quite straight-forward manner,
by just keeping a separate table of employees.
For example, here it is in Java.

And in case you didn't like the type declarations and downcast that you
need in Java, here it is in Mercury.

:- module employed.
:- interface.
:- import_module io.

:- pred main(io::di, io::uo) is det.

:- implementation.
:- import_module map, string, std_util.

default_company = "constanz-inc".

:- type employee ---> some [Obj]
employee(object::Obj, salary::int, company::string).

hire(Obj, Salary, !Employees) :-
hire(Obj, Salary, default_company, !Employees).
hire(Obj, Salary, Company, !Employees) :-
set(!.Employees, Obj, 'new employee'(Obj, Salary, Company),
!:Employees).
fire(Obj, !Employees) :-
delete(!.Employees, Obj, !:Employees).

:- type person ---> person(name::string).

test_employed(!.Employees) -->
{ Joe = person("joe") },
print("-> hire joe"), nl,
{ hire(Joe, 60000, !Employees) },
print("name: " ++ Joe^name), nl,
print("class: " ++ type_name(type_of(Joe))), nl,
print("employed: " ++ (if !.Employees `contains` Joe
then "yes" else "no")), nl,
print("company: " ++
!.Employees^det_elem(Joe)^company), nl,
print("salary: "),
print(!.Employees^det_elem(Joe)^salary), nl,
print("-> fire joe"), nl,
{ fire(Joe, !Employees) },
(if {!.Employees `contains` Joe} then
print("joe is still employed."), nl
else
print("joe is not employed anymore."), nl
).

main --> { init(Employees) }, test_employed(Employees).
 
P

Pascal Costanza

Fergus said:
And in case you didn't like the type declarations and downcast that you
need in Java, here it is in Mercury.

Thanks for the variations on this theme. However, your abstraction is
still leaking.
print("employed: " ++ (if !.Employees `contains` Joe
^^^^^^^^^^^^^^^^^^^^^^
then "yes" else "no")), nl,
print("company: " ++
!.Employees^det_elem(Joe)^company), nl,
^^^^^^^^^^^^^^^^^^^^^^^^^
print("salary: "),
print(!.Employees^det_elem(Joe)^salary), nl,
^^^^^^^^^^^^^^^^^^^^^^^^^

Pascal
 
F

Fergus Henderson

Pascal Costanza said:
^^^^^^^^^^^^^^^^^^
This part is not domain-specific, but shows that your abstraction leaks.
I.e. the client of your interface has to remember how the employee
abstraction is implemented in order to use it correctly.

In my original example, I was able to just call (company joe) and
(salary joe) (or, in Java syntax, this would be joe.salary and
joe.company). I.e., I don't have to know anything about the internal
implementation.

Well, you have a point, I didn't encapsulate the use of the Hashtable.
I can do that quite easily:

static Employee employee(Object obj) {
return (Employee) employees.get(obj);
}

Then the code there could become as follows.

System.out.println("employed: " +
(employee(joe) != null ? "yes" : "no"));
System.out.println("company: " + employee(joe).company);
System.out.println("salary: " + employee(joe).salary);

If you prefer, you can make make it simpler still,

System.out.println("employed: " +
(employeed(joe) ? "yes" : "no"));
System.out.println("company: " + company(joe));
System.out.println("salary: " + salary(joe));

by defining suitable methods:

static bool employed(Object obj) {
return employee(obj) != null;
}
static String company(Object obj) {
return employee(obj).company;
}
static int salary(Object obj) {
return employee(obj).salary;
}

Now, my guess is that you're still going to be complaining about the
abstraction leaking, but I don't think such complaints are valid.
Yes, the syntax is different than a field access, but that's not
important. The client is going to need to know the names of the
attributes or methods they want to use anyway; as long as they need to
know whether the name of the entity is "wage" or "salary", it doesn't make
a significant difference that they also need to know whether the interface
to that entity is a field, a member function, or a static function.

And of course, this syntax issue is language-specific. In Mercury
and Haskell, the same syntax is used for field access, method call,
and function call, so the issue doesn't arise!
You can't implement unanticipated optional features in a statically
typed language that doesn't involve leaking abstractions.

Not true. See above.
I would still need to remember what features happen to be kept external
from my objects and what not.

No, that's not what you need to remember. You just need to remember the
names of the features, and for each feature what kind of feature it is:
whether it is a field, an instance method, or a static method.
If it is a field, you know it is kept inside the object, but in
the other two cases the implementation is encapsulated -- the user
can't tell where the data is stored.
 
C

CezaryB

Just check the archives for comp.lang.ada and Ariane-5.

Short version: The software performed correctly, to
specification (including the failure mode) -- ON THE ARIANE 4 FOR
WHICH IT WAS DESIGNED.


Nonsense. From: http://www.sp.ph.ic.ac.uk/Cluster/report.html

"The internal SRI software exception was caused during execution of a
data conversion from 64-bit floating point to 16-bit signed integer [...]

LISP wouldn't have helped -- since the A-4 code was supposed
to failure with values that large... And would have done the same
thing if plugged in the A-5. (Or are you proposing that the A-4 code
is supposed to ignore a performance requirement?)

"supposed to" fail? chya. This was nothing more than an unhandled
exception crashing the sytem and its identical backup. Other conversions
were protected so they could handle things intelligently, this bad boy
went unguarded. Note also that the code functionality was pre-ignition
only, so there is no way they were thinking that a cool way to abort the
flight would be to leave a program exception unhandled.

What happened (aside from an unnecessary chunk of code running
increasing risk to no good end) is that the extra power of the A5 caused
oscillations greater than those seen in the A4. Those greater
oscillations took the 64-bit float beyond what would fit in the 16-bit
int. kablam. Operand Error. This is not a system saying "whoa, out of
range, abort".


"To determine the vulnerability of unprotected code, an analysis was performed
on every operation which could give rise to an exception, including an Operand
Error. [...] It is important to note that the decision to protect certain
variables but not others was taken jointly by project partners at several
contractual levels."

"There is no evidence that any trajectory data were used to analyse the
behaviour of the unprotected variables, and it is even more important to note
that it was jointly agreed not to include the Ariane 5 trajectory data in the
SRI requirements and specification."


"It was the decision to cease the processor operation which finally proved
fatal. Restart is not feasible since attitude is too difficult to re-calculate
after a processor shutdown; therefore the Inertial Reference System becomes
useless. The reason behind this drastic action lies in the culture within the
Ariane programme of only addressing random hardware failures. From this point of
view exception - or error - handling mechanisms are designed for a random
hardware failure which can quite rationally be handled by a backup system."


As for Lisp not helping:

"It has been stated to the Board that not all the conversions were protected
because a maximum workload target of 80% had been set for the SRI computer"



CB
 
L

Lex Spoon

Fergus Henderson said:
Yes, we covered that already. But that's not what is happening in
the scenario that I was describing. The scenario that I'm describing is

Collection c;

...
foreach x in c do
use(x);

where use(x) might be a method call, a field access, or similar.
For example, perhaps the collection is a set of integers, and you
are computing their sum, so use(x) would be "sum += x".

I see. "sum += x" would indeed tend to cause a lot of checks, but
then again the checks might well end up costing 0 overall CPU cycles.
The general technique of optimizing methods for common types, plus the
likelihood that a CPU will have multiple functional units, can make a
big difference.

Also, keep in mind that if this is a performance critical blotch of
code, then the programmer has the option of making "c" be a
specialized array or matrix type.


-Lex
 
F

Fergus Henderson

Lex Spoon said:
I see. "sum += x" would indeed tend to cause a lot of checks, but
then again the checks might well end up costing 0 overall CPU cycles.

Occaisionally, perhaps. But I think that would be rare.
The general technique of optimizing methods for common types, plus the
likelihood that a CPU will have multiple functional units, can make a
big difference.

Even with an unlimited number of functional units, there's still the
extra CPU cycles to wait for instruction cache misses or page faults
caused by the greater code size.
 
F

Fergus Henderson

Pascal Costanza said:
OK, let's try to distill this to some simple questions.

Assume you have a compiler ML->CL that translates an arbitrary ML
program with a main function into Common Lisp. The main function is a
distinguished function that starts the program (similar to main in C).
The result is a Common Lisp program that behaves exactly like its ML
counterpart, including the fact that it doesn't throw any type errors at
runtime.

Assume furthermore that ML->CL retains the explicit type annotations in
the result of the translation in the form of comments, so that another
compiler CL->ML can fully reconstruct the original ML program without
manual help.

Now we can modify the result of ML->CL for any ML program as follows. We
add a new function that is defined as follows:

(defun new-main ()
(loop (print (eval (read)))))

(We assume that NEW-MAIN is a name that isn't defined in the rest of the
original program. Otherwise, it's easy to automatically generate a
different unique name.)

Note that we haven't written an interpreter/compiler by ourselves here,
we just use what the language offers by default.

Furthermore, we add the following to the program: We write a function
RUN (again a unique name) that spawns two threads. The first thread
starts the original main function, the second thread opens a console
window and starts NEW-MAIN.

Now, RUN is a function that executes the original ML program (as
translated by ML->CL, with the same semantics, including the fact that
it doesn't throw any runtime type errors in its form as generated by
ML->CL), but furthermore executes a read-eval-print-loop that allows
modification of the internals of that original program in arbitrary
ways. For example, the console allows you to use DEFUN to redefine an
arbitrary function of the original program that runs in the first
thread, so that the original definition is not visible anymore and all
calls to the original definiton within the first thread use the new
definition after the redefinition is completed. [1]

Now here come the questions.

Is it possible to modify CL->ML in a way that any program originally
written in ML, translated with ML->CL, and then modified as sketched
above (including NEW-MAIN and RUN) can be translated back to ML?

Yes, it is possible. For example, have CL->ML ignore the type annotation
comments, and translate CL values into a single ML discriminated union type
that can represent all CL values. Or (better, but not quite the same semantics)
translate those CL values with ML type annotations back to corresponding ML
types, and insert conversions to convert between the generic CL type and
other specific types where appropriate.

Suppose the original ML program defines the following functions

foo : int -> int
bar : string -> string
...

We can add dynamic typing like this:

datatype Generic = Int of int
| String of string
| Atom of string
| Cons Generic Generic
| Apply Generic Generic
| ...

fun foo_wrapper (Int x) = (Int (foo x))

fun bar_wrapper (String x) (String (foo x))

and then dynamic binding like this

val foo_binding = ref foo_wrapper

val bar_binding = ref bar_wrapper

For dynamic binding, function calls will have to be translated to does
an indirection. So a call

let y = foo x

will become

let y = !foo_binding x

or, if x and y are used in a way that requires that they have type int,
then

let (Int y) = !foo_binding (Int x)

We can then simulate eval using an explicit symbol table.

fun lookup "foo" = !foo_binding
| lookup "bar" = !bar_binding
| lookup "define" = !define_binding
...

fun define "foo" f = foo_binding := f
| define "bar" f = bar_binding := f
| define "define" f = define_binding := f
...

fun define_wrapper (Cons (Atom name) body) =
let () = define name body in Atom "()"
val define_binding = ref define_wrapper

fun eval (Apply func arg) =
case (eval func) of
Atom funcname => (lookup funcname) (eval arg)
| eval x = x

Note that our symbol table includes an entry for the function "define",
so that eval can be used to modify the dynamic bindings.

The rest (e.g. read) is straight-forward.
To ask the question in more detail:

a) Is it possible to write CL->ML in a way that the result is both still
statically type checkable

Yes, but only in a weak sense. Since we are allowing dynamic binding,
and we want to be able to dynamically bind symbols to a different type,
we're definitely going to have the possibility of dynamic type errors.
The way this is resolved is that things which would have been type errors
in the original ML program may become data errors (invalid constructor
in an algebraic type Generic) in the final ML program.
and not considerably larger than the original program that was given to
ML->CL.

There's a little bit of overhead for the wrapper functions of type
"Generic -> Generic", and for the variables of type "ref (Generic -> Generic)"
which store the dynamic bindings. It's about two lines of code per
function in the original program. I don't think this is excessive.
Especially, is it possible to do this
without implementing a new interpreter/compiler on top of ML and then
letting the program run in that language, but keep the program
executable in ML itself?

The program includes a definition for "eval", and "eval" is an
interpreter, So in that sense, we have added a new interpreter.
But the bulk of the program is written in ML, without making use of eval.
c) If you respond with yes to either a or b, what does your sketch of an
informal proof in your head look like that convincingly shows that this
can actually work?

See above.
 
P

Pascal Costanza

Fergus said:
Suppose the original ML program defines the following functions

foo : int -> int
bar : string -> string
...

We can add dynamic typing like this:

datatype Generic = Int of int
| String of string
| Atom of string
| Cons Generic Generic
| Apply Generic Generic
| ...
^^^
How many do you need of those, especially when you want to allow that
type to be extended in the running program?
Note that our symbol table includes an entry for the function "define",
so that eval can be used to modify the dynamic bindings.

DEFUN is just one example. What about DEFTYPE, DEFCLASS, DEFPACKAGE, and
so forth...
The program includes a definition for "eval", and "eval" is an
interpreter, So in that sense, we have added a new interpreter.

That's the whole point of my argument.


Pascal
 
F

Fergus Henderson

Pascal Costanza said:
That's the whole point of my argument.

Then it's a pretty silly argument.

You ask if we can implement eval, which is an interpreter,
without including an interpreter?
I don't see what you could usefully conclude from the answer.
 
F

Fergus Henderson

Pascal Costanza said:
^^^
How many do you need of those, especially when you want to allow that
type to be extended in the running program?

In Haskell you would just use "Dynamic" instead of the above, and be done
with it. In Mercury, you'd just use "univ".

In SML, I don't know. Probably none. In fact, the first two entries in
the type above are not really necessary, you could just represent ints
and strings as Atoms. Even Apply could be represented using just Cons
and Atom: instead of Apply x y, we could use Cons (Atom "apply") (Cons x y).
DEFUN is just one example. What about DEFTYPE, DEFCLASS, DEFPACKAGE, and
so forth...

Well, DEFTYPE in lisp really just defines a function, doesn't it?
So that's not much different than DEFUN. Likewise, DEFCLASS just
defines a collection of functions, doesn't it? OK, maybe these
also record some extra information in some global symbol tables.
That is easily emulated if you really want.

I don't know exactly what DEFPACKAGE does, but if the others are any
guide, it's probably not too hard either.
 
P

Pascal Costanza

Fergus said:
Then it's a pretty silly argument.

You ask if we can implement eval, which is an interpreter,
without including an interpreter?
Right.

I don't see what you could usefully conclude from the answer.

....that you can't statically type check your code as soon as you
incorporate an interpreter/compiler into your program that can interact
with and change your program at runtime in arbitrary ways.


Pascal
 
P

Pascal Costanza

Fergus said:
Well, DEFTYPE in lisp really just defines a function, doesn't it?
So that's not much different than DEFUN. Likewise, DEFCLASS just
defines a collection of functions, doesn't it? OK, maybe these
also record some extra information in some global symbol tables.
That is easily emulated if you really want.

I don't know exactly what DEFPACKAGE does, but if the others are any
guide, it's probably not too hard either.

I am not sure if I understand you correctly, but are you actually
suggesting that it is better to reimplement Common Lisp on your own than
to just use one of the various Common Lisp implementations?

All I am trying to get across is that, in case you need the flexibility
a dynamic language provides by default, and only occasionally need to
restrict that flexibility, it's better to use a dynamic language from
the outset. The price to pay is that you cannot make use of a 100%
strict static type system anymore, but on the other hand you can pick
one of the stable Common Lisp implementations with language features
that have proven to work well during the past few decades.

Sure, if you don't need the flexibility of a dynamic language then you
can think about using a static language that might buy you some
advantages wrt to static checkability. But I highly doubt that this is a
rational choice because I don't know any empirical studies that show
that the problems that static languages intend to solve are in fact
problems that occur in practice.

Let's inspect the list of those problems again:

a) performance

Good Lisp/Scheme implementations don't have problems in this regard.

b) documentation

Documentation can be handled well with comments and well-chosen names.

c) absence of a certain class of bugs

It's not clear whether this class of bugs really occurs in practice.
There are also indications that these relatively trivial bugs are also
covered by test suites as soon as they consist of a reasonable number of
test cases.

d) unbreakable abstraction boundaries

Such boundaries seem to be as tedious to implement in dynamic languages,
as is the case for dynamic features in static languages.

My conclusions would be as follows:

a) and b) are relatively uninteresting. c) needs convincing studies from
the proponents of static type systems. As long as they don't provide
them, it's just guesswork that these bugs are really important.

d) is the hard part. Proponents of static languages say that it's
important to be able to express such boundaries because it increases
their expressive power. (That's one thing I have learned from this
discussion: It is arguable that this can also be an important kind of
expressive power.) Proponents of dynamic languages say that it's more
important to be able to work around restrictions, no matter whether they
are intentional or not; it's better not to be able to paint yourself
into a corner.

With regard to d, both "groups" don't have enough objective empirical
evidence beyond their own subjective experiences. Static programmers
don't have enough objective empirical evidence that their languages
objectively increase the quality of their software, and dynamic
programmers don't have enough objective empirical evidence that painting
oneself into corners is a problem that occurs regularly.

So both views essentially boil down to be no more than subjective belief
systems. And IMHO that's not so far from the "truth": I am convinced
that these issues depend mostly on personal programming style and
preferences and not so much on technological issues. Software quality is
a social construct, and social problems can hardly be solved with
technical means. If you have a bunch of good programmers and let them
choose their preferred tools, it's more likely that they produce good
software than when you have a bunch of average programmers and tell them
what tools they must use.

(It's very important in this regard that we are not talking about
braindead languages. There are both extremely stupid as well as
excellent exemplars of static and dynamic languages.)


Pascal
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,766
Messages
2,569,569
Members
45,042
Latest member
icassiem

Latest Threads

Top