Diseñamos un lenguaje de programación multiparadigma. Parte 1 - ¿Para qué sirve?

Habr es un lugar maravilloso donde puede sentirse libre de compartir sus ideas (incluso si parecen locas). Habr vio muchos lenguajes de programación caseros y les contaré sobre mis experimentos en esta área. Pero mi historia será diferente a las demás. Primero, no será solo un lenguaje de programación, sino un lenguaje híbrido que combina varios paradigmas de programación. En segundo lugar, uno de los paradigmas será bastante inusual: estará destinado a una descripción declarativa del modelo de dominio. Y en tercer lugar, la combinación de herramientas de modelado declarativo y enfoques tradicionales orientados a objetos u funcionales en un lenguaje puede dar lugar a un nuevo estilo original de programación: la programación orientada a ontologías. Planeo revelar problemas y preguntas principalmente teóricos,que encontré, y contar no solo sobre el resultado, sino también sobre el proceso de creación del diseño de dicho lenguaje. Habrá muchas revisiones de tecnología y enfoques científicos, así como discursos filosóficos. Hay mucho material, tendrás que dividirlo en toda una serie de artículos. Si está interesado en una tarea tan compleja y de gran envergadura, prepárese para una lectura larga e inmersión en el mundo de la lógica informática y los lenguajes de programación híbridos.



Describiré brevemente la tarea principal.



Consiste en crear un lenguaje de programación de este tipo que sea conveniente tanto para describir el modelo de dominio como para trabajar con él. Hacer que la descripción del modelo sea lo más natural posible, comprensible para los humanos y cercana a las especificaciones del software. Pero al mismo tiempo, debe ser parte del código en un lenguaje de programación completo. Para ello, el modelo tendrá la forma de una ontología y estará compuesto por hechos concretos, conceptos abstractos y relaciones entre ellos. Los hechos describirán el conocimiento directo del área temática y los conceptos y las relaciones lógicas entre ellos: su estructura.



Además de las herramientas de modelado, el lenguaje también necesitará herramientas para preparar los datos iniciales para el modelo, creando dinámicamente sus elementos, procesando los resultados de las consultas al mismo, creando aquellos elementos del modelo que sean más convenientes para describir en forma algorítmica. Es mucho más conveniente hacer todo esto describiendo explícitamente la secuencia de cálculos. Por ejemplo, usando OOP o un enfoque funcional.



Y, por supuesto, ambas partes del lenguaje deben interactuar estrechamente y complementarse entre sí. Para que se puedan combinar fácilmente en una aplicación y resolver cada tipo de problema con la herramienta más conveniente.



Comenzaré mi historia con la pregunta de por qué incluso crear un lenguaje así, por qué un lenguaje híbrido y dónde sería útil. En los próximos artículos, planeo hacer una breve descripción general de tecnologías y marcos que le permiten combinar el estilo declarativo con imperativo o funcional. Además, será posible revisar los lenguajes para describir ontologías, formular los requisitos y principios básicos de un nuevo lenguaje híbrido y, en primer lugar, su componente declarativo. Finalmente, describa sus conceptos y elementos básicos. Después de eso, consideraremos qué problemas surgen al usar los paradigmas declarativo e imperativo juntos y cómo se pueden resolver. También analizaremos algunos aspectos de la implementación del lenguaje, por ejemplo, el algoritmo de inferencia. Finalmente, veamos uno de los ejemplos de su aplicación.



Elegir el estilo de lenguaje de programación correcto es una condición importante para la calidad del código



Muchos de nosotros hemos tenido que lidiar con el apoyo de proyectos complejos creados por otras personas. Es bueno si el equipo tiene personas que están familiarizadas con el código del proyecto y pueden explicar cómo funciona, hay documentación, el código es limpio y comprensible. Pero en realidad, a menudo sucede de una manera diferente: los autores del código se retiraron mucho antes de que llegaras a este proyecto, no hay documentación en absoluto, o es muy fragmentario y está desactualizado hace mucho tiempo, y sobre la lógica comercial del componente requerido, un analista comercial o un proyecto. - el gerente solo puede decirlo en términos generales. En este caso, la limpieza y la comprensión del código es fundamental.

