Buscando aclaraciones sobre las aparentes contradicciones con respecto a los lenguajes mal tipados.

Creo que entiendo la tipificación fuerte , pero cada vez que busco ejemplos de lo que es la tipificación débil, termino encontrando ejemplos de lenguajes de progtwigción que simplemente coaccionan / convierten tipos automáticamente.

Por ejemplo, en este artículo llamado Escribir: Fuerte frente a Débil, Estático vs. Dinámico dice que Python está fuertemente tipado porque obtienes una excepción si intentas:

Pitón

1 + "1" Traceback (most recent call last): File "", line 1, in ? TypeError: unsupported operand type(s) for +: 'int' and 'str' 

Sin embargo, tal cosa es posible en Java y en C #, y no los consideramos tipificados débilmente para eso.

Java

  int a = 10; String b = "b"; String result = a + b; System.out.println(result); 

DO#

 int a = 10; string b = "b"; string c = a + b; Console.WriteLine(c); 

En este otro artículo llamado Weakly Type Languages, el autor dice que Perl está mal escrito simplemente porque puedo concatenar una cadena a un número y viceversa sin ninguna conversión explícita.

Perl

 $a=10; $b="a"; $c=$a.$b; print $c; #10a 

Entonces, el mismo ejemplo hace que Perl esté débilmente escrito, pero no Java y C #?

Caramba, esto es confuso introduzca la descripción de la imagen aquí

Los autores parecen implicar que un lenguaje que impide la aplicación de ciertas operaciones en valores de diferentes tipos está fuertemente tipado y lo contrario significa que está tipificado débilmente.

Por lo tanto, en algún momento me he sentido obligado a creer que si un idioma proporciona muchas conversiones automáticas o coacción entre tipos (como perl) puede terminar siendo considerado poco tipificado, mientras que otros idiomas que proporcionan solo unas pocas conversiones pueden terminar siendo considerados considerado fuertemente tipado.

Sin embargo, me inclino a creer que debo estar equivocado en esta interpretación, simplemente no sé por qué ni cómo explicarlo.

Entonces, mis preguntas son:

  • ¿Qué significa realmente que un idioma se escriba de manera débil?
  • ¿Podría mencionar algún buen ejemplo de escritura débil que no esté relacionada con la conversión automática / coerción automática realizada por el idioma?
  • ¿Se puede escribir un idioma débilmente y escribir fuerte al mismo tiempo?

ACTUALIZACIÓN: Esta pregunta fue el tema de mi blog el 15 de octubre de 2012. ¡ Gracias por la gran pregunta!


¿Qué significa realmente que un idioma esté “escrito débilmente”?

Significa que “este lenguaje usa un sistema de tipos que encuentro desagradable”. Por contraste, un lenguaje “fuertemente tipado” es un lenguaje con un sistema de tipos que me parece agradable.

Los términos son esencialmente sin sentido y usted debe evitarlos. Wikipedia enumera once significados diferentes para “tipificado fuertemente”, varios de los cuales son contradictorios. Esto indica que las posibilidades de confusión que se crean son altas en cualquier conversación que involucre el término “tipificado fuertemente” o “tipificado débilmente”.

Todo lo que realmente puede decir con certeza es que un lenguaje “fuertemente tipado” en discusión tiene alguna restricción adicional en el sistema de tipos, ya sea en tiempo de ejecución o de comstackción, que carece de un lenguaje “débilmente tipado” en discusión. Lo que esa restricción podría ser no puede determinarse sin más contexto.

