Python from Wise Guy's Viewpoint

M

Matthias Blume

Pascal Costanza said:
Can you give a better example of a program that would render
meaningless without type annotations?

fun f A = "A"
| f B = "B"

This could be the function that always returns "A" for arbitrary
arguments, it could be the function that can only be legally applied
to the value (constructed by) A where it returns "A", it could be the
function that can be applied precisely to values A and B, it could
accept more than those two and raise an exception if the argument is
neither A nor B, it could be the function that can be applied to A
where it returns "A" and also many other values where it always
returns "B", ...

Which of these versions you get depends in part on the typing
environment that is in effect when the above code is encountered by
the compiler.

Matthias

PS: Just in case you wonder, here are examples of type definitions
which would trigger the results outlined above. I go in the same
order:

1. (* no type definition in effect *)
2. datatype t = A
3. datatype t = A | B
4. datatype t = A | B | C
5. datatype t = A | C | D of int | E of real | F of t -> t

Notice that the compiler will probably warn about a redundant match in
cases 1. and 2. and about a non-exhaustive match in case 4.
 
S

Stephen J. Bevan

Pascal Costanza said:
Not if I only want to check whether the first ten tests work, and
don't care about the remaining ones.

Perhaps I'm just a low tech kind of guy but if I just want to run the
first ten then I comment out the rest. Even without a fancy IDE that
only take a few key presses.
 
M

Matthew Danish

No, the correct answer isn't 600 in all cases.
If it's infinite-precision arithmetic, the correct answer is indeed 600.
If it's 8-bit arithmetic with overflow, there is no correct answer.
If it's 8-bit arithmetic with wrap-around, the correct answer is 88.
If it's 8-bit arithmetic with saturation, the correct answer is 255.
(The result happens to be independent of whether the arithmetic is
signed or unsigned.)

What is this stuff? I am talking about integers here! You know, the
infinite set with the same cardinality as natural numbers. Why can't
the implementation figure out how to represent them most efficiently?
Using infinite-precision internally for all calculations isn't always
practicable, and in some algorithms, this will give you even wrong
results (e.g. when multiplying or dividing using negative numbers, or
with saturating arithmetic).

If it gives you the wrong results, I'm not interested. How hard is it
to get the arithmetically correct result of 20 * 30? Surely 20 * -30 is
not that difficult either. It is a pet peeve I have with many
programming languages, especially the ones that are so big on proofs and
correctness.

Arithmetic as it exists in ML is a good example of the difference
between correctness and type-safety. It is also a good example of the
difference between correctness and proof.

Of course, you may claim, that the definition of div, or / or whatever
it is called, is "correct" in that it conforms to the specification.
All that says to me is that the specification is wrong. Garbage can be
proven, in some system. Doesn't mean I'm interested.
Of course, this doesn't say much about the distinction between static
and dynamic typing, actually the issues and unlucky fringe cases seem
very similar to me. (In particular, overloading and type inference don't
tell us whether the multiplication should be done in 8-bit, 16-bit,
machine-precision, infinite-precision, wrap-around, or saturated
arithmetic, and run-time types don't give us any useful answer either.)

Lisp gets exact rational arithmetic right, why don't ML or Haskell?
Clever compilers like CMUCL can even emit efficient code when it can
figure out the specific types. Surely a statically typed language would
find this even easier!
 
M

Marcin 'Qrczak' Kowalczyk

Sorry, I don't get this. Why should it be more dangerous with dynamic
typing? Common Lisp definitely gets this right, and most probably some
other dynamically typed languages.

Common Lisp, Scheme and Perl get it right: the result of / on integers
is a rational or floating point number, and integer division is spelled
differently.

Python and Ruby get it wrong: the result is an integer (truncated
division), very different from the result of / on the same numbers
represented in a different type.

If a statically typed language get's this "wrong", it doesn't hurt, except
newbies which write 1/n. For example consider this:

double foo(double *a, int len)
{
... Some computation involving a/a[j] which is supposed
... to produce a true real quotient.
}

You make some array of doubles, set a = exp(i) (a double) and it works.
Then you set a = 2*i (an integer) and it still works, because the
integer has been converted to double during assignment. An integer can be
used in place of a double with the same value.

Now in a dynamically typed language the analogous code would set some
array elements to integers without conversion. If division on them means
a different thing, an integer can no longer be used in place of a floating
point number with the same value. And division is the only operation whith
breaks this.

Dynamically typed languages shouldn't use / for both real and truncating
division. For statically typed languages it's only a matter of taste (I
prefer to use different symbols), but for dynamically typed languages it's
important to prevent bugs.
 