La calidad del código tiene muchos aspectos, uno de ellos es la correcta elección del lenguaje de programación, que debe corresponder al problema que se está resolviendo. Cuanto más fácil y natural sea que un desarrollador implemente sus ideas en el código, más rápido podrá resolver el problema y menos errores cometerá. Ahora tenemos un número bastante grande de paradigmas de programación para elegir, cada uno con su propia área de aplicación. Por ejemplo, la programación funcional es preferible para las aplicaciones centradas en la computación porque proporciona más flexibilidad para estructurar, combinar y reutilizar funciones que realizan operaciones en datos. Programación orientada a objetossimplifica la creación de estructuras a partir de datos y funciones mediante encapsulación, herencia, polimorfismo. OOP es adecuado para aplicaciones orientadas a datos. La programación lógica es conveniente para problemas basados ​​en reglas que requieren trabajar con tipos de datos complejos, definidos de forma recursiva, como árboles y gráficos, y es adecuada para resolver problemas combinatorios. Además, la programación reactiva, impulsada por eventos y de múltiples agentes tiene sus propios alcances.



Los lenguajes de programación modernos de propósito general pueden admitir múltiples paradigmas. La combinación de paradigmas funcionales y de programación orientada a objetos ha sido durante mucho tiempo la corriente principal.



La programación lógica funcional híbrida también tiene una larga historia, pero nunca fue más allá del mundo académico. Se presta menos atención a la combinación de programación OOP lógica e imperativa (planeo hablar sobre ellos con más detalle en una de las próximas publicaciones). Aunque, en mi opinión, un enfoque lógico podría resultar muy útil en el campo tradicional de la POO - aplicaciones de servidor de sistemas de información corporativos. Solo necesitas mirarlo desde un ángulo ligeramente diferente.



Por qué encuentro subestimado el estilo de programación declarativa



Intentaré fundamentar mi punto de vista.



Para hacer esto, considere lo que puede ser una solución de software. Sus principales componentes son: la parte del cliente (escritorio, móvil, aplicaciones web); lado del servidor (un conjunto de servicios separados, microservicios o una aplicación monolítica); sistemas de gestión de datos (relacionales, orientados a documentos, orientados a objetos, bases de datos de gráficos, servicios de almacenamiento en caché, índices de búsqueda). Una solución de software tiene que interactuar con algo más que personas: usuarios. La integración con servicios externos que brindan información a través de API es una tarea común. Además, las fuentes de datos pueden ser documentos de audio y video, textos en lenguaje natural, contenido de páginas web, registros de eventos, datos médicos, lecturas de sensores, etc.



Por un lado, una aplicación de servidor almacena datos en una o más bases de datos. Por otro lado, responde a las solicitudes provenientes de los puntos finales de la API, procesa los mensajes entrantes y responde a los eventos. Las estructuras de los mensajes y las consultas casi nunca coinciden con las estructuras almacenadas en las bases de datos. Los formatos de datos de entrada / salida están diseñados para uso externo, optimizados para el consumidor de esta información y ocultan la complejidad de la aplicación. Los formatos de datos almacenados están optimizados para su sistema de almacenamiento, por ejemplo, para un modelo de datos relacionales. Por lo tanto, necesitamos una capa intermedia de conceptos que permita combinar la entrada / salida de aplicaciones con los sistemas de almacenamiento de datos. Normalmente, esta capa de middleware se denomina capa de lógica empresarial e implementa las reglas y principios para el comportamiento de los objetos de dominio.



La tarea de vincular el contenido de la base de datos a los objetos de la aplicación tampoco es fácil. Si la estructura de las tablas en el almacenamiento coincide con la estructura de los conceptos en el nivel de la aplicación, entonces puede utilizar la tecnología ORM. Pero para casos más complejos que el acceso a registros por clave primaria y operaciones CRUD, debe asignar una capa de lógica separada para trabajar con la base de datos. Normalmente, el esquema de la base de datos es lo más general posible, de modo que diferentes servicios pueden trabajar con él. Cada uno de los cuales asigna este esquema de datos a su propio modelo de objeto. La estructura de la aplicación se vuelve aún más confusa si la aplicación no funciona con un almacén de datos, sino con varios, de diferentes tipos, cargando datos de fuentes de terceros, por ejemplo, a través de la API de otros servicios.En este caso, es necesario crear un modelo de dominio unificado y asignarle datos de diferentes fuentes.

