Self es un lenguaje de programación de propósito general , de alto nivel y orientado a objetos basado en el concepto de prototipos . Self comenzó como un dialecto de Smalltalk , con tipado dinámico y compilación just-in-time (JIT) con el enfoque basado en prototipos para objetos: se utilizó por primera vez como un sistema de prueba experimental para el diseño de lenguajes en los años 1980 y 1990. En 2006, Self todavía se estaba desarrollando como parte del proyecto Klein, que era una máquina virtual Self escrita completamente en Self. La última versión, 2024.1, se lanzó en agosto de 2024. [2]
En la investigación de Self se desarrollaron y mejoraron varias técnicas de compilación justo a tiempo, ya que eran necesarias para permitir que un lenguaje orientado a objetos de muy alto nivel funcionara a hasta la mitad de la velocidad de C optimizado . Gran parte del desarrollo de Self se llevó a cabo en Sun Microsystems , y las técnicas que desarrollaron se implementaron más tarde para la máquina virtual HotSpot de Java .
En algún momento se implementó una versión de Smalltalk en Self que, como era capaz de utilizar JIT, también ofrecía un rendimiento extremadamente bueno. [3]
Self fue diseñado principalmente por David Ungar y Randall Smith en 1986 mientras trabajaban en Xerox PARC . Su objetivo era avanzar en el estado del arte en la investigación de lenguajes de programación orientados a objetos, una vez que Smalltalk -80 fuera lanzado por los laboratorios y comenzara a ser tomado en serio por la industria. Se trasladaron a la Universidad de Stanford y continuaron trabajando en el lenguaje, creando el primer compilador Self funcional en 1987. Luego, el enfoque cambió a trabajar para crear un sistema completo para Self, en contraste con solo el lenguaje.
El primer lanzamiento público fue en 1990, y al año siguiente el equipo se trasladó a Sun Microsystems donde continuaron trabajando en el lenguaje. Siguieron varios lanzamientos nuevos hasta que cayó en gran medida inactivo en 1995 con la versión 4.0. En 2006, se lanzó la versión 4.3, para Mac OS X y Solaris . En 2010, un grupo que incluía a algunos del equipo original y programadores independientes desarrolló una nueva versión, la versión 4.4, [4] para Mac OS X y Linux , al igual que todas las versiones posteriores. En enero de 2014, se lanzó una continuación, la 4.5, [5] y tres años después, se lanzó la versión 2017.1 en mayo de 2017.
El entorno de construcción de la interfaz de usuario Morphic fue desarrollado originalmente por Randy Smith y John Maloney para el lenguaje de programación Self. [6] Morphic ha sido portado a otros lenguajes de programación notables, incluidos Squeak , JavaScript , Python y Objective-C .
Self también inspiró varios lenguajes basados en sus conceptos. Los más notables, quizás, fueron NewtonScript para Apple Newton y JavaScript utilizado en todos los navegadores modernos. Otros ejemplos incluyen Io , Lisaac y Agora . El sistema de objetos distribuidos de IBM Tivoli Framework, desarrollado en 1990, fue, en el nivel más bajo, un sistema de objetos basado en prototipos inspirado en Self.
Los lenguajes OO tradicionales basados en clases se basan en una dualidad profundamente arraigada:
Por ejemplo, supongamos que los objetos de la Vehicle
clase tienen un nombre y la capacidad de realizar varias acciones, como conducir hasta el trabajo y entregar materiales de construcción . Bob's car
es un objeto particular (instancia) de la clase Vehicle
, con el nombre "El auto de Bob". En teoría, uno puede entonces enviar un mensaje a Bob's car
, diciéndole que entregue materiales de construcción .
Este ejemplo muestra uno de los problemas de este enfoque: el coche de Bob, que resulta ser un coche deportivo, no puede transportar y entregar materiales de construcción (en ningún sentido significativo), pero esta es una capacidad que Vehicle
se modela para que tengan los s. Un modelo más útil surge del uso de subclasificación para crear especializaciones de Vehicle
; por ejemplo Sports Car
y Flatbed Truck
. Solo los objetos de la clase Flatbed Truck
necesitan proporcionar un mecanismo para entregar materiales de construcción ; los coches deportivos, que no son adecuados para ese tipo de trabajo, solo necesitan conducir rápido . Sin embargo, este modelo más profundo requiere más conocimiento durante el diseño, conocimiento que puede que solo salga a la luz a medida que surjan los problemas.
Este problema es uno de los factores que motivan la creación de prototipos . A menos que se pueda predecir con certeza qué cualidades tendrá un conjunto de objetos y clases en un futuro lejano, no se puede diseñar una jerarquía de clases de forma adecuada. Con demasiada frecuencia, el programa acabaría necesitando comportamientos añadidos y sería necesario rediseñar (o refactorizar ) secciones del sistema para dividir los objetos de una forma diferente. [ cita requerida ] La experiencia con los primeros lenguajes orientados a objetos, como Smalltalk, mostró que este tipo de problema surgía una y otra vez. Los sistemas tendían a crecer hasta un punto en el que luego se volvían muy rígidos, a medida que las clases básicas que se encontraban muy por debajo del código del programador se volvían simplemente "incorrectas". Sin alguna forma de cambiar fácilmente la clase original, podrían surgir problemas graves. [ cita requerida ]
Los lenguajes dinámicos como Smalltalk permitían este tipo de cambio a través de métodos bien conocidos en las clases; al cambiar la clase, los objetos basados en ella cambiarían su comportamiento. Sin embargo, tales cambios debían realizarse con mucho cuidado, ya que otros objetos basados en la misma clase podrían estar esperando este comportamiento "incorrecto": "incorrecto" a menudo depende del contexto. (Esta es una forma del problema de la clase base frágil ). Además, en lenguajes como C++ , donde las subclases se pueden compilar por separado de las superclases, un cambio en una superclase puede, de hecho, romper los métodos de subclase precompilados. (Esta es otra forma del problema de la clase base frágil, y también una forma del problema de la interfaz binaria frágil ).
En Self y otros lenguajes basados en prototipos, se elimina la dualidad entre clases e instancias de objetos.
En lugar de tener una "instancia" de un objeto que se basa en alguna "clase", en Self se hace una copia de un objeto existente y se lo modifica. Por ejemplo, Bob's car
se crearía haciendo una copia de un objeto "Vehicle" existente y luego agregando el método drive fast , modelando el hecho de que resulta ser un Porsche 911. Los objetos básicos que se usan principalmente para hacer copias se conocen como prototipos . Se afirma que esta técnica simplifica enormemente el dinamismo. Si un objeto existente (o un conjunto de objetos) demuestra ser un modelo inadecuado, un programador puede simplemente crear un objeto modificado con el comportamiento correcto y usarlo en su lugar. El código que usa los objetos existentes no se modifica.
Los objetos propios son una colección de "ranuras". Las ranuras son métodos de acceso que devuelven valores y colocar dos puntos después del nombre de una ranura establece el valor. Por ejemplo, para una ranura llamada "nombre",
mi nombre de persona
devuelve el valor en nombre, y
mi nombre personal : 'foo'
lo establece.
Self, al igual que Smalltalk, utiliza bloques para el control de flujo y otras funciones. Los métodos son objetos que contienen código además de espacios (que utilizan para argumentos y valores temporales), y se pueden colocar en un espacio de Self como cualquier otro objeto: un número, por ejemplo. La sintaxis sigue siendo la misma en ambos casos.
Tenga en cuenta que en Self no hay distinción entre campos y métodos: todo es una ranura. Dado que el acceso a las ranuras a través de mensajes constituye la mayor parte de la sintaxis en Self, muchos mensajes se envían a "self" y "self" se puede omitir (de ahí el nombre).
La sintaxis para acceder a las ranuras es similar a la de Smalltalk. Hay tres tipos de mensajes disponibles:
receiver slot_name
receiver + argument
receiver keyword: arg1 With: arg2
Todos los mensajes devuelven resultados, por lo que el receptor (si está presente) y los argumentos pueden ser en sí mismos el resultado de otros mensajes. Si se coloca un punto después de un mensaje, Self descartará el valor devuelto. Por ejemplo:
'¡Hola, mundo!' impresión .
Esta es la versión propia del programa "¡Hola, mundo!" . La '
sintaxis indica un objeto de cadena literal. Otros literales incluyen números, bloques y objetos generales.
La agrupación se puede forzar mediante el uso de paréntesis. En ausencia de una agrupación explícita, se considera que los mensajes unarios tienen la mayor precedencia, seguidos de los binarios (agrupación de izquierda a derecha) y las palabras clave, que tienen la menor. El uso de palabras clave para la asignación daría lugar a algunos paréntesis adicionales en los casos en que las expresiones también tuvieran mensajes de palabras clave, por lo que para evitarlo, Self requiere que la primera parte de un selector de mensajes de palabras clave comience con una letra minúscula y las partes posteriores comiencen con una letra mayúscula.
válido: base inferior entre: ligadura inferior + altura Y: base superior / factor de escala .
se puede analizar sin ambigüedades y significa lo mismo que:
válido: (( base inferior ) entre: (( ligadura inferior ) + altura ) y: (( base superior ) / ( factor de escala ))) .
En Smalltalk-80, la misma expresión se escribiría así:
válido := base propia inferior entre: ligadura propia inferior + altura propia y: base propia superior / factor de escala propio .
asumiendo base
que , ligature
, height
y scale
no eran variables de instancia de self
sino que, de hecho, eran métodos.
Consideremos un ejemplo un poco más complejo:
labelWidget copia etiqueta: '¡Hola, mundo!' .
Hace una copia del objeto "labelWidget" con el mensaje de copia (no hay atajo esta vez), luego le envía un mensaje para que coloque "Hola, mundo" en la ranura llamada "etiqueta". Ahora, para hacer algo con él:
( escritorio activeWindow ) dibujar: ( labelWidget copiar etiqueta: '¡Hola, mundo!' ) .
En este caso, (desktop activeWindow)
primero se ejecuta el comando, devolviendo la ventana activa de la lista de ventanas que el objeto de escritorio conoce. A continuación (leyendo de dentro hacia fuera, de izquierda a derecha), el código que examinamos antes devuelve el widget de etiqueta. Por último, el widget se envía a la ranura de dibujo de la ventana activa.
En teoría, cada objeto Self es una entidad independiente. Self no tiene clases ni metaclases. Los cambios en un objeto en particular no afectan a ningún otro, pero en algunos casos es deseable que así sea. Normalmente, un objeto solo puede entender mensajes correspondientes a sus ranuras locales, pero al tener una o más ranuras que indiquen objetos padre , un objeto puede delegar cualquier mensaje que no entienda por sí mismo al objeto padre. Cualquier ranura puede convertirse en un puntero padre agregando un asterisco como sufijo. De esta manera, Self maneja tareas que usarían herencia en lenguajes basados en clases. La delegación también se puede utilizar para implementar características como espacios de nombres y alcance léxico .
Por ejemplo, supongamos que se define un objeto llamado "cuenta bancaria", que se utiliza en una aplicación de contabilidad sencilla. Por lo general, este objeto se crearía con los métodos dentro, tal vez "depósito" y "retiro", y cualquier ranura de datos que necesiten. Este es un prototipo, que solo es especial en la forma en que se utiliza, ya que también resulta ser una cuenta bancaria completamente funcional.
Al crear un clon de este objeto para la "cuenta de Bob", se creará un nuevo objeto que comienza exactamente como el prototipo. En este caso, hemos copiado las ranuras, incluidos los métodos y los datos. Sin embargo, una solución más común es crear primero un objeto más simple llamado objeto de rasgos que contiene los elementos que normalmente se asociarían con una clase.
En este ejemplo el objeto "cuenta bancaria" no tendría el método de depositar y retirar, pero tendría como padre un objeto que sí lo tuviera. De esta forma se pueden hacer muchas copias del objeto cuenta bancaria, pero aún así podemos cambiar el comportamiento de todas ellas modificando las ranuras en ese objeto raíz.
¿En qué se diferencia de una clase tradicional? Consideremos el significado de:
myObject padre: someOtherObject .
Este extracto cambia la "clase" de myObject en tiempo de ejecución al cambiar el valor asociado con la ranura 'parent*' (el asterisco es parte del nombre de la ranura, pero no de los mensajes correspondientes). A diferencia de la herencia o el alcance léxico, el objeto delegado se puede modificar en tiempo de ejecución.
Los objetos de Self se pueden modificar para incluir espacios adicionales. Esto se puede hacer usando el entorno de programación gráfica o con el primitivo '_AddSlots:'. Un primitivo tiene la misma sintaxis que un mensaje de palabra clave normal, pero su nombre comienza con el carácter de guión bajo. El primitivo _AddSlots se debe evitar porque es un remanente de las primeras implementaciones. Sin embargo, lo mostraremos en el ejemplo a continuación porque hace que el código sea más corto.
Un ejemplo anterior trataba sobre la refactorización de una clase simple llamada Vehicle para poder diferenciar el comportamiento entre autos y camiones. En Self, esto se lograría con algo como esto:
_ AddSlots: ( | vehículo <- ( | padre * = rasgos clonables| ) | ) .
Dado que el receptor de la primitiva '_AddSlots:' no se indica, es "self". En el caso de expresiones escritas en el indicador, se trata de un objeto llamado "lobby". El argumento para '_AddSlots:' es el objeto cuyos slots se copiarán al receptor. En este caso, se trata de un objeto literal con exactamente un slot. El nombre del slot es 'vehicle' y su valor es otro objeto literal. La notación "<-" implica un segundo slot llamado 'vehicle:' que se puede utilizar para cambiar el valor del primer slot.
El "=" indica una ranura constante, por lo que no hay un 'parent:' correspondiente. El objeto literal que es el valor inicial de 'vehicle' incluye una única ranura para que pueda comprender mensajes relacionados con la clonación. Un objeto verdaderamente vacío, indicado como (| |) o más simplemente como (), no puede recibir ningún mensaje.
vehículo _ AddSlots: ( | nombre <- 'automóvil' | ) .
Aquí el receptor es el objeto anterior, que ahora incluirá las ranuras 'nombre' y 'nombre:' además de 'padre*'.
_ AddSlots: ( | sportsCar <- copia del vehículo | ) . sportsCar _ AddSlots: ( | driveToWork = ( '' algún código, este es un método '' ) | ) .
Aunque antes "vehículo" y "coche deportivo" eran exactamente iguales, ahora este último incluye una nueva ranura con un método que el original no tiene. Los métodos solo se pueden incluir en ranuras constantes.
_ AddSlots: ( | porsche911 <- copia sportsCar | ) . Nombre de Porsche911 : 'Bobs Porsche' .
El nuevo objeto 'porsche911' comenzó exactamente igual que 'sportsCar', pero el último mensaje cambió el valor de su ranura 'name'. Tenga en cuenta que ambos siguen teniendo exactamente las mismas ranuras, aunque uno de ellos tenga un valor diferente.
Una característica de Self es que se basa en el mismo tipo de sistema de máquina virtual que utilizaban los sistemas Smalltalk anteriores. Es decir, los programas no son entidades independientes como en lenguajes como C , sino que necesitan todo su entorno de memoria para poder ejecutarse. Esto requiere que las aplicaciones se envíen en fragmentos de memoria guardada conocidos como instantáneas o imágenes . Una desventaja de este enfoque es que las imágenes a veces son grandes y difíciles de manejar; sin embargo, depurar una imagen suele ser más sencillo que depurar programas tradicionales porque el estado de ejecución es más fácil de inspeccionar y modificar. (La diferencia entre el desarrollo basado en código fuente y el basado en imágenes es análoga a la diferencia entre la programación basada en clases y la programación orientada a objetos prototípica).
Además, el entorno está adaptado al cambio rápido y continuo de los objetos del sistema. Refactorizar un diseño de "clase" es tan sencillo como arrastrar métodos desde los ancestros existentes a otros nuevos. Las tareas sencillas, como los métodos de prueba, se pueden gestionar haciendo una copia, arrastrando el método a la copia y luego cambiándolo. A diferencia de los sistemas tradicionales, solo el objeto modificado tiene el nuevo código y no es necesario reconstruir nada para probarlo. Si el método funciona, simplemente se puede arrastrar de nuevo al ancestro.
Las máquinas virtuales propias lograron un rendimiento de aproximadamente la mitad de la velocidad del C optimizado en algunos puntos de referencia. [7]
Esto se logró mediante técnicas de compilación justo a tiempo que fueron desarrolladas y mejoradas en Self Research para lograr que un lenguaje de alto nivel funcione tan bien.
El recolector de basura de Self utiliza la recolección de basura generacional , que separa los objetos por antigüedad. Al utilizar el sistema de administración de memoria para registrar las escrituras de páginas, se puede mantener una barrera de escritura. Esta técnica ofrece un rendimiento excelente, aunque después de ejecutarse durante un tiempo puede producirse una recolección de basura completa, que demora un tiempo considerable. [ vago ]
El sistema de tiempo de ejecución aplana selectivamente las estructuras de llamadas. Esto proporciona modestas mejoras de velocidad en sí mismo, pero permite un amplio almacenamiento en caché de información de tipos y múltiples versiones de código para diferentes tipos de llamantes. Esto elimina la necesidad de realizar muchas búsquedas de métodos y permite insertar declaraciones de bifurcación condicionales y llamadas codificadas de forma rígida, lo que a menudo proporciona un rendimiento similar al de C sin pérdida de generalidad a nivel de lenguaje, pero en un sistema totalmente recolectado de basura. [8]