K

ketil+news

What is this stuff? I am talking about integers here!

But the SML program isn't. Or it may be, and maybe not. So it's
ambigous without type information.
Why can't the implementation figure out how to represent them most
efficiently?

Because it needs a type annotation or inference to decide that the
numbers are indeed integers, and not a different set with different
arithmetic properties.
Lisp gets exact rational arithmetic right, why don't ML or Haskell?

Could you point to a case where they don't? I don't understand your
criticism at all. Is the ability to do modulo arithmetic "wrong"?

-kzm
 
A

Andreas Rossberg

I offered my `black hole' program and got these responses:

Remi Vanicat <[email protected]>
`I don't see how this function can be useful...'

Jesse Tov <[email protected]>
`we don't need functions with those types.'

Dirk Thierbach <[email protected]>
`recursive types are very often a result of a real typing error.'

"Marshall Spight" <[email protected]>
`I don't believe the feature this function illustrates could be
useful.'

Will this the response for any program that does, in fact, fool a
number of static type checkers?

You missed one important point here. It is, in fact, trivial to extend
Hindley-Milner type inference in such a way that it can deal with your
function. That's what OCaml does when given the -rectypes option.
However, it is a conscious, deliberate decision not to do so, at least
not by default!

Why is that? Well, Dirk's quote gives the reason. But let me elaborate.

The design of a type system is by no means canonical. In fact, it is
based on set of pragmatic decisions and trade-offs. Idealized, you start
with the trivial type system, which has only one type. Then you refine
it incrementally by distinguishing certain classes of values through
introduction of new types and typing rules. Introduction of typing rules
is based on the following criteria:

- Do they catch a lot of errors?
- Are these errors serious?
- Are they hard to localize otherwise?
- Does the refinement rule out useful constructions?
- Are such constructions common?
- Are they expressible by other means?
- Are the rules intuitive?
- Do they interact nicely with other rules?

And probably more. There are never any definite answers to any of these
questions. The trade-off depends on many factors, such as the problem
domain the language is used for. Usually the line is drawn based on
experience with other languages and known problem domains. In the case
of arbitrary recursive types, experience with languages that allowed
them has clearly shown that it caused much more grief than joy.

BTW, almost the same criteria as above apply when you as a programmer
use the type system as a tool and program it by introducing your own
types. It can be tremendously useful if you make the right strategic
decisions. OTOH, especially if you are unexperienced, type abstractions
might also turn out to be counterproductive.

- Andreas

--
Andreas Rossberg, (e-mail address removed)-sb.de

"Computer games don't affect kids; I mean if Pac Man affected us
as kids, we would all be running around in darkened rooms, munching
magic pills, and listening to repetitive electronic music."
- Kristian Wilson, Nintendo Inc.
 
M

Matthew Danish

But the SML program isn't. Or it may be, and maybe not. So it's
ambigous without type information.


Because it needs a type annotation or inference to decide that the
numbers are indeed integers, and not a different set with different
arithmetic properties.

1 is an integer. Simple type inference. In Lisp, it's also a fixnum,
it's also an (unsigned-byte 23), it's also an (integer 1 (2)), etc.
Could you point to a case where they don't? I don't understand your
criticism at all. Is the ability to do modulo arithmetic "wrong"?

- fun fact 0 = 1 | fact n = n * fact (n - 1);
val fact = fn : int -> int
- fact 10;
val it = 3628800 : int
- fact 15;
val it = 1307674368000 : int (* ideally *)

- 1 * 1.0;
val it = 1.0 : float (* ideally *)

- 2 / 4;
val it = 1/2 : ratio (* ideally *)

- 2 truncate 4;
val it = 0 : int
 
K

ketil+news

Matthew Danish said:
1 is an integer. Simple type inference.

Okay, I'm not familiar enough with ML, and I didn't know that it won't
let you use 1 to represent 1.0 or 1/1 or whatever.
- fun fact 0 = 1 | fact n = n * fact (n - 1);
val fact = fn : int -> int
- fact 10;
val it = 3628800 : int
- fact 15;
val it = 1307674368000 : int (* ideally *)

Here's Haskell as provided by GHCi:

Prelude> let fact n = if n == 0 then 1 else n * fact (n-1)
Prelude> fact 10
3628800
Prelude> :i it
-- it is a variable
it :: Integer
Prelude> fact 15
1307674368000

So '10' defaults to Integer, which is infinite precision.
You can of course also do:

Prelude> fact 10.0
3628800.0
- 1 * 1.0;
val it = 1.0 : float (* ideally *)

Prelude> 1*1.0
1.0

The 1 is treated as a Float, since that is what * requires
- 2 / 4;
val it = 1/2 : ratio (* ideally *)

Prelude> 2/4
0.5
Prelude> (2/4 :: Rational)
1 % 2

The default is Float, but Rational is there if that's what you want
- 2 truncate 4;
val it = 0 : int

I assume this means:

Prelude> 2 `div` 4
0
Prelude> :i it
-- it is a variable
it :: Integer

If I understood you correctly, Haskell doesn't get it all that wrong?

-kzm
 
T

Tomasz Zielonka

I offered my `black hole' program and got these responses:

It can be written in Haskell using laziness. Am I quite sure you will
object in some way ;)

blackHole :: a
blackHole = error "black-hole"

*BH> :t blackHole 1 2 3 'a' "ho" (blackHole, 1.2)
blackHole 1 2 3 'a' "ho" (blackHole, 1.2) :: forall t. t

*BH> blackHole 1 2 3 'a' "ho" (blackHole, 1.2)
*** Exception: black-hole

*BH> let _ = blackHole 1 2 3 'a' "ho" (blackHole, 1.2) in "abcdef"
"abcdef"

Best regards,
Tom
 
J

Joachim Durchholz

Matthew said:
What is this stuff? I am talking about integers here! You know, the
infinite set with the same cardinality as natural numbers. Why can't
the implementation figure out how to represent them most efficiently?

Hey, that 20*30 expression given above didn't say what type of integer
arithmetic was meant, or did it?
Lisp gets exact rational arithmetic right, why don't ML or Haskell?

They do as well - infinite-precision integers are standard issue in
these languages.

It's just that Andreas's answer to the challenge of presenting a
type-dependent expression was simpler than anybody would have expected.
Of course, if your best response is that this all is ill-defined when it
isn't - then, I fear, you'll simply have to stay on your side of the fence.

Regards,
Jo
 
A

Andreas Rossberg

Matthew said:
May I point out that the correct answer is 600, not overflow?

No, it's not. If I choose type Int8 then I do so precisely for the
reason that I want to be signalled if some computation overflows the
intended value domain. Otherwise I had chosen IntInf or something. You
see, this is exactly the reason why I say that the type system gives you
expressiveness.
Something that annoys me about many statically-typed languages is the
insistence that arithmetic operations should return the same type as the
operands.

I should note in this context is that static types usually express
different things than dynamic ones, especially when it comes to number
types. In Lisp, the runtime tag of a number will usually describe the
representation of the number. This may well change between operations.
But static typing, at least in high-level languages, is about semantics.
If I choose a certain integer type I do so because it has the desired
domain, which I want to have checked - I'm not at all interested in its
representation. In fact, values of IntInf are likely to have multiple
representations depending on their size, but the type is invariant,
abstracting away from such low-level representation details.

Actually, I think dynamic typing should abstract from this as well, but
unfortunately this does not seem to happen.
2 / 4 is 1/2, not 0.

Integer division is not real division (and should not use the same name).
Arithmetically, 1 * 1.0 is
well-defined, so why can I not write this in an SML program?

