El lenguaje LOGO fue creado en el "Massachusetts Institute of
Technology" (MIT) por el investigador en inteligencia artificial y
renombrado teórico de la educación Seymour Papert. Pertenece a la
familia de lenguajes simbólicos, y por ello se diferencia
sustancialmente de los más conocidos lenguajes de programación,
como Basic, Pascal, Cobol o Fortran. No obstante, tiene también
capacidades matemáticas suficientes como para haber sido postulado
como la mejor introducción a estas disciplinas por el mismo
Papert. (PAPERT 80) En particular, LOGO es un pariente cercano de otros
lenguajes simbólicos diseñados específicamente para sustentar la
investigación en inteligencia artificial. Más concretamente, LOGO
es una versión simplificada del lenguaje LISP (del inglés "List
Processing"), uno de los más evolucionados lenguajes de
inteligencia artificial, adaptada para tener capacidades gráficas
y ser más asequible a los niños.
No deja de ser paradójico, y emocionante, que el lenguaje de
computación que usan los niños de las escuelas rurales más remotas
de Costa Rica, sea un miembro de la familia de los lenguajes de
inteligencia artificial, que son los lenguajes de programación más
poderosos que existen. El tópico es suficientemente sugerente como
para que lo exploremos con alguna profundidad, con la misma
curiosidad que demostraríamos al descubrir que alguno de nuestros
amigos está emparentado con miembros de la realeza reinante. Un
título alternativo para esta charla, pues, podría haber sido "La
Ilustre Parentela del Lenguaje LOGO". En todo caso, y vana
curiosidad aparte, resulta interesante para personas que trabajamos
rutinariamente con LOGO, estar enterados de su posición en el
contexto general de los lenguajes de programación y familiarizarnos
con algunas de las coincidencias y divergencias de los lenguajes
más cercanos a él.
Para comenzar nuestra tarea, tratemos de ubicar primero a toda la
familia de los lenguajes de inteligencia artificial dentro del
género total de los lenguajes de programación. Nada mejor para ello
que citar a Marvin Minsky, en su estupendo artículo sobre las
capacidades intelectuales de las computadoras.
(MINSKY 82) En ese artículo
Minsky se pregunta si, como creen muchas personas, las computadoras
pueden solamente hacer lo que se les ordena. Como forma de
contestar esta pregunta, reconoce, en primer lugar, que los más
antiguos programas computacionales eran ciclos de instrucciones del
estilo "haga esto; haga lo otro; haga esto y lo otro, y siga
haciéndolo hasta que aquello pase". Enseguida procede a dar una
clasificación aproximada de los lenguajes de programación, y nos
dice que esos primeros programas estaban escritos en lenguajes del
tipo hacer ahora. Esos lenguajes obligan al programador "a imaginar
todos los detalles acerca de cómo el programa va a pasar de un
estado a otro, en cada momento". Es difícil concebir como un
programa así "podría hacer algo que su programador no hubiera
pensado".
Pero los investigadores desarrollaron otras maneras de programar.
"Por ejemplo, el GPS creado por Newell, Shaw y Simon permite
describir procesos en términos de declaraciones como 'si la
diferencia entre el estado que tiene y el que desea es de clase D,
entonces trate de cambiar la diferencia mediante el método M'.
Llamaremos a esto programación de hacer cuando." Los programas
escritos en esta clase de lenguajes deben decir lo que necesita
suceder en cada estado, pero el programador no tiene que saber por
adelantado cuándo va ocurrir cada cosa. Es difícil decir en este
caso que todo lo que hace la computadora lo previó el programador.
En efecto, la situación es semejante a la educación que recibe un
ser humano: el maestro dota al alumno de una serie de recursos,
pero cómo los llegue a aplicar es algo que no puede preverse.
(GUTIÉRREZ Y CASTRO 87 )
Un tercer nivel de capacidad se da con lenguajes, como LISP,
capaces de usar recursión general: "Estos lenguajes son todavía más
expresivos que los de hacer cuando, porque sus programadores no
tienen que prever con claridad ni las clases de estados que puedan
ocurrir ni cuando habrán de ocurrir; el programa únicamente
coacciona la manera en que los estados y las estructuras se van a
relacionar unos con otros. Podemos llamarlos lenguajes coactivos."
(MINSKY 82) En este caso no se le dice a la computadora qué debe hacer sino a
qué constreñimientos debe someterse en lo que hace. En estos
lenguajes no hay realmente previsión del programador sobre lo que
haga la máquina. El programador se coloca en un nivel abstracto,
donde el procedimiento se define de manera general, para toda clase
de datos, y la computadora se encarga de realizar los detalles. La
situación es semejante a la de un empleado inteligente al que
confiamos una misión, especificándola solamente en sus grandes
rasgos.
Finalmente, y como una extrapolación hacia el futuro, Minsky nos
habla de lenguajes posibles pero todavía no operacionales de hacer
algo razonable. Tales lenguajes permitirán eventualmente a las
máquinas tener "sentido común", como los seres humanos. Sin
embargo, no se perfilan todavía en el horizonte. Los lenguajes de
que vamos hablar, a cuya familia pertenece LOGO, son todos de la
tercera clase, los llamados lenguajes de inteligencia artificial.
Se llaman así porque se usan preferentemente en aplicaciones que
producen en las máquinas comportamientos que, si los ejecutaran
seres humanos, diríamos que requieren inteligencia.
Podemos distinguir tres grandes estilos o subfamilias en los
lenguajes de inteligencia artificial. Todos ellos están formados
por lenguajes de tipo coactivo y disponen del poder de la
recursión. Pero tienen cada uno de ellos características especiales
que les singularizan y les han dado popularidad entre sus
practicantes. Voy a tratar de presentar a ustedes un ejemplo de
cada estilo, advirtiendo que, aunque cada subfamilia tiene sus
lenguajes específicos, los diferentes estilos de programación son,
hasta cierto punto reproducibles en todos los grandes lenguajes que
los representan por antonomasia, como veremos al final.
Los tres estilos de programación a que quiero referirme son los
siguientes: programación funcional, programación relacional, y
programación por objetos. El lenguaje más representativo del estilo
funcional es LISP; LOGO, por su identificación con LISP, cae de
lleno dentro de este estilo. El lenguaje más representativo del
estilo relacional es PROLOG. El lenguaje más representativo del
estilo de programación por objetos es SMALLTALK, pero existen
varios dialectos de LISP que permiten programar en esta forma.
Los lenguajes funcionales se caracterizan porque sus programas son
expresiones simbólicas que se evalúan y producen un resultado, en
el mismo sentido en que la expresión simbólica "(7 + 5)", al ser
evaluada, produce "12". Esta característica contrasta notablemente
con lo que ocurre con lenguajes del tipo imperativo, como FORTRAN
o BASIC, en que un programa se define como una secuencia de
comandos a ejecutar. Solo ocasionalmente una expresión en LISP, u
otro lenguaje funcional, produce un "efecto", como distinto del "valor"
de la expresión evaluada, por ejemplo, la activación de un
periférico de la computadora o la asociación de un término con su
significado en una tabla de definiciones. En el caso de LOGO, el
aspecto de efecto está enfatizado, dado el sesgo del programa,
hacia las aplicaciones gráficas. Una nota importante que debe
subrayarse es que en los lenguajes funcionales puros, los
argumentos de las funciones pueden a su vez ser funciones, las
cuales deben ser evaluadas con anterioridad para que la función de
mayor nivel pueda obtener sus argumentos efectivos con los cuales
deberá evaluarse.
Voy a ilustrar el estilo funcional con un ejemplo del lenguaje
funcional por antonomasia, LISP, creado por John McCarthy en los
años cincuenta, específicamente para servir a la comunidad de
investigadores de inteligencia artificial. (MCCARTHY 60)Al mismo tiempo
ilustraré el poderoso recurso programático de la recursión, en la
solución de un problema no trivial. Finalmente, este ejemplo nos
servirá de ilustración para la tesis de Minsky de que, en los
lenguajes coactivos, el programador no puede, ni necesita, prever
todas las acciones concretas del programa.
Había una vez un monasterio en Hanoi que en su patio central tenía
tres mástiles. En uno de los mástiles, de color azul, había doce
discos, todos de distinto tamaño, uno encima del otro, siempre el
más pequeño encima del más grande. Los otros dos mástiles, rojo y
verde, estaban vacíos. Los monjes debían pasar cada día un solo
disco de un mástil a otro, cumpliendo con la condición de que nunca
un disco más grande debía caer sobre uno más pequeño. Los monjes
asegurarían su salvación si lograban que todos los discos llegasen a
estar en el mástil rojo. Lamentablemente, los monjes murieron antes de
concluir su empresa piadosa, pues no encontraron las 4.095 movidas
individuales de disco necesarias para resolver el acertijo.
En el gráfico adjunto representamos una versión más simple del
problema: se trata de pasar una torre de solamente cinco discos.
Si intentamos solucionar este acertijo con un lenguaje de hacer
ahora, o incluso con uno de hacer cuando, el problema es
dificilísimo de solucionar. En cambio, usando un lenguaje coactivo
podemos fácilmente definir, en forma recursiva, los
constreñimientos que deben cumplirse para que se dé una solución.
La maniobra es equivalente a ver el problema como un proceso de mover torres,
en vez de un problema de mover discos. Se nos dirá que las reglas del juego no.
permiten mover torres, y es verdad. Pero precisamente se trata aquí de definir un
procedimiento para mover torres que no infrinja las reglas.
La versión simplificada del problema, para torres de cinco discos,
se expresa con la siguiente invocación en LISP:
que quiere decir "pasar una torre de tamaño 5 tomando como mástil
de origen el mástil azul, como mástil de destino el mástil rojo y
como mástil de ayuda el mástil verde" (el mástil de ayuda es
necesario para poner los discos que hay que apartar
provisionalmente para llegar al próximo disco más grande). El valor
de la función será, por supuesto, la lista de jugadas legítimas
para solucionar el acertijo.
Veamos cómo se razona coactivamente para idear este programa.
Nuestro problema puede reducirse a tres problemas más pequeños:
pasar una subtorre al mástil de ayuda, pasar el disco mayor al
mástil de destino, y volver a pasar la subtorre, esta vez al disco
de destino. Todo esto supone, desde luego, que se pueda encontrar
una manera de pasar la subtorre sin infringir las reglas. Pero como
lo mismo que acabamos de decir podemos decirlo de la subtorre,
hasta llegar a una torre de tamaño nulo, es claro que podemos
lograr la solución buscada.
He aquí como queda nuestro programa en un lenguaje funcional
(COMMON LISP). Damos por supuesto que existe una definición previa
de la función "pasar-disco", que simplemente pone los nombres de los
mástiles de origen y destino en una doble lista(1).
La traducción a
LOGO es trivial.
Como el programa es recursivo, la computadora tendrá que llamarlo
muchas veces desde dentro del mismo programa, y cada vez usará
diferentes mástiles como los mástiles de origen, destino y ayuda.
En cada grupo de llamadas cambiará además el tamaño de la torre
secundaria que la computadora deberá pasar, la cual irá siendo
tanto más pequeña conforme más avance la recursión. El valor
reportado por la función será una lista de listas, cada una de las
cuales representará la movida de un disco de un mástil a otro.
Lo importante en todo esto es que el problema se nos aclara si
pensamos coactivamente, definiendo en abstracto los
constreñimientos que tienen que cumplirse, sabiendo que la
recursión llegará a un punto en que no necesite hacer nada para
resolver el problema más simple. En este caso, ese problema más
simple es pasar una torre con cero discos, que por supuesto es muy
fácil: no hay que hacer nada en absoluto.
Algo interesante de este programa es que la computadora lo obedece
moviendo siempre un solo disco por vez, como lo exigen las reglas,
pero esas movidas son inesperadas para el programador, que no tiene
idea del orden en que efectivamente se tienen que ir moviendo los
discos: lo único expresado en el programa es el constreñimiento
recursivo que exige pasar un disco en medio de dos movidas de
torre. Lo sorprendente es que, establecidos esos constreñimientos,
el programa resuelve el problema sin hacer nunca una jugada
innecesaria; además, es capaz de pasar torres de cualquier tamaño.
Confirmando el decir de Minsky, los detalles de la ejecución no son
establecidos directamente por el programador.
El segundo estilo de programación de inteligencia artificial es el
estilo relacional. El mejor ejemplo es PROLOG, un lenguaje simple
y poderoso desarrollado en los años setenta por varios
investigadores europeos que aplicaron descubrimientos de otros
lógicos. (KOWALSKI 82)
Es también un lenguaje recursivo. Se diferencia de
LISP y los otros lenguajes funcionales, en que es más explícito en
la definición de los constreñimientos, incorporando dentro de la
expresión que se evalúa, un lugar para el resultado de la
computación (en contraste con la función, cuyo valor es simplemente
reportado). Una típica expresión en PROLOG es
que al ser evaluada podría dar como resultado (dependiendo del
resto del programa) las ecuaciones
Se dice de PROLOG que es equivalente a la lógica de predicados,
usada como lenguaje de programación. En esta perspectiva, se da una
considerable simplificación de las tareas del programador. En
efecto, para programar debemos normalmente primero especificar
nuestro problema usando la lógica. Una vez hecho esto, nuestro
trabajo consiste en traducir la especificación al código particular
del lenguaje de programación. De ahí la siguiente ecuación:
Pero si especificamos en lógica, y la lógica que usamos es la
versión llamada de cláusulas Horn, que tiene la misma sintaxis de
PROLOG, de hecho no necesitamos codificar. Sin embargo, se reconoce
que los programas especificados en pura lógica, incluso usando
solamente cláusulas Horn, no son todo lo eficientes posible: es
necesario entonces agregar un toque de decisión de control, como
por ejemplo cambiar el orden de las cláusulas o de los elementos de
las cláusulas, a fin de lograr una ejecución eficiente. Así pues,
se nos ofrece la ecuación:
Sea esto como sea, lo cierto es que PROLOG ofrece dos
características muy atractivas para el programador de alto nivel.
Por una parte, la ejecución de un programa se identifica con un
procedimiento deductivo, lo que permite muy fácilmente dotar de
capacidades intelectuales a nuestros programas. Por otra parte, las
expresiones relacionales, con ayuda del uso de variables lógicas,
permiten a los programas PROLOG ser usados "en varias direcciones",
como por ejemplo si un mismo programa nos sirviera para sumar y
restar. En el caso de la expresión mencionada arriba, si nuestro
programa consiste únicamente de la expresión
es posible evaluar la pregunta
pero también la pregunta muy diferente
con exitosos resultados.
Las capacidades deductivas del lenguaje las podemos fácilmente
ilustrar si agregamos a nuestro programa las dos cláusulas
siguientes:
donde las letras mayúsculas son variables lógicas. Si ahora
preguntamos:
donde la palabra con mayúscula es una variable, el sistema
responderá con la ecuación:
Todo esto nos hace ver que PROLOG es un lenguaje muy plástico,
susceptible de diversas interpretaciones. Puede entenderse:
llamadas cláusulas de hechos, podría funcionar como una base de
datos ordinaria. Pero al tener también cláusulas del tipo
llamadas cláusulas de reglas, en realidad se constituye en algo muy
superior, a saber, una base de conocimientos. La diferencia es que
la base de datos solo puede contestar reportando los hechos que
tiene almacenados; la base de conocimientos, además, es capaz de
contestar reportando esos hechos, y además todo lo que se puede
deducir de ellos con ayuda de las reglas.
Los lenguajes de programación por objetos, por su parte, tienen un
estilo muy particular, ya que tratan de representar a los objetos
del mundo con que lidian nuestros programas con entidades
computacionales cerradas sobre sí mismas, llamadas también objetos.
Estos objetos computacionales son al mismo tiempo datos y
procedimientos. En cuanto datos, los objetos se definen por
variables privadas que no son inspeccionables desde fuera de ellos,
y que conservan sus valores a través del tiempo. En cuanto
procedimientos, los objetos se definen como métodos, que solo ellos
pueden aplicar como reacción a ciertos mensajes que reciben. La
computación se realiza por medio de intercambio de mensajes entre
los distintos objetos. Los mensajes suelen consistir en
requerimientos para que un objeto aplique uno de sus métodos a sí
mismo (por ejemplo para variar el estado de sus variables privadas)
o a otros objetos (enviándoles un mensaje); pero puede también
suceder que un mensaje consista en un objeto que se envía a otro
objeto. Generalmente los objetos se organizan en clases
jerárquicamente organizadas, de modo que todos los miembros de una
clase comparten ciertos atributos y que los miembros de una clase
subalterna heredan atributos de la clase subalternante (por
ejemplo, una clase de objetos puede ser "vehículos" y otra
"automóviles", y los objetos de la segunda heredar de la primera
clase el atributo de servir para transporte o necesitar
combustible). (SHRIVER 87)
SMALLTALK fue desarrollado por el Learning Research Group del
Centro de Investigación de la Compañía Xerox, en Palo Alto,
California, al comienzo de los años setenta. Sus ideas principales
se deben a Alan Kay. Las primeras realizaciones del lenguaje las
produjo Dan Ingalls (primera persona que programó ventanas
superpuestas en ambientes de cómputo). Otra persona que colaboró de
manera importante en el proyecto fue Adele Golberg, del mismo
laboratorio. (KAEHLER 86)
El desarrollo de SMALLTALK fue guiado por la experiencia con el
sistema FLEX, creado por el mismo Alan Kay a fines de la década de
los sesenta, y por ideas centrales del lenguaje SIMULA,
desarrollado a la mitad de los sesenta por Ole-Johan Dahl y
Kristen Nygaard del Centro Noruego de Cómputo en Oslo. El esfuerzo
paralelo realizado en MIT para la creación de LOGO tuvo también
innegable influencia. Su motivación principal fue proporcionar a
los niños un ambiente de computación gráfico y concreto. Sin
embargo, el lenguaje ha resultado ser de uso general, para toda
clase de aplicaciones.
Aunque existan lenguajes específicos para este estilo objetivo de
programación, es interesante subrayar que el mismo estilo de
programación puede lograrse con un lenguaje funcional como LISP, en
varios de sus dialectos. En particular, admiro la tersura con que
este tipo de programación puede realizarse en uno de los dialectos
más puros y elegantes de esta gran familia: el lenguaje SCHEME,
otro producto del MIT. Me voy a permitir presentar un ejemplo
bastante elaborado de programación por objetos, usando este terso
lenguaje.
Se trata de crear un sistema de cuentas corrientes en un banco.
Será necesario poder abrir cuentas a nombre de diferentes personas,
emitir chequeras y libretas de depósito para cada cuenta, y
realizar operaciones de giro y de depósito. El sistema deberá
mantener, como es lógico, una contabilidad separada para cada
cuenta, pero deberá ser capaz de recibir depósitos por medio de
diversas libretas de depósito de la misma cuenta, así como aceptar
cheques de distintas chequeras de la misma cuenta, todo ello sin
confundirse. Además, cada cuentacorrentista deberá poder consultar
el saldo de su cuenta en cualquier momento. El problema no es
trivial, ya que su solución nos permitirá realizar todas las
operaciones que normalmente asociamos con las cuentas corrientes.
El programa en SCHEME que realiza todo eso, en el estilo de la
programación por objetos, es el siguiente:
La palabra "lambda" en LISP es semejante a la primitiva "para" en LOGO,
en el sentido de que identifica un procedimiento. Difiere de ella,
sin embargo, en cuanto a que en LOGO no se puede usar "para" dentro
de un procedimiento(2); mientras que en SCHEME la posibilidad de usar
"lambda" dentro de la definición de otro procedimiento, con cualquier
nivel de anidamiento, resulta crucial para producir la programación
por objetos.
Explicaremos el programa por medio de su uso. Ante todo, el
programa permitirá crear una cuenta corriente a mi nombre. Eso lo
hago asignando a la variable "cuenta-claudio" un objeto que voy a
crear con el procedimiento "haga-cuenta". Cuando uno abre una cuenta
debe hacer un depósito inicial, de ahí que el procedimiento tenga
un parámetro, "saldo":
De aquí en adelante, podemos contar con que "cuenta-claudio" es una
cuenta corriente a mi nombre, que mantendrá un saldo determinado.
La cuenta es un objeto de la clase "cuenta", que hereda todos los
atributos (capacidades) de esa clase; pero que al mismo tiempo
difiere de todos los otros objetos de la misma clase por el valor
de su variable privada, a saber, "saldo". Así por ejemplo, la
Fundación Omar Dengo puede abrir otra cuenta, con
y el objeto cuenta-fod dispondrá de su propio saldo que no tiene
nada que ver con el mío. Literalmente "cuenta-fod" ni siquiera conoce
la existencia del saldo de "cuenta-claudio", y "cuenta-claudio" ignora
totalmente la existencia del saldo de "cuenta-fod".
¿En qué consiste el objeto "cuenta-claudio"? Como explicamos arriba,
los objetos se definen por estados internos —variables privadas— y
métodos —procedimientos privados—. En el caso de "cuenta-claudio", el
estado interno es "saldo", con un valor inicial de 100.000. El método
que define el objeto es el siguiente:
Nótese
que el objeto que hemos almacenado en "cuenta-claudio" es un
procedimiento, y en consecuencia, podrá ser activado, una o muchas
veces, en el futuro; por cierto que es un procedimiento con un solo
argumento, a saber, "mensaje", el cual deberá dársele cada vez que
invoquemos el objeto (activemos el procedimiento). Y esto es
precisamente la siguiente cosa que vamos a experimentar. Vamos a invocar
el objeto "cuenta-claudio" para producir una chequera de mi cuenta
corriente:
A partir de este momento, la variable "chequera-claudio" contiene un
objeto de la clase "chequera" que me permitirá retirar fondos de mi
cuenta. Nótese que en la definición de "chequera-claudio" se usa el
procedimiento "cuenta-claudio" al cual le enviamos el mensaje "retiro".
Vemos aquí un ejemplo de intercambio de mensajes entre objetos.
Cuando "cuenta-claudio" recibe ese mensaje produce, de conformidad
con su programa interno, el siguiente objeto que reporta como
valor:
Tal objeto, que es un procedimiento (nótese que comienza con la
palabra lambda), formará ahora el contenido de la variable
"chequera-claudio". El procecimiento tendrá acceso a la variable
privada "saldo" que esconde el valor actual del saldo de la cuenta
corriente. Tal procedimiento podrá activarse en su oportunidad,
para producir los resultados buscados con la operación de la
chequera. Usemos ahora la chequera que hemos creado:
En ese momento se supone que yo he recibido el dinero correspondiente, y la computadora imprime el saldo que queda en mi cuenta
después de este retiro, a saber, 99.000.
Más tarde, mi esposa me pide que le dé una chequera sobre mi
cuenta, para usarla en los gastos de la casa. La produzco con la
siguiente invocación:
Marlene va de compras, y en cierto momento considera oportuno
averiguar el saldo actual de mi cuenta. Lo obtiene con la siguiente
invocación:
que le responde imprimiendo el número 99.000. Nótese que dos
chequeras diferentes, pero de la misma cuenta, dan resultados
perfectamente congruentes con la realidad del movimiento de la
cuenta.
Mientras tanto, la FOD considera oportuno emitir una libreta de
depósitos sobre su cuenta, lo que hace con la invocación:
Esta invocación creará un objeto de la clase "libreta de depósito"
cuyo contenido será el procedimiento
que tiene acceso al valor de la variable privada "saldo" de la
respectiva cuenta corriente.
Ahora se puede hacer un depósito adicional en la cuenta, por
5.000.000, con la invocación:
lo que le producirá la impresión del saldo correcto, 10.000.000.
Por supuesto, no habrá confusión con el saldo de "cuenta-claudio".
Lo más sorprendente de este programa —además de que funcione del
todo— es su brevedad, ya que no ocupa más que una pantalla de
texto. Lo cual es índice revelador del poder del estilo de
programación por objetos.
Hemos presentado una visión muy sucinta de los tres estilos de
programación en inteligencia artificial. Nos resta subrayar un
hecho sumamente interesante, a saber, que —por lo menos en
principio— es posible programar usando cualquiera de los tres
estilos desde cualquiera de los tres lenguajes principales que
epitomizan cada estilo: LISP, PROLOG, SMALLTALK. Puedo hablar de
primera mano en el caso de LISP, pues he desarrollado un lenguaje
escrito en LISP, llamado GOAL, que hace todo lo que hace PROLOG (y
un poco más) en el estilo relacional. Además, acabamos de ver cómo
un dialecto de LISP, SCHEME, se presta directa y elegantemente a la
programación por objetos. Conozco de oídas intentos de programar
desde PROLOG o SMALLTALK en los estilos diferentes al propio del
lenguaje. La idea general, que cubre todos los cruces entre estilos
y lenguajes, es la siguiente.
LISP es un lenguaje funcional. En consecuencia, sus unidades de
acción son funciones. PROLOG, como lenguaje relacional, tiene
unidades de acción que son relaciones. SMALLTALK, como lenguaje
objetivo, tiene como sus unidades de acción objetos. Una
estrategia razonable para integrar los estilos dentro de uno de los
lenguajes consiste en identificar una de sus unidades de acción con
la invocación al estilo diverso deseado.
Más concretamente: estando en ambiente LISP debo poder invocar una
función que se llame —digamos— PROLOG, dándole como argumento el
problema que quiero resolver en estilo relacional; el valor
reportado por la función sería la misma fórmula de la invocación
pero con todas sus variables lógicas sustituidas por los
respectivos valores encontrados por el intérprete relacional.
Estando en ambiente PROLOG, debo poder ejecutar una relación que se
llame —digamos— LISP, uno de cuyos argumentos sea una expresión
funcional y otro sea el valor de la misma, que produciría el
intérprete funcional.
Estando en un ambiente de programación por objetos, debiera poder
dar un mensaje a un objeto llamado LISP, o PROLOG, que consista
precisamente en una invocación que debe ser ejecutada por el
respectivo intérprete.
LISP no tiene problema en trabajar con objetos, y varios dialectos
del lenguaje han incorporado ya la programación por objetos,
llamados sabores en varias de las implantaciones (en ZETA LISP, por
ejemplo, el objeto más corriente es llamado "vainilla", por alguna
extraña razón).
En cuanto a la posibilidad de llamar a SMALLTALK desde PROLOG,
puede hacerse de manera análoga a la llamada a LISP desde un
ambiente funcional.
De más está decir que todas estas llamadas de un estilo a otro
pueden multiplicarse recursivamente, dando un resultado bastante
alambicado que me gusta identificar —por razones obvias— como "club
sandwich". Por experiencia de mi propia investigación en
inteligencia artificial con el lenguaje GOAL, sé que esta forma de
programar es extraordinariamente poderosa.
En un artículo reciente, Sherry Turkle y Seymour Papert examinan la
tendencia hacia lo que ellos llaman pluralismo epistemológico como
un rasgo deseable de la cultura computacional. La manera de
combinar los distintos estilos de programación que hemos explicado
aquí se inspira en el mismo espíritu. De un modo especial, estos
autores resaltan la importancia de la programación por objetos, la
cual les parece eminentemente compatible con la orientación de
muchas personas que favorecen un acercamiento concreto a la
computación. Tal estilo tiene la ventaja, para los usos en que
estamos interesados —en palabras de Turkle y Papert— de emplear
como unidad de pensamiento "la creación y modificación de agentes
dentro de un programa para el cual las metáforas naturales son
biológicas y sociales en vez de algebraicas. Tales agentes son
capaces de interactuar entre sí como los actores de una obra de
teatro" o, podríamos agregar, de un juego de niños. (TURKLE 90)
Nota 1:
La razón de la doble lista (una lista dentro de otra) estriba en que la lista interior representa una jugada particular,
siendo un par ordenado
cuyo primer miembro es el nombre del mástil de origen y el segundo el del destino. La
lista exterior, por su parte, es solo un envoltorio provisional que se va unificando con los
otros factores de la operación de concatenación para formar una gran "bolsa", a saber la lista
reportada como resultado final. Esta lista resultante contendrá
todos los pares ordenados que representan las jugadas en secuencia correcta.
La función "concatenar" admite en el ejemplo tres argumentos, que deben ser también listas de listas de jugadas. Las listas
vacías reportadas por la función en las puntas de la recursión no afectan el resultado, naturalmente.
Nota agregada en 2002 por prurito de claridad.
Nota 2:
Esta restricción de LOGO hace que sea imposible programar por objetos dentro de su ambiente.
Nota 3:
Nótese que LOGO no puede hacer algo equivalente: al no permitirse el uso de "para" dentro de la definición de un procedimiento, no es posible que el valor reportado por un procedimiento de LOGO sea otro procedimiento.