En lugar de usar “fuertemente tipado” y “débilmente tecleado”, debe describir en detalle a qué tipo de tipo de seguridad se refiere. Por ejemplo, C # es un lenguaje tipificado estáticamente y un lenguaje de tipo seguro y un lenguaje de memoria segura , en su mayor parte . C # permite que se violen las tres formas de escritura “fuerte”. El operador de reparto viola la escritura estática; le dice al comstackdor “Sé más sobre el tipo de tiempo de ejecución de esta expresión que usted”. Si el desarrollador está equivocado, entonces el tiempo de ejecución lanzará una excepción para proteger la seguridad del tipo. Si el desarrollador desea romper la seguridad del tipo o la seguridad de la memoria, puede hacerlo apagando el sistema de seguridad del tipo creando un bloque “inseguro”. En un bloque inseguro, puede usar la magia del puntero para tratar un int como un flotador (violando la seguridad del tipo) o para escribir en la memoria que no es de su propiedad. (Violando la seguridad de la memoria.)

C # impone restricciones de tipo que se verifican tanto en tiempo de comstackción como en tiempo de ejecución, por lo que es un lenguaje “fuertemente tipado” en comparación con los idiomas que hacen menos chequeos en tiempo de comstackción o menos chequeos en tiempo de ejecución. C # también le permite realizar una ejecución final alrededor de esas restricciones, lo que lo convierte en un lenguaje “de tipo débil” en comparación con los idiomas que no le permiten realizar dicha ejecución final.

¿Cuál es realmente? Es imposible decirlo; depende del punto de vista del hablante y de su actitud hacia las distintas características del idioma.

Como otros han señalado, los términos “fuertemente tipado” y “débilmente tecleado” tienen tantos significados diferentes que no hay una respuesta única a tu pregunta. Sin embargo, como mencionó específicamente a Perl en su pregunta, permítame tratar de explicar en qué sentido Perl está tipificado débilmente.

El punto es que, en Perl, no existe una “variable entera”, una “variable flotante”, una “variable de cadena” o una “variable booleana”. De hecho, en la medida en que el usuario puede (generalmente) decir, ni siquiera hay valores enteros, flotantes, de cadena o booleanos: todo lo que tiene son “escalares”, que son todas estas cosas al mismo tiempo. Así que puedes, por ejemplo, escribir:

 $foo = "123" + "456"; # $foo = 579 $bar = substr($foo, 2, 1); # $bar = 9 $bar .= " lives"; # $bar = "9 lives" $foo -= $bar; # $foo = 579 - 9 = 570 

Por supuesto, como nota correctamente, todo esto puede verse como un simple tipo de coerción. Pero el punto es que, en Perl, los tipos siempre están obligados. De hecho, es bastante difícil para un usuario decir cuál podría ser el “tipo” interno de una variable: en la línea 2 en mi ejemplo anterior, preguntar si el valor de $bar es la cadena "9" o el número 9 es bastante no tiene mucho sentido, ya que, en lo que respecta a Perl, son la misma cosa . De hecho, incluso es posible que un escalar de Perl internamente tenga tanto una cadena como un valor numérico al mismo tiempo, como es el caso de $foo después de la línea 2 anterior.

La otra cara de todo esto es que, dado que las variables de Perl no están tipificadas (o, más bien, no exponen su tipo interno al usuario), los operadores no pueden ser sobrecargados para hacer cosas diferentes para diferentes tipos de argumentos; no puede simplemente decir “este operador hará X para los números y Y para las cadenas”, porque el operador no puede (no) decir qué tipo de valores son sus argumentos.

Así, por ejemplo, Perl tiene y necesita un operador de sum numérico ( + ) y un operador de concatenación de cadenas ( . ): Como se vio anteriormente, es perfectamente correcto agregar cadenas ( "1" + "2" == "3" ) o para concatenar números ( 1 . 2 == 12 ). De manera similar, los operadores de comparación numéricos == != , < , > , <= , >= Y <=> comparan los valores numéricos de sus argumentos, mientras que los operadores de comparación de cadenas eq , ne , lt , gt , le , ge y cmp compara lexicográficamente como cuerdas. Entonces, 2 < 10 , pero 2 gt 10 (pero "02" lt 10 , mientras que "02" == 2 ). (Tenga en cuenta que algunos otros lenguajes, como JavaScript, intentan adaptarse a la tipificación débil similar a la de Perl al tiempo que realizan sobrecargas de operadores. Esto a menudo conduce a la fealdad, como la pérdida de asociatividad para + ).