Because the designers decided (rightly so, IMHO) that it is best to
avoid implicit conversions, since they might introduce subtle bugs and
does not coexist well with type inference. But anyway, this has nothing
to do with static vs dynamic typing - C allows the above, and there
might well be dynamic languages that raise a runtime error if you try it.
I do believe Haskell does it right, though, with its numeric tower
derived from Lisp.

Yes, in Haskell you can write the above, but for slightly different
reasons (integer literals are overloaded for floating types).

- Andreas

--
Andreas Rossberg, (e-mail address removed)-sb.de

"Computer games don't affect kids; I mean if Pac Man affected us
as kids, we would all be running around in darkened rooms, munching
magic pills, and listening to repetitive electronic music."
- Kristian Wilson, Nintendo Inc.
 
M

Matthew Danish

You are right about Haskell, as I wrote in my initial post on this, it
has a numeric tower derived from Lisp (though I think it is not enabled
by default, or something?). Mostly I was annoyed that 20 * 30 being
possibly an overflow was brought up as an example of static typing being
expressive.
 
A

Andreas Rossberg

Would this count?

(defun noisy-apply (f arglist)
(format t "I am now about to apply ~s to ~s" f arglist)
(apply f arglist))

Moscow ML version 2.00 (June 2000)
Enter `quit();' to quit.
- fun noisyApply f x =
(print "I am about to apply "; printVal f;
print " to "; printVal x; print "\n";
f x);
> val ('a, 'b) noisyApply = fn : ('a -> 'b) -> 'a -> 'b
- noisyApply Math.sin Math.pi;
I am about to apply fn to 3.14159265359
> val it = 1.22460635382E~16 : real


In this example, printVal requires some runtime type information. But
that does in no way preclude static type checking.

--
Andreas Rossberg, (e-mail address removed)-sb.de

"Computer games don't affect kids; I mean if Pac Man affected us
as kids, we would all be running around in darkened rooms, munching
magic pills, and listening to repetitive electronic music."
- Kristian Wilson, Nintendo Inc.
 
A

Andreas Rossberg

Pascal said:

Not sure how the "wrong" relates to the quoted paragraph, but I guess
you mean my conjecture that you cannot dispatch on return types with
dynamic typing.
(defmethod from-string-expansion (to-type string)
(if (or (subtypep to-type 'sequence)
(subtypep to-type 'character)
(eq to-type t))
`(coerce ,string ',to-type)
`(coerce (read-from-string ,string) ',to-type)))

(defmacro set-from-string (x string &environment env)
(multiple-value-bind
(bound localp declarations)
(variable-information x env)
(declare (ignore bound localp))
(let ((type (or (cdr (assoc 'type declarations)) t)))
`(setf ,x ,(from-string-expansion type string)))))

Interesting example, thanks for showing it. I'm not fluent enough in
Lisp to understand how this actually works but it does not seem to be
extensible in a compositional way (you have to insert all type cases by
hand). And does the resolution work transitively? I.e. if I write some
complex function f using fromString somewhere, performing arbitrary
calculations on its return value of type t, and returning something of a
type containing t, is all this code parametric in t such that I can call
f expecting arbitrary result types? All this would be automatic in Haskell.

Also note that your transcript shows that your approach indeed requires
*explicit* type annotations, while you would rarely need them when doing
the same thing in a language like Haskell.

Anyway, your code is complicated enough to make my point that the static
type system gives you similar expressiveness with less fuss.

- Andreas

--
Andreas Rossberg, (e-mail address removed)-sb.de

"Computer games don't affect kids; I mean if Pac Man affected us
as kids, we would all be running around in darkened rooms, munching
magic pills, and listening to repetitive electronic music."
- Kristian Wilson, Nintendo Inc.
 
P

Pascal Costanza

Andreas said:
Not sure how the "wrong" relates to the quoted paragraph, but I guess
you mean my conjecture that you cannot dispatch on return types with
dynamic typing.



Interesting example, thanks for showing it. I'm not fluent enough in
Lisp to understand how this actually works but it does not seem to be
extensible in a compositional way (you have to insert all type cases by
hand).

No, at least not in the default method for from-string-expansion I have
shown above. That method only covers the default cases, and needs to
distinguish different ways how Common Lisp handles pre-defined types.