En algunos casos, el modelo de dominio puede tener una estructura compleja de varios niveles. Por ejemplo, al compilar informes analíticos, algunos indicadores se pueden construir sobre la base de otros, que, a su vez, serán una fuente para construir el tercero, etc. Además, los datos de entrada pueden tener una forma semiestructurada. Estos datos no tienen un esquema estricto, como, por ejemplo, en el modelo de datos relacionales, pero aún contienen algún tipo de marcado que le permite extraer información útil de ellos. Ejemplos de tales datos pueden ser recursos de la Web Semántica, resultados de análisis de páginas web, documentos, registros de eventos, lecturas de sensores, resultados de preprocesamiento de datos no estructurados como textos, videos e imágenes, etc. El esquema de datos de estas fuentes se construirá exclusivamente a nivel de aplicación. También habrá un código,convertir los datos de origen en objetos de lógica empresarial.



Entonces, la aplicación contiene no solo algoritmos y cálculos, sino también una gran cantidad de información sobre la estructura del modelo de dominio: la estructura de sus conceptos, sus relaciones, jerarquía, reglas para construir algunos conceptos sobre la base de otros, reglas para transformar conceptos entre diferentes capas de la aplicación, etc. Cuando redactamos una documentación o un proyecto, describimos esta información de forma declarativa, en forma de estructuras, diagramas, declaraciones, definiciones, reglas, descripciones en lenguaje natural. Nos conviene pensar así. Desafortunadamente, estas descripciones no siempre se pueden expresar en código de la misma forma natural.



Consideremos un pequeño ejemplo y especulemos cómo se verá su implementación usando diferentes paradigmas de programación.



Digamos que tenemos 2 archivos CSV. En el primer archivo:



la primera columna contiene el ID del cliente.

El segundo contiene la fecha.

En el tercero, el monto facturado,

en el cuarto, el monto del pago.


En el segundo archivo:

la primera columna almacena el ID del cliente.

En el segundo, el nombre.

El tercero es la dirección de correo electrónico.


Introduzcamos algunas definiciones:

La factura incluye el identificador del cliente, la fecha, el monto facturado, el monto del pago y la deuda de las celdas de una línea del archivo 1.

El monto de la deuda es la diferencia entre el monto facturado y el monto del pago.

El cliente se describe utilizando la identificación del cliente, el nombre y la dirección de correo electrónico de las celdas de una línea en el archivo 2.

Una factura impaga es una factura de deuda positiva.

Las cuentas están vinculadas a un cliente por valor de ID de cliente.

Un deudor es un cliente que tiene al menos una factura impaga, cuya fecha es 1 mes más antigua que la fecha actual.

Un moroso malintencionado es un cliente que tiene más de 3 facturas impagas.


Además, utilizando estas definiciones, puede implementar la lógica de enviar un recordatorio a todos los deudores, transmitir datos sobre morosos persistentes a los cobradores, calcular una multa sobre el monto de la deuda, compilar varios informes, etc.



En lenguajes de programación funcionalesdicha lógica empresarial se implementa utilizando un conjunto de estructuras de datos y funciones para su transformación. Además, las estructuras de datos están fundamentalmente separadas de las funciones. Como resultado, el modelo, y especialmente su componente, como las relaciones entre entidades, se oculta dentro de un conjunto de funciones, untado sobre el código del programa. Esto crea una gran brecha entre la descripción declarativa del modelo y su implementación de software y complica su comprensión. Especialmente si el modelo tiene un gran volumen.



Estructurar un programa en orientado a objetosEl estilo ayuda a mitigar este problema. Cada entidad de dominio está representada por un objeto cuyos campos de datos corresponden a los atributos de la entidad. Y las relaciones entre entidades se implementan en forma de relaciones entre objetos, en parte basadas en los principios de la programación orientada a objetos (herencia, abstracción de datos y polimorfismo, en parte) utilizando patrones de diseño. Pero en la mayoría de los casos, las relaciones deben implementarse codificándolas en métodos de objeto. Además, además de crear clases que representen entidades, también necesitarás estructuras de datos para ordenarlas, algoritmos para llenar estas estructuras y buscar información en ellas.