(La cuestión en la pomada aquí es que, por razones históricas, Perl 5 tiene algunos casos de esquina, como los operadores lógicos a nivel de bits, cuyo comportamiento depende de la representación interna de sus argumentos. Estos son generalmente considerados como un defecto de diseño molesto, ya que la representación interna puede cambiar por razones sorprendentes, por lo que predecir qué hacen esos operadores en una situación determinada puede ser complicado.)

Dicho todo esto, se podría argumentar que Perl tiene tipos fuertes; Simplemente no son el tipo de tipos que podrías esperar. Específicamente, además del tipo "escalar" discutido anteriormente, Perl también tiene dos tipos estructurados: "array" y "hash". Esos son muy distintos de los escalares, hasta el punto donde las variables de Perl tienen diferentes sigilos que indican su tipo ( $ para escalares, @ para matrices, % para hashes) 1 . Existen reglas de coerción entre estos tipos, por lo que puede escribir, por ejemplo, %foo = @bar , pero muchas de ellas son bastante deficientes: por ejemplo, $foo = @bar asigna la longitud de la matriz @bar a $foo , no su contenido . (Además, hay algunos otros tipos extraños, como typeglobs y controles de E / S, que a menudo no se ven expuestos).

Además, un pequeño detalle en este bonito diseño es la existencia de tipos de referencia, que son un tipo especial de escalares (y que se pueden distinguir de los escalares normales, utilizando el operador de ref ). Es posible usar referencias como escalares normales, pero sus valores numéricos / de cadena no son particularmente útiles, y tienden a perder su carácter de referencia especial si los modifica utilizando las operaciones escalares normales. Además, cualquier variable 2 de Perl puede ser bless para una clase, convirtiéndola en un objeto de esa clase; El sistema de clase OO en Perl es algo ortogonal al sistema de tipo primitivo (o falta de tipografía) descrito anteriormente, aunque también es "débil" en el sentido de seguir el paradigma de tipificación de pato . La opinión general es que, si te encuentras revisando la clase de un objeto en Perl, estás haciendo algo mal.


1 En realidad, el sigilo denota el tipo de valor al que se accede, de modo que, por ejemplo, el primer escalar en la matriz @foo se denota $foo[0] . Ver perlfaq4 para más detalles.

A los objetos en Perl (normalmente) se accede mediante referencias a ellos, pero lo que realmente se bless es la variable (posiblemente anónima) a la que apunta la referencia. Sin embargo, la bendición es, de hecho, una propiedad de la variable, no de su valor, por lo que, por ejemplo, la asignación de la variable bendecida real a otra solo te da una copia superficial y sin bendiciones de la misma. Ver perlobj para más detalles.

Además de lo que Eric ha dicho, considere el siguiente código C:

 void f(void* x); f(42); f("hello"); 

En contraste con lenguajes como Python, C #, Java o lo que sea, lo anterior se escribe débilmente porque perdemos información de tipo. Eric señaló correctamente que en C # podemos sortear el comstackdor lanzando, diciéndole efectivamente “Sé más sobre el tipo de esta variable que tú”.

Pero incluso entonces, ¡el tiempo de ejecución comprobará el tipo! Si el lanzamiento no es válido, el sistema de ejecución lo detectará y lanzará una excepción.

Con el borrado de tipo, esto no sucede: la información de tipo se desecha. Un elenco para void* en C hace exactamente eso. En este sentido, lo anterior es fundamentalmente diferente de una statement de método C # como void f(Object x) .