What you would need to do for your own types is to write your own
specialized versions of from-string-expansion:

(defmethod from-string-expansion ((to-type (eql 'foo)) string)
`(somehow-convert-string-to-foo ,string))

You don't need to modify the method given above. (You need to write
conversion routines for your own types in any language.)
And does the resolution work transitively? I.e. if I write some
complex function f using fromString somewhere, performing arbitrary
calculations on its return value of type t, and returning something of a
type containing t, is all this code parametric in t such that I can call
f expecting arbitrary result types? All this would be automatic in Haskell.

It should be possible in principle. The macro shown above is expanded at
compile-time (this is 100% correct, but sufficiently correct for the
sake of our discussion). The &environment keyword captures the lexical
environment that is current at macro expansion time.
VARIABLE-INFORMATION looks up type information about a variable in that
environment, before the code is actually translated into its final form.

The original idea for such environment objects was that you could not
only look up standardized information about entities of the compilation
environment, but that you can also stuff in your own information. So in
principle, it should be possible to use this as a basis for a type
inferencing mechanism.

The downside of all this is that ANSI Common Lisp doesn't define the
needed functions to do all this. It defines such environment objects
only in very rudimentary ways that are actually not powerful enough.
CLtL2 had this stuff, but apparently it had some glitches that are hard
to get right, and it was decided to leave this stuff out of the ANSI
standard.

There are Common Lisp implementations that implement this functionality
to a certain degree, and apparently Duane Rettig of Franz Inc. is
currently working on an implementation that has sorted out the edge
cases. They seem to consider making this available as open source.

It's probably possible to do similar things with the ANSI-defined
DEFINE-COMPILER-MACRO, or with proprietary extensions like DEFTRANSFORM
in LispWorks.
Also note that your transcript shows that your approach indeed requires
*explicit* type annotations, while you would rarely need them when doing
the same thing in a language like Haskell.

I think it should be possible in principle to add type inferencing to
Common Lisp as a sophisticated macro package, even without proper
environment objects.

It would probably take serious effort to implement such a beast, and it
would be difficult to solve interferences with "third-party" Lisp code,
but at least you would not need to change the base compiler to add this.
Anyway, your code is complicated enough to make my point that the static
type system gives you similar expressiveness with less fuss.

Yes, you're right. If you would want/need static type checking by
default, it wouldn't make much sense to do this in a dynamically typed
language that treats static type checking as an exceptional case. And
vice versa.


Pascal
 
A

Alexander Schmolck

Marcin 'Qrczak' Kowalczyk said:
Python and Ruby get it wrong: the result is an integer (truncated
division), very different from the result of / on the same numbers
represented in a different type.

It's being fixed since python >= 2.2 (sort of):
2

'as
 
M

Matthias Blume

Matthew Danish said:
What is this stuff? I am talking about integers here! You know, the
infinite set with the same cardinality as natural numbers.

I didn't see you say that. If you write 20 * 30 in SML, then you are
*not* talking about the infinite set of integers but rather about a set
[-2^N..2^N-1] where N is something like 31 or 32. If you had written

20 * 30 : IntInf.int

or something like that, then you would have talked about the infinite
set of integeres, and you would have gotten your "correct" result of
600. [I still have no idea why that is more "correct" than, say, 18.
That's the result in Z_{97}.]
Lisp gets exact rational arithmetic right, why don't ML or Haskell?
Clever compilers like CMUCL can even emit efficient code when it can
figure out the specific types. Surely a statically typed language would
find this even easier!

Sure, it is definitely not any harder than it is in Lisp. The problem
is that for many algorithms people want to be sure that the compiler
represents their values in machine words. Infinite precision is
needed sometimes, but in the majority of cases it is overkill. If you
need infinite precision, specify the type (IntInf.int in SML's case).
A clever compiler might optimize that like a Lisp compiler does. In
most other cases, why take any chances?

Matthias
 
M

Matthias Blume

Matthew Danish said:
1 is an integer. Simple type inference. In Lisp, it's also a fixnum,
it's also an (unsigned-byte 23), it's also an (integer 1 (2)), etc.


- fun fact 0 = 1 | fact n = n * fact (n - 1);
val fact = fn : int -> int
- fact 10;
val it = 3628800 : int
- fact 15;
val it = 1307674368000 : int (* ideally *)

$ sml
Standard ML of New Jersey v110.43.3 [FLINT v1.5], September 26, 2003
- fun fact 0 = 1 : IntInf.int
= | fact n = n * fact (n - 1);
[autoloading]
[autoloading done]
val fact = fn : IntInf.int -> IntInf.int
- fact 15;
val it = 1307674368000 : IntInf.int
-
- 1 * 1.0;
val it = 1.0 : float (* ideally *)

That's not "ideal" at all, to me. I find the automatic conversions in
C a paint in the b*tt because it is not obvious at all where they
happen in a given expression. How hard is it to write

real 1 * 1.0

thereby making things explicit, unambiguous, and non-surprising?

Matthias
 
D

Don Geddis

Marcin 'Qrczak' Kowalczyk said:
You make some array of doubles, set a = exp(i) (a double) and it works.
Then you set a = 2*i (an integer) and it still works, because the
integer has been converted to double during assignment. An integer can be
used in place of a double with the same value.


Surely in a "proper" staticly typed language, you can't assign an integer
to a variable declared (floating point) double. Shouldn't that be a type
error? (Unless you do an explicit cast, of course, but then the programmer
made the decision, not the language.)
Now in a dynamically typed language the analogous code would set some
array elements to integers without conversion. If division on them means
a different thing, an integer can no longer be used in place of a floating
point number with the same value. And division is the only operation whith
breaks this.

Why would division on integers mean something different than division on
(floating point) doubles?
Dynamically typed languages shouldn't use / for both real and truncating
division. For statically typed languages it's only a matter of taste (I
prefer to use different symbols), but for dynamically typed languages it's
important to prevent bugs.

In Lisp, "/" always means the same thing. It's never a truncating operation.

This doesn't seem to be related to static vs. dynamic typing.

-- Don
_______________________________________________________________________________
Don Geddis http://don.geddis.org/ (e-mail address removed)
Football combines two of the worst things about American life.
It is violence punctuated by committee meetings.
-- George Will
 
D

Don Geddis

Andreas Rossberg said:
I should note in this context is that static types usually express different
things than dynamic ones, especially when it comes to number types. In Lisp,
the runtime tag of a number will usually describe the representation of the
number. This may well change between operations. But static typing, at least
in high-level languages, is about semantics. If I choose a certain integer
type I do so because it has the desired domain, which I want to have checked
- I'm not at all interested in its representation.

Types don't have to be disjoint. In Lisp, if a data object is a FIXNUM,
at the same time it's also a NUMBER. And perhaps an (INTEGER 0 16) too.

Yes, at least one of the types defines the representation. But there are
semantic types as well.

As to "change between operations": it doesn't matter what your type system is.
Any function call has the potential to "change types". It would be a silly
system that requires the (type) domain and range of every function to always
be identical.
In fact, values of IntInf are likely to have multiple representations
depending on their size, but the type is invariant, abstracting away from
such low-level representation details.

And Lisp's NUMBER type also has multiple representations. And the SQRT
function takes a NUMBER and returns a NUMBER. But also, at the same time,
it takes INTEGERs and returns FLOATs, and takes negative INTEGERs and returns
COMPLEX NUMBERs. Semantics and representation, all at the same time!
Actually, I think dynamic typing should abstract from this as well, but
unfortunately this does not seem to happen.

But it does.
Because the designers decided (rightly so, IMHO) that it is best to avoid
implicit conversions, since they might introduce subtle bugs and does not
coexist well with type inference.

Sounds like a tradeoff. Perhaps, for some programmers, the freedom you get
with implicit type conversions is more valuable than the benefits of type
inference?

-- Don
_______________________________________________________________________________
Don Geddis http://don.geddis.org/ (e-mail address removed)
 

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

Forum statistics

Threads
473,795
Messages
2,569,644
Members
45,357
Latest member
RuthEsteve

Latest Threads

Top