En el ejemplo con deudores, podemos describir clases que describen la estructura de los conceptos "Cuenta" y "Cliente". Pero la lógica de crear objetos, vincular la cuenta y los objetos del cliente entre sí a menudo se implementa por separado en clases o métodos de fábrica. Para los conceptos de deudores y facturas impagas, no se necesitan clases separadas en absoluto, sus objetos se pueden obtener filtrando clientes y facturas donde se necesiten. Como resultado, algunos de los conceptos del modelo se implementarán en forma de clases explícitamente, algunos, implícitamente, a nivel de objeto. Algunas de las relaciones entre conceptos se encuentran en los métodos de las clases correspondientes y otras están separadas. La implementación del modelo se distribuirá entre clases y métodos, mezclada con la lógica auxiliar de su almacenamiento, búsqueda, procesamiento y conversión de formato. Se necesitará un poco de esfuerzo para encontrar este modelo en su código y comprenderlo.



Lo más cercano a la descripción será la implementación del modelo conceptual en lenguajes de representación del conocimiento . Ejemplos de tales lenguajes son Prolog, Datalog, OWL, Flora y otros. Planeo hablar sobre estos idiomas en la tercera publicación. Se basan en la lógica de primer orden o sus fragmentos, por ejemplo, la lógica descriptiva. Estos lenguajes permiten de forma declarativa especificar la especificación de la solución al problema, describir la estructura del objeto o fenómeno modelado y el resultado esperado. Y los motores de búsqueda integrados encontrarán automáticamente una solución que cumpla con las condiciones especificadas. La implementación del modelo de dominio en dichos lenguajes será extremadamente concisa, comprensible y cercana a la descripción en lenguaje natural.



Por ejemplo, la implementación del problema con deudores en Prolog estará muy cerca de las definiciones del ejemplo. Para hacer esto, las celdas de la tabla deberán representarse como hechos y las definiciones del ejemplo, como reglas. Para comparar cuentas y clientes, basta con especificar la relación entre ellos en la regla, y sus valores específicos se mostrarán automáticamente.



Primero, declaramos hechos con el contenido de las tablas en el formato: ID de tabla, fila, columna, valor:



cell(“Table1”,1,1,”John”). 


Luego le damos nombres a cada una de las columnas:



clientId(Row, Value) :- cell(“Table1”, Row, 1, Value).


Luego, puede combinar todas las columnas en un concepto:



bill(Row, ClientId, Date, AmountToPay, AmountPaid) :- clientId(Row, ClientId), date(Row, Date), amountToPay(Row, AmountToPay), amountPaid(Row, AmountPaid).
unpaidBill(Row, ClientId, Date, AmountToPay, AmountPaid) :- bill(Row, ClientId, Date, AmountToPay, AmountPaid),  AmountToPay >  AmountPaid.
debtor(ClientId, Name, Email) :- client(ClientId, Name, Email), unpaidBill(_, ClientId, _, _, _).


Etc.



Las dificultades comenzarán al trabajar con el modelo: al implementar la lógica para enviar mensajes, transferir datos a otros servicios, cálculos algorítmicos complejos. El punto débil de Prolog es su descripción de secuencias de acciones. Su implementación declarativa, incluso en casos simples, puede parecer muy poco natural y requiere un esfuerzo y habilidad significativos. Además, la sintaxis de Prolog no se acerca mucho al modelo orientado a objetos, y las descripciones de conceptos compuestos complejos con una gran cantidad de atributos serán bastante difíciles de entender.



¿Cómo reconciliamos el lenguaje de desarrollo funcional u orientado a objetos convencional con la naturaleza declarativa del modelo de dominio?



El enfoque más conocido es el diseño orientado a objetos ( diseño dirigido por dominios). Esta metodología facilita la creación e implementación de modelos de dominio complejos. Dicta que todos los conceptos del modelo se expresan explícitamente en el código de la capa de lógica empresarial. Los conceptos del modelo y los elementos del programa que los implementan deben estar lo más cerca posible entre sí y corresponder a un solo lenguaje, comprensible tanto para programadores como para expertos en la materia.



Un modelo de dominio enriquecido para el ejemplo con deudores contendrá adicionalmente clases para los conceptos "Factura impaga" y "Deudor", clases agregadas para combinar los conceptos de cuentas y clientes, fábricas para crear objetos. La implementación y el soporte de un modelo de este tipo consume más tiempo y el código es engorroso; lo que antes se podía hacer en una línea requiere varias clases en un modelo enriquecido. Como resultado, en la práctica, este enfoque solo tiene sentido cuando grandes equipos están trabajando en modelos a escala complejos.