(Técnicamente, C # también permite el borrado de tipos a través de código inseguro o ordenamiento).

Esto se escribe tan débilmente como se obtiene. Todo lo demás es solo una cuestión de comprobación de tipos estática frente a dinámica, es decir, del momento en que se verifica un tipo.

Un ejemplo perfecto proviene del artículo de Wikipedia de Strong Typing :

En general, una tipificación fuerte implica que el lenguaje de progtwigción impone severas restricciones a la mezcla que se permite.

Mecanografía débil

 a = 2 b = "2" concatenate(a, b) # returns "22" add(a, b) # returns 4 

Mecanografía fuerte

 a = 2 b = "2" concatenate(a, b) # Type Error add(a, b) # Type Error concatenate(str(a), b) #Returns "22" add(a, int(b)) # Returns 4 

Observe que un lenguaje de escritura débil puede mezclar diferentes tipos sin errores. Un lenguaje de tipo fuerte requiere que los tipos de entrada sean los tipos esperados. En un lenguaje de tipo fuerte, un tipo puede convertirse ( str(a) convierte un número entero en una cadena) o convertir ( int(b) ).

Todo esto depende de la interpretación de la tipificación.

Me gustaría contribuir a la discusión con mi propia investigación sobre el tema, ya que otros comentan y contribuyen. He estado leyendo sus respuestas y siguiendo sus referencias, y he encontrado información interesante. Como se sugirió, es probable que la mayor parte de esto se discuta mejor en el foro de Progtwigdores, ya que parece ser más teórico que práctico.

Desde un punto de vista teórico, creo que el artículo de Luca Cardelli y Peter Wegner titulado Sobre la comprensión de los tipos, la abstracción de datos y el polymorphism tiene uno de los mejores argumentos que he leído.

Un tipo puede verse como un conjunto de ropa (o una armadura) que protege una representación subyacente sin tipo de uso arbitrario o no intencionado. Proporciona una cubierta protectora que oculta la representación subyacente y restringe la forma en que los objetos pueden interactuar con otros objetos. En un sistema sin tipo, los objetos sin tipo están desnudos en el sentido de que la representación subyacente está expuesta para que todos la vean. Violar el sistema de tipo implica quitarse el conjunto de ropa protectora y operar directamente en la representación desnuda.

Esta statement parece sugerir que una escritura débil nos permitiría acceder a la estructura interna de un tipo y manipularlo como si fuera otra cosa (otro tipo). Quizás lo que podríamos hacer con el código inseguro (mencionado por Eric) o con los punteros borrados de tipo c mencionados por Konrad.

El artículo continúa …

Los idiomas en los que todas las expresiones son congruentes con el tipo se denominan lenguajes fuertemente tipados. Si se escribe un idioma con fuerza, su comstackdor puede garantizar que los progtwigs que acepta se ejecutarán sin errores de tipo. En general, debemos esforzarnos por una tipificación fuerte y adoptar la tipificación estática siempre que sea posible. Tenga en cuenta que cada lenguaje tipado estáticamente está fuertemente tipado, pero lo contrario no es necesariamente cierto.

Como tal, la tipificación fuerte significa la ausencia de errores tipográficos, solo puedo suponer que la tipificación débil significa lo contrario: la posible presencia de errores tipográficos. ¿En tiempo de ejecución o comstackción? Parece irrelevante aquí.

Lo curioso, según esta definición, un lenguaje con poderosas coerciones de tipo como Perl se consideraría fuertemente tipado, porque el sistema no está fallando, pero está tratando con los tipos al obligarlos a obtener equivalencias apropiadas y bien definidas.

Por otro lado, ¿podría decir que la asignación de ClassCastException y ArrayStoreException (en Java) e InvalidCastException , ArrayTypeMismatchException (en C #) indicaría un nivel de escritura débil, al menos en tiempo de comstackción? La respuesta de Eric parece estar de acuerdo con esto.

En un segundo artículo llamado Progtwigción tipográfica provista en una de las referencias proporcionadas en una de las respuestas en esta pregunta, Luca Cardelli profundiza en el concepto de violaciones de tipo:

La mayoría de los lenguajes de progtwigción del sistema permiten violaciones de tipo arbitrarias, algunas de manera indiscriminada, algunas solo en partes restringidas de un progtwig. Las operaciones que implican violaciones de tipo se denominan no válidas. Las violaciones de tipo se dividen en varias clases [entre las que podemos mencionar]:

Coerciones de valor básico : incluyen conversiones entre enteros, booleanos, caracteres, conjuntos, etc. Aquí no es necesario que se cometan infracciones de tipo, ya que se pueden proporcionar interfaces incorporadas para llevar a cabo las coerciones en forma de sonido de tipo.

Como tal, las coerciones de tipo como las proporcionadas por los operadores podrían considerarse violaciones de tipo, pero a menos que rompan la consistencia del sistema de tipo, podríamos decir que no conducen a un sistema de tipo débil.

Basados ​​en esto, ni Python, Perl, Java o C # están mal escritos.

Cardelli menciona dos tipos de vilaciones que considero muy bien los casos de tipificación verdaderamente débil:

Aritmética de direcciones. Si es necesario, debe haber una interfaz incorporada (sin sonido), que proporcione las operaciones adecuadas en las direcciones y conversiones de tipo. Varias situaciones involucran punteros en el montón (muy peligrosos al reubicar coleccionistas), punteros a la stack, punteros a áreas estáticas, y punteros a otros espacios de direcciones. A veces, la indexación de matrices puede reemplazar la aritmética de direcciones. Mapeo de memoria. Esto implica observar un área de la memoria como una matriz no estructurada, aunque contiene datos estructurados. Esto es típico de los asignadores y recolectores de memoria.

Este tipo de cosas posibles en lenguajes como C (mencionados por Konrad) o mediante código inseguro en .Net (mencionados por Eric) realmente implicarían una tipificación débil.

Creo que la mejor respuesta hasta ahora es la de Eric, porque la definición de estos conceptos es muy teórica, y cuando se trata de un lenguaje en particular, las interpretaciones de todos estos conceptos pueden llevar a diferentes conclusiones discutibles.

La tipificación débil de hecho significa que un alto porcentaje de tipos puede ser forzado implícitamente, intentando adivinar lo que pretendía el codificador.

La escritura fuerte significa que los tipos no están obligados, o al menos obligados menos.

La escritura estática significa que los tipos de sus variables se determinan en el momento de la comstackción.

Mucha gente recientemente ha estado confundiendo “manifiestamente tipado” con “fuertemente tipado”. “Escrito de forma manifiesta” significa que usted declara explícitamente los tipos de sus variables.

Python es en su mayoría fuertemente tipado, aunque puedes usar casi cualquier cosa en un contexto booleano, y los booleanos pueden usarse en un contexto entero, y puedes usar un entero en un contexto flotante. No se escribe de forma manifiesta, porque no es necesario que declare sus tipos (excepto Cython, que no es del todo Python, aunque sea interesante). Tampoco está tipificado estáticamente.

C y C ++ se tipifican de forma manifiesta, estática y de forma algo rígida, porque declara sus tipos, los tipos se determinan en el momento de la comstackción y puede mezclar enteros y punteros, o enteros y dobles, o incluso convertir un puntero a un tipo en Un puntero a otro tipo.

Haskell es un ejemplo interesante, porque no está tipificado manifiestamente, pero también está tipificado de manera estática y fuerte.

La escritura fuerte <=> débil no solo se trata de la continuidad de la cantidad o la cantidad de valores que el lenguaje coacciona automáticamente para un tipo de datos a otro, sino de la fuerza o la debilidad de escribir los valores reales. En Python y Java, y principalmente en C #, los valores tienen sus tipos establecidos en piedra. En Perl, no tanto, en realidad solo hay un puñado de tipos de valores diferentes para almacenar en una variable.

Abramos los casos uno por uno.


Pitón

En el ejemplo 1 + "1" Python, el operador + llama al __add__ para el tipo int dándole la cadena "1" como un argumento; sin embargo, esto da como resultado No implementado:

 >>> (1).__add__('1') NotImplemented 

A continuación, el intérprete prueba __radd__ de str:

 >>> '1'.__radd__(1) Traceback (most recent call last): File "", line 1, in  AttributeError: 'str' object has no attribute '__radd__' 

A medida que falla, el operador + falla con el resultado TypeError: unsupported operand type(s) for +: 'int' and 'str' . Como tal, la excepción no dice mucho acerca de la tipificación fuerte, pero el hecho de que el operador + no obligue sus argumentos automáticamente al mismo tipo, es un indicador del hecho de que Python no es el lenguaje más débilmente escrito en el continuo.

Por otro lado, en Python 'a' * 5 se implementa:

 >>> 'a' * 5 'aaaaa' 

Es decir,

 >>> 'a'.__mul__(5) 'aaaaa' 

El hecho de que la operación sea diferente requiere una tipificación fuerte; sin embargo, lo contrario a * forzar los valores a los números antes de multiplicarlos aún no necesariamente hace que los valores estén tipificados de manera débil.


Java

El ejemplo de Java, String result = "1" + 1; funciona solo porque, por conveniencia, el operador + está sobrecargado para cadenas. El operador Java + reemplaza la secuencia con la creación de un StringBuilder (ver esto ):

 String result = a + b; // becomes something like String result = new StringBuilder().append(a).append(b).toString() 

Este es más bien un ejemplo de escritura muy estática, sin ninguna coacción real: StringBuilder tiene un método append(Object) que se usa específicamente aquí. La documentación dice lo siguiente:

Anexa la representación de cadena del argumento Object .

El efecto general es exactamente como si el argumento se convirtiera en una cadena con el método String.valueOf(Object) , y los caracteres de esa cadena se agregaran a esta secuencia de caracteres.

Donde String.valueOf entonces

Devuelve la representación de cadena del argumento Objeto. [Devuelve] si el argumento es null , entonces una cadena igual a "null" ; de lo contrario, se obj.toString() el valor de obj.toString() .

Por lo tanto, este es un caso de absolutamente ninguna coerción por parte del lenguaje, delegando cada preocupación en los objetos en sí.


DO#

De acuerdo con la respuesta de Jon Skeet aquí , el operador + ni siquiera está sobrecargado para la clase de string – similar a Java, esto es solo una conveniencia generada por el comstackdor, gracias a la tipificación estática y fuerte.


Perl

Como explica la perldata ,

Perl tiene tres tipos de datos incorporados: escalares, matrices de escalas y matrices asociativas de escalas, conocidas como “hashes”. Un escalar es una cadena única (de cualquier tamaño, limitada solo por la memoria disponible), número o una referencia a algo (que se analizará en perlref). Las matrices normales son listas ordenadas de escalares indexadas por número, comenzando con 0. Los hashes son colecciones desordenadas de valores escalares indexados por su clave de cadena asociada.

Sin embargo, Perl no tiene un tipo de datos separado para números, booleanos, cadenas, nulos, s undefined , referencias a otros objetos, etc. – solo tiene un tipo para todos estos, el tipo escalar; 0 es un valor escalar tanto como es “0”. Una variable escalar que se configuró como una cadena realmente puede cambiar a un número, y de ahí en adelante se comportará de manera diferente a “solo una cadena”, si se accede a ella en un contexto numérico . El escalar puede contener cualquier cosa en Perl, es tanto el objeto como existe en el sistema. mientras que en Python los nombres solo se refieren a los objetos, en Perl los valores escalares en los nombres son objetos modificables. Además, el sistema de tipo orientado a objetos está pegado encima de esto: solo hay 3 tipos de datos en perl: escalares, listas y hashes. Un objeto definido por el usuario en Perl es una referencia (que es un puntero a cualquiera de los 3 anteriores) bless para un paquete: puede tomar cualquier valor y bendecirlo a cualquier clase en el instante que desee.

Perl incluso le permite cambiar las clases de valores a su gusto – esto no es posible en Python donde crear un valor de alguna clase necesita construir explícitamente el valor que pertenece a esa clase con el object.__new__ o similar. En Python no puedes realmente cambiar la esencia del objeto después de la creación, en Perl puedes hacer mucho:

 package Foo; package Bar; my $val = 42; # $val is now a scalar value set from double bless \$val, Foo; # all references to $val now belong to class Foo my $obj = \$val; # now $obj refers to the SV stored in $val # thus this prints: Foo=SCALAR(0x1c7d8c8) print \$val, "\n"; # all references to $val now belong to class Bar bless \$val, Bar; # thus this prints Bar=SCALAR(0x1c7d8c8) print \$val, "\n"; # we change the value stored in $val from number to a string $val = 'abc'; # yet still the SV is blessed: Bar=SCALAR(0x1c7d8c8) print \$val, "\n"; # and on the course, the $obj now refers to a "Bar" even though # at the time of copying it did refer to a "Foo". print $obj, "\n"; 

por lo tanto, la identidad de tipo está unida débilmente a la variable, y se puede cambiar a través de cualquier referencia sobre la marcha. De hecho, si lo haces

 my $another = $val; 

\$another no tiene la identidad de clase, aunque \$val aún dará la referencia bendita.


TL; DR

Hay mucho más acerca de la tipificación débil para Perl que solo las coerciones automáticas, y se trata más bien de que los tipos de los valores en sí mismos no están escritos en piedra, a diferencia del Python, que es un lenguaje dinámicamente pero muy fuertemente tipado. Ese python da TypeError en 1 + "1" es una indicación de que el lenguaje está fuertemente tipado, aunque el contrario es hacer algo útil, ya que en Java o C # no les impide ser lenguajes fuertemente tipados.

I like @Eric Lippert’s answer , but to address the question – strongly typed languages typically have explicit knowledge of the types of variables at each point of the program. Weakly typed languages do not, so they can attempt to perform an operation that may not be possible for a particular type. It think the easiest way to see this is in a function. C++:

 void func(string a) {...} 

The variable a is known to be of type string and any incompatible operation will be caught at compile time.

Pitón:

 def func(a) ... 

The variable a could be anything and we can have code that calls an invalid method, which will only get caught at runtime.

As many others have expressed, the entire notion of “strong” vs “weak” typing is problematic.

As a archetype, Smalltalk is very strongly typed — it will always raise an exception if an operation between two objects is incompatible. However, I suspect few on this list would call Smalltalk a strongly-typed language, because it is dynamically typed.

I find the notion of “static” versus “dynamic” typing more useful than “strong” versus “weak.” A statically-typed language has all the types figured out at compile-time, and the programmer has to explicitly declare if otherwise.

Contrast with a dynamically-typed language, where typing is performed at run-time. This is typically a requirement for polymorphic languages, so that decisions about whether an operation between two objects is legal does not have to be decided by the programmer in advance.

In polymorphic, dynamically-typed languages (like Smalltalk and Ruby), it’s more useful to think of a “type” as a “conformance to protocol.” If an object obeys a protocol the same way another object does — even if the two objects do not share any inheritance or mixins or other voodoo — they are considered the same “type” by the run-time system. More correctly, an object in such systems is autonomous, and can decide if it makes sense to respond to any particular message referring to any particular argument.

Want an object that can make some meaningful response to the message “+” with an object argument that describes the colour blue? You can do that in dynamically-typed languages, but it is a pain in statically-typed languages.