En algunos casos, la solución puede ser una combinación de un lenguaje de programación básico funcional u orientado a objetos y un sistema externo de representación del conocimiento.... El modelo de dominio se puede transferir a una base de conocimiento externa, por ejemplo, en Prolog u OWL, y el resultado de las consultas se procesa en el nivel de la aplicación. Pero este enfoque complica la solución, las mismas entidades deben implementarse en ambos lenguajes, la interacción entre ellas debe configurarse a través de la API, adicionalmente soportada por el sistema de representación del conocimiento, etc. Por lo tanto, se justifica solo si el modelo es grande y complejo, y requiere inferencia lógica. Para la mayoría de las tareas, esto será excesivo. Además, este modelo no siempre puede separarse de la aplicación sin dolor.



Otra opción para combinar bases de conocimiento y aplicaciones OOP es la programación orientada a ontologías.... Este enfoque se basa en las similitudes entre las herramientas de descripción de ontologías y el modelo de programación de objetos. Las clases, entidades y atributos de ontología escritos, por ejemplo, en el lenguaje OWL, se pueden asignar automáticamente a clases, objetos y sus campos del modelo de objetos. Y luego las clases resultantes se pueden usar junto con otras clases de la aplicación. Desafortunadamente, la implementación básica de esta idea tendrá un alcance bastante limitado. Los lenguajes de ontología son bastante expresivos y no todos los componentes de ontología se pueden convertir en clases de OOP de una manera simple y natural. Además, para implementar una inferencia completa, no es suficiente crear un conjunto de clases y objetos. Necesita información sobre los elementos de la ontología en forma explícita, por ejemplo, en forma de metaclases.Planeo hablar sobre este enfoque con más detalle en una de las próximas publicaciones.



También existe un enfoque tan extremo para el desarrollo de software como el desarrollo impulsado por modelos . Según él, la principal tarea del desarrollo pasa a ser la creación de modelos de dominio, a partir de los cuales se genera automáticamente el código del programa. Pero en la práctica, una solución tan radical no siempre es lo suficientemente flexible, especialmente en términos de desempeño del programa. El creador de tales modelos tiene que combinar los roles de programador y analista de negocios. Por lo tanto, este enfoque no puede desplazar los enfoques tradicionales para implementar el modelo en lenguajes de programación de propósito general.



Todos estos enfoques son bastante engorrosos y tienen sentido para modelos de gran complejidad, a menudo descritos por separado de la lógica de su uso. Me gustaría algo más ligero, más cómodo y natural. De modo que con la ayuda de un lenguaje fue posible describir tanto el modelo en forma declarativa como los algoritmos para su uso. Por lo tanto, pensé en cómo combinar el paradigma funcional o orientado a objetos (llamémoslo componente de computación ) y el paradigma declarativo (llamémoslo componente de modelado ) dentro de un solo lenguaje de programación híbrido . A primera vista, estos paradigmas se ven opuestos entre sí, pero es aún más interesante intentarlo.



Entonces, el objetivo es crear un lenguaje que sea cómodo para el modelado conceptual basado en datos semiestructurados y dispares. La forma del modelo debe ser cercana a la ontología y consistir en una descripción de las entidades del dominio y las relaciones entre ellas. Ambos componentes del lenguaje deben estar estrechamente integrados, incluso a nivel semántico.



Los elementos de la ontología deben ser entidades del lenguaje de primer nivel: pueden pasarse a funciones como argumentos, asignarse a variables, etc. Dado que el modelo - ontología se convertirá en uno de los elementos principales del programa, este enfoque de programación puede llamarse orientado ontológicamente. Combinar la descripción del modelo con algoritmos para su uso haría que el código del programa fuera más comprensible y natural para una persona, lo acercaría al modelo conceptual del dominio y simplificaría el desarrollo y mantenimiento del software.



Suficiente por primera vez. En la próxima publicación, quiero hablar sobre algunas tecnologías modernas que combinan estilos imperativos y declarativos: PL / SQL, Microsoft LINQ y GraphQL. Para aquellos que no quieran esperar el lanzamiento de todas las publicaciones sobre Habré, hay un texto completo en estilo científico en inglés, disponible en el enlace:

Programación híbrida orientada a la ontología para el procesamiento de datos semiestructurados .



All Articles