En el ultimo articuloComencé mi historia sobre el componente de modelado de lenguaje híbrido. Es un conjunto de conceptos-objetos conectados por relaciones lógicas. Logré hablar sobre las principales formas de definir conceptos, herencia y definir la relación entre ellos. Las razones que me impulsaron a comenzar a diseñar un lenguaje híbrido, y sus características, se pueden encontrar en mis publicaciones anteriores sobre este tema. Los enlaces a ellos se pueden encontrar al final de este artículo.
Y ahora propongo sumergirnos en algunos de los matices de la programación lógica. Dado que el lenguaje del componente de modelado tiene una forma lógica declarativa, será necesario resolver problemas como definir la semántica del operador de negación, introducir elementos de lógica de orden superior y agregar la capacidad de trabajar con variables lógicas. Para hacer esto, tendrás que lidiar con cuestiones teóricas como la asunción de la apertura / cierre del mundo, la negación como rechazo, la semántica del modelo estable y la semántica bien fundada. Y también con cómo se implementan las posibilidades de la lógica de orden superior en otros lenguajes de programación lógica.
Empecemos con variables booleanas
En la mayoría de los lenguajes de programación lógica, las variables se utilizan como una designación simbólica (marcador de posición) de declaraciones arbitrarias. Ocurren en las posiciones de los argumentos de los predicados y conectan los predicados entre sí. Por ejemplo, en la siguiente regla del lenguaje Prolog, las variables juegan el papel de objetos X e Y , conectados por relaciones: hermano , padre , varón y desigualdad:
brothers(X,Y) :- parent(Z,X), parent(Z,Y), male(X), male(Y), X\=Y.
En el componente de modelado, el papel de los argumentos de término lo juegan principalmente los atributos de concepto:
concept brothers (person1, person2) FROM parent p1 (child = person1), parent p2 (child = person2), male(person: person1), male(person: person2) WHERE p1.parent = p2.parent AND person1 != person2
Se puede acceder a ellos por su nombre directamente, como en SQL. Aunque la sintaxis propuesta parece más engorrosa en comparación con Prolog, en el caso de una gran cantidad de atributos, esta opción será más conveniente, ya que enfatiza la estructura del objeto.
Pero en algunos casos aún sería conveniente declarar una variable booleana que no estaría incluida en los atributos de ninguno de los conceptos, pero que al mismo tiempo se usaría en la expresión de relaciones. Por ejemplo, si una subexpresión tiene una forma compleja, puede dividirla en sus componentes vinculándolos a variables booleanas. Además, si una subexpresión se usa varias veces, puede declararla solo una vez asociándola con una variable. Y en el futuro, use una variable en lugar de una expresión. Para distinguir las variables booleanas de los atributos de concepto y las variables de componentes de cálculo, decidamos que los nombres de las variables booleanas deben comenzar con el símbolo $ .
Como ejemplo, podemos analizar el concepto que especifica la pertenencia de un punto a un anillo, que se describe por los radios externo e interno. La distancia desde un punto al centro de un anillo se puede calcular una vez, vincularla a una variable y compararla con los radios:
relation pointInRing between point p, ring r where $dist <= r.rOuter and $dist >= r.rInner and $dist = Math.sqrt((p.x – r.x) * (p.x – r.x) + (p.y – r.y) * (p.y – r.y))
Al mismo tiempo, esta distancia en sí tiene un papel auxiliar y no será parte del concepto.
Negación.
Ahora veamos un tema más complejo: la implementación del operador de negación dentro del componente de modelado. Los sistemas de programación lógica suelen incluir, además del operador de negación booleano, la regla de negación como rechazo. Le permite imprimir no p si falla la derivación de p . En diferentes sistemas de representación del conocimiento, tanto el significado como el algoritmo de la regla de inferencia de la negación pueden diferir.
Primero, es necesario responder la pregunta sobre la naturaleza del sistema de conocimiento en términos de su integridad. En los sistemas que se adhieren al "supuesto de mundo abierto" , la base de conocimientos se considera incompleta, por lo que las declaraciones que faltan se consideran desconocidas. No p afirmación sólo se puede generar si la base de conocimientos almacena explícitamente la declaración de que pfalso. Tal negación se llama fuerte. Las declaraciones faltantes se consideran desconocidas, no falsas. Un ejemplo de un sistema de conocimiento que utiliza tal suposición es la WEB Semántica. Es una red semántica global de acceso público formada sobre la base de la World Wide Web. La información que contiene es, por definición, incompleta: no está completamente digitalizada ni traducida a una forma legible por máquina, se distribuye entre diferentes nodos y se complementa constantemente. Por ejemplo, si en Wikipedia, en un artículo sobre Tim Berners-Lee, creador de la World Wide Web y autor del concepto de Web Semántica, no se dice nada sobre sus preferencias culinarias, entonces esto no quiere decir que no las tenga, el artículo simplemente está incompleto.
La suposición opuesta es "la suposición de que el mundo está cerrado".... Se cree que en tales sistemas la base de conocimientos es completa y las declaraciones que faltan en ella se toman como inexistentes o falsas. La mayoría de las bases de datos siguen este supuesto. Si la base de datos no tiene un registro sobre una transacción o sobre un usuario, entonces estamos seguros de que dicha transacción no existió y el usuario no está registrado en el sistema (al menos toda la lógica del sistema se basa en el hecho de que no existen).
Las bases de conocimiento completas tienen sus ventajas. Primero, no hay necesidad de codificar información desconocida: la lógica de dos valores (verdadero, falso) es suficiente en lugar de la de tres valores (verdadero, falso, desconocido). En segundo lugar, puede combinar el operador de negación booleano y la verificación de la derivabilidad de un enunciado de la base de conocimientos en un operador de negación como un rechazo(negación como fracaso). Devolverá verdadero no solo si se almacena la declaración de que la declaración es falsa, sino también si no hay información sobre ella en la base de conocimientos. Por ejemplo, la
regla
p <— not q
inferirá que q es falso (ya que no hay ninguna afirmación de que sea verdadero) y p es verdadero.
Desafortunadamente, la semántica de la negación como rechazo no es tan obvia ni más compleja de lo que parece. En diferentes sistemas de programación lógica, su significado tiene sus propias características. Por ejemplo, en el caso de definiciones cíclicas:
p ← not q q ← not p
la resolución SLDNF clásica (SLD + Negation as Failure) utilizada en el lenguaje Prolog fallará. La salida de la declaración p necesita la salida de q , y q - en p , el procedimiento de salida caerá en un bucle infinito. En Prolog, tales definiciones se consideran inválidas y la base de conocimientos se considera inconsistente.
Al mismo tiempo, estas declaraciones no son un problema para nosotros. Intuitivamente, comprendemos estas dos reglas diciendo que p y qtienen significados opuestos, si uno de ellos es verdadero, el otro es falso. Por tanto, es deseable que el operador de la negación como rechazo pueda trabajar con tales reglas, para que las construcciones de programas lógicos sean más naturales y comprensibles para una persona.
Además, la coherencia en la base de conocimientos no siempre se puede lograr. Por ejemplo, a veces las definiciones de reglas se separan deliberadamente de los hechos para que el mismo conjunto de reglas se pueda aplicar a diferentes conjuntos de hechos. En este caso, no hay garantía de que las reglas concuerden con todos los posibles conjuntos de hechos. A veces también es aceptable que las propias reglas sean inconsistentes, por ejemplo, si son elaboradas por diferentes expertos.
Las campañas más conocidas, permite formalizar la conclusión lógica bajo definiciones cíclicas y las inconsistencias del programa son "Semántica sustentable" (Semántica del modelo estable) y "Semántica razonable" (la Semántica Fundamentada).
La regla de inferencia con semántica de modelo persistente se basa en el supuesto de que algunos operadores de negación en un programa pueden ignorarse si no son consistentes con el resto del programa. Dado que puede haber varios subconjuntos consistentes del conjunto inicial de reglas, puede haber varias soluciones, respectivamente. Por ejemplo, en la definición anterior, la inferencia puede comenzar con la primera regla ( p ← no q ), descarte el segundo ( q ← no p ) y obtenga la solución {p, no q} . Y luego haz lo mismo para el segundo y obtén {q, no p} . La solución general sería un conjunto combinado de soluciones alternativas. Por ejemplo, de las reglas:
person(alex) alive(X) ←person(X) male(X) ←person(X) AND NOT female(X) female(X) ←person(X) AND NOT male(X)
podemos mostrar dos opciones de respuesta: {persona (alex), vivo (alex), masculino (alex)} y {persona (alex), vivo (alex), femenino (alex)} .
La semántica razonable comienza con los mismos supuestos, pero busca encontrar una solución parcial general que satisfaga todos los subconjuntos consensuales alternativos de reglas. La solución parcial significa que los valores "verdadero" o "falso" se mostrarán solo para una parte de los hechos y los valores del resto permanecerán desconocidos. Así, en la descripción de hechos en el programa, se usa lógica de dos valores, y en el proceso de inferencia, de tres valores. Para las reglas considerados anteriormente, los significados de ambos p y qserá desconocido. Pero, por ejemplo, para
p ← not q q ←not p r ← s s
podemos inferir con seguridad que r y s son ciertos, aunque p y q siguen siendo desconocidos.
Por ejemplo, del ejemplo de alex, podríamos inferir {persona (alex), vivo (alex)} , mientras que las declaraciones masculino (alex) y femenino {alex} siguen siendo desconocidas.
En SQL, la negación booleana ( NO ) y la verificación de derivabilidad ( NO EXISTE) están separados. Estos operadores se aplican a diferentes tipos de argumentos: NOT invierte un valor booleano y EXISTS / NOT EXISTS comprueba el resultado de una consulta anidada para ver si está vacío, por lo que no tiene sentido combinarlos. La semántica de los operadores de negación en SQL es muy simple y no está diseñada para trabajar con consultas inconsistentes recursivas o complejas; con una habilidad especial de SQL, la consulta se puede enviar a la recursividad infinita. Pero las construcciones lógicas complejas están claramente fuera del alcance tradicional de SQL, por lo que no necesita una semántica de operador de negación sofisticada.
Ahora intentemos comprender la semántica de los operadores de negación del componente de modelado del lenguaje híbrido diseñado.
Primero, el componente de modelado está diseñado para integrar fuentes de datos dispares. Pueden ser muy variados y pueden ser completos o incompletos. Por lo tanto, definitivamente se necesitan operadores de verificación de derivabilidad.
En segundo lugar, la forma de modelar conceptos de componentes está mucho más cerca de las consultas SQL que de las reglas de programación lógica. El concepto también tiene una estructura compleja, por lo que mezclar en un operador de negación booleana y verificar la derivabilidad del concepto no tiene sentido. La negación booleana solo tiene sentido para aplicar a atributos, variables y resultados de expresión; pueden ser falsos o verdaderos. Es más difícil aplicarlo a un concepto, puede constar de diferentes atributos y no está claro cuál de ellos debería ser responsable de la falsedad o la verdad del concepto en su conjunto. El concepto puede derivarse de los datos iniciales como un todo, y no de sus atributos individuales. A diferencia de SQL, donde la estructura de las tablas es fija, la estructura de los conceptos puede ser flexible, un concepto puede no tener el atributo requerido en su composición,por lo tanto, también deberá verificar la existencia del atributo.
Por lo tanto, tiene sentido introducir operadores separados para cada tipo de negación mencionado anteriormente. La falsedad de los atributos se puede comprobar utilizando el operador NOT booleano tradicional , si un concepto contiene un atributo utilizando la función DEFINED incorporada y el resultado de inferir el concepto de los datos originales utilizando la función EXISTS . Los tres operadores separados son más predecibles, comprensibles y fáciles de usar que el operador complejo de negación como falla. Si es necesario, se pueden combinar en un operador de una forma u otra que tenga sentido para cada caso específico.
En tercer lugar, por el momento, el componente de modelado se considera una herramienta para crear pequeñas ontologías a nivel de aplicación. Es poco probable que su lenguaje necesite una expresividad lógica especial y reglas de inferencia sofisticadas que puedan hacer frente a definiciones recursivas e inconsistencias lógicas del programa. Por tanto, la implementación de reglas de inferencia complejas basadas en la semántica de modelos persistentes o semántica fundamentada no parece aconsejable, al menos en esta etapa. La resolución SLDNF clásica debería ser suficiente.
Ahora veamos algunos ejemplos.
A un concepto se le puede dar un significado negativo si algunos de sus atributos tienen un significado que lo indique. La negación de atributos le permite encontrar explícitamente tales entidades:
concept unfinishedTask is task t where not t.completed
La función de comprobar la ambigüedad de un atributo será conveniente si las entidades de un concepto pueden tener diferentes estructuras:
concept unassignedTask is task t where not defined(t.assignedTo) or empty(t.assignedTo)
La función de comprobar la deducibilidad de un concepto es insustituible cuando se trabaja con definiciones recursivas y estructuras jerárquicas:
concept minimalElement is element greater where not exists(element lesser where greater.value > lesser.value)
En este ejemplo, la verificación de la existencia de un elemento más pequeño se realiza como una subconsulta. Planeo considerar en detalle la creación de consultas anidadas en la próxima publicación.
Elementos de lógica de orden superior
En la lógica de primer orden, las variables solo pueden coincidir con conjuntos de objetos y aparecer solo en las posiciones de los argumentos de los predicados. En la lógica de orden superior, también pueden corresponder a conjuntos de relaciones y aparecer en la posición de nombres de predicados. En otras palabras, la lógica de primer orden nos permite afirmar que alguna relación es verdadera para todos o algunos de los objetos. Y la lógica de orden superior es describir la relación entre relaciones.
Por ejemplo, podemos hacer declaraciones de que algunas personas son hermanos, hermanas, hijos o padres, tíos o tías, etc.:
Brother(John, Joe). Son(John, Fred). Uncle(John, Alex).
Pero para hacer una afirmación de relación, en lógica de primer orden, necesitamos enumerar todas las declaraciones anteriores, combinándolas usando la operación OR:
ⱯX,ⱯY(Brother(X, Y) OR Brother(Y, X) OR Son(X, Y) OR Son(Y, X) OR Uncle(X, Y) OR Uncle(Y, X) → Relative(X, Y)).
La lógica de segundo orden le permite hacer una declaración sobre otras declaraciones. Por ejemplo, se podría afirmar directamente que la relación entre hermanos, hermanas, padres e hijos, tíos y sobrinos es una relación de parentesco:
RelativeRel(Brother). RelativeRel(Son). RelativeRel(Uncle). ⱯX,ⱯY(ⱻR(RelativeRel(R) AND (R(X, Y) OR R(Y, X))) → Relative(X, Y)).
Argumentamos que si para cada X e Y hay una relación R que es una relación entre hermanos RelativeRel , y X e Y satisfacen esta relación, entonces X e Y son hermanos. Los argumentos de relación pueden ser otras relaciones y las variables se pueden sustituir por nombres de relación.
La lógica de tercer orden le permite construir declaraciones sobre declaraciones sobre declaraciones, etc., lógica de orden superior- sobre declaraciones de cualquier nivel de anidamiento. La lógica de orden superior es mucho más expresiva, pero también mucho más compleja. En la práctica, los sistemas de programación lógica admiten solo algunos de sus elementos, que se limitan principalmente al uso de variables y expresiones arbitrarias en las posiciones de los predicados.
En Prolog, los elementos de dicha lógica se implementan utilizando varios metapredicados incorporados, cuyos argumentos son otros predicados. El principal es la llamada de predicado que le permite agregar predicados dinámicamente a la lista de destino de la regla actual. Su primer argumento se trata como un objetivo y el resto como sus argumentos. Prolog buscará en la base de conocimientos los predicados que coincidan con el primer argumento y los agregará a la lista de objetivos actual. Un ejemplo con familiares se vería así:
brother(john, jack). sister(mary, john). relative_rel(brother). relative_rel(sister). relative(X, Y) :- relative_rel(R), (call(R, X, Y); call(R, Y, X)).
Prolog también admite predicados findall (plantilla, objetivo, bolsa) , bolsa de (plantilla, objetivo, bolsa) , conjunto de (plantilla, objetivo, conjunto) , etc., que le permiten encontrar todas las soluciones de objetivo de objetivo que coinciden con la plantilla de plantilla y unificar (enlace) su lista con el resultado Bolsa (o Conjunto ). Prolog tiene predicados incorporados current_predicate , cláusula y otros para encontrar predicados en la base de conocimiento. También puede manipular predicados y sus atributos en bases de conocimiento: agréguelos, elimínelos y cópielos.
El lenguaje HiLog admite una lógica de orden superior a nivel de sintaxis. En lugar de metapredicados especiales, permite usar términos arbitrarios (como variables) directamente en la posición de los nombres de predicados. La regla para determinar los familiares tomará la forma:
relative(X, Y) :- relative_rel(R), (R(X, Y); R(Y, X)).
Esta sintaxis es más declarativa, concisa, comprensible y natural en comparación con Prolog. Al mismo tiempo, HiLog sigue siendo una variante sintáctica de Prolog, ya que todas las construcciones sintácticas de HiLog se pueden transformar en expresiones lógicas de primer orden utilizando los meta-predicados de llamada .
Se considera que HiLog tiene una sintaxis de orden superior pero semántica de primer orden . Esto significa que al comparar variables que representan reglas o funciones, solo se tienen en cuenta sus nombres, no su implementación. También hay lenguajes que admiten semántica de orden superior, como λ-Prolog, que también permiten involucrar la implementación de reglas y funciones en el proceso de inferencia. Pero tal lógica y sus algoritmos de inferencia son mucho más complicados.
Pasemos ahora a la funcionalidad lógica de orden superior del componente de modelado . Para la mayoría de las tareas prácticas de metaprogramación, Prolog y HiLog deberían ser suficientes. HiLog tiene una sintaxis más natural, por lo que tiene sentido tomarlo como base. Para poder utilizar expresiones arbitrarias en las posiciones de los nombres de conceptos y sus atributos y distinguirlos de variables, llamadas a funciones y otras construcciones, introducimos un operador especial para nombres dinámicos :
< >
Le permite evaluar el valor de una expresión y usarlo como nombre de concepto, alias o nombre de atributo, según el contexto. Si este operador ocupa el lugar del nombre del concepto en la sección FROM y el valor de su expresión está definido, entonces se encontrarán todos los conceptos con el nombre especificado y se realizará una búsqueda lógica para ellos:
concept someConcept ( … ) from conceptA a, <a.conceptName> b where …
Si el valor de la expresión no está definido, por ejemplo, la expresión es una variable booleana no asociada con el valor. , entonces el procedimiento encontrará todos los conceptos adecuados y asociará el valor de la variable con sus nombres:
concept someConcept is <$conceptName> where …
Podemos decir que en el contexto de la sección FROM , el operador para especificar el nombre tiene semántica lógica .
Además, se puede utilizar el operador <> y las cláusulas WHERE en la posición de un alias de concepto o nombre de atributo:
concept someConcept ( … ) from conceptA a, conceptB b where conceptB.<a.foreignKey> = a.value ...
Las expresiones de la cláusula WHERE son deterministas, es decir, no utilizan la búsqueda lógica para encontrar valores desconocidos para sus argumentos. Esto significa que la expresión conceptB. <A.foreignKey> = a.value se evaluará solo después de que se encuentren las entidades del concepto a , y sus atributos ForeignKey y value estén asociados con valores. Por lo tanto, podemos decir que en el contexto de la cláusula FROM , la declaración de nombre tiene semántica funcional .
Consideremos algunas posibles aplicaciones de la lógica de orden superior.
El ejemplo más obvio en el que la lógica de orden superior será conveniente es la unión bajo un nombre de todos los conceptos que satisfacen ciertas condiciones. Por ejemplo, tener ciertos atributos. Por lo que el concepto de punto se puede considerar todos los conceptos que incluyen las coordenadas X e Y :
concept point is <$anyConcept> a where defined(a.x) and defined(a.y)
La búsqueda booleana vinculará la variable $ anyConcept con todos los nombres de conceptos declarados (excepto, por supuesto, él mismo) que tengan atributos de coordenadas.
Un ejemplo más complejo sería declarar una relación general que se aplica a muchos conceptos. Por ejemplo, una relación transitiva padre-hijo entre conceptos:
relation ParentRel between <$conceptName> parent, <$conceptName> child where defined(parent.id) and defined(child.parent) and ( parent.id = child.parent or exists( <$conceptName> intermediate where intermediate.parent = parent.id and ParentRel(intermediate, child) ))
La variable $ conceptName se usa en los tres conceptos de padre, hijo e intermedio, lo que significa que sus nombres deben ser iguales.
La lógica de orden superior abre posibilidades para la programación genérica en el sentido de que puede crear conceptos y relaciones genéricos que se pueden aplicar a muchos conceptos que satisfacen condiciones específicas sin estar atados a sus nombres específicos.
Además, la sustitución dinámica de nombres será conveniente en los casos en que los atributos de un concepto sean referencias a los nombres de otros conceptos o sus atributos, o cuando los datos de origen contengan no solo hechos, sino también su estructura. Por ejemplo, los datos de origen pueden incluir una descripción de los esquemas de documentos o tablas XML en una base de datos. Los datos originales también pueden incluir información adicional sobre los hechos, por ejemplo, tipos de datos, formatos o valores predeterminados, condiciones de validación o algunas reglas. Además, los datos iniciales pueden describir el modelo de algo, y el componente de modelado será responsable de construir el metamodelo. Trabajar con textos en lenguaje natural también supone que los datos de origen incluirán no solo declaraciones, sino también declaraciones sobre declaraciones.En todos estos casos, la lógica de primer orden no será suficiente y se necesita un lenguaje más expresivo.
Como ejemplo sencillo, considere el caso en el que los datos incluyen algunos objetos, así como las reglas para validar los atributos de estos objetos como una entidad separada:
fact validationRule {objectName: “someObject”, attributeName: “someAttribute”, rule: function(value) {...}}
Los resultados de la validación se pueden describir mediante el siguiente concepto:
concept validationRuleCheck ( objectName = r.objectName, attributeName = r.attrName, result = r.rule(o.<r.attrName>) ) from validationRule r, <r.objectName> o where defined(o.<r.attrName>)
La lógica de orden superior abre algunas posibilidades bastante interesantes para la programación generalizada y la metaprogramación. Pudimos considerar solo su idea general. Esta área es bastante compleja y requiere una investigación exhaustiva en el futuro. Tanto desde el punto de vista de la elección de un diseño conveniente del lenguaje, como desde el punto de vista de su desempeño.
conclusiones
En el proceso de trabajar en el componente de modelado, tuve que lidiar con problemas bastante específicos de programación lógica, como trabajar con variables booleanas, la semántica del operador de negación y elementos de lógica de orden superior. Si todo resultó ser bastante simple con las variables, entonces no existe un enfoque único bien establecido para la implementación del operador de negación y la lógica de orden superior, y hay varias soluciones que tienen sus propias características, ventajas y desventajas.
Traté de elegir una solución que fuera fácil de entender y conveniente en la práctica. Preferí dividir el operador de negación monolítica como un rechazo en tres operadores separados de negación booleana, verificando la deducibilidad de un concepto y la existencia de un atributo en un objeto. Si es necesario, estos tres operadores se pueden combinar para obtener la semántica de negación requerida para cada caso específico. Para la regla de negación de inferencia, decidí tomar como base la resolución SLDNF estándar. Si bien no es capaz de lidiar con declaraciones recursivas inconsistentes, es mucho más fácil de entender e implementar que alternativas más sofisticadas como la semántica de modelo persistente o la semántica razonada.
Se necesitará una lógica de orden superior para la programación generalizada y la metaprogramación. Por programación genérica, me refiero a la capacidad de construir nuevos conceptos a partir de una amplia variedad de conceptos secundarios sin estar ligado a nombres específicos de estos últimos. Por metaprogramación, me refiero a la capacidad de describir la estructura de algunos conceptos con la ayuda de otros conceptos. Decidí tomar el lenguaje HiLog como modelo para los elementos lógicos de orden superior del componente de modelado. Tiene una sintaxis flexible y conveniente que le permite usar variables en las posiciones de los nombres de los predicados, así como una semántica simple y clara.
Planeo dedicar el próximo artículo a tomar prestado del mundo de SQL: consultas anidadas y agregaciones. También hablaré de otro tipo de conceptos que no usan inferencia, sino que las entidades se generan directamente usando una función determinada. Y cómo se puede utilizar para convertir tablas, matrices y matrices asociativas en formato de objeto e incluirlas en el proceso de inferencia lógica (por analogía con la operación SQL UNNEST, que convierte matrices a formato de tabla).
El texto completo en estilo científico en inglés está disponible aquí .
Enlaces a publicaciones anteriores:
Diseño de un lenguaje de programación multiparadigma. Parte 1 - ¿Para qué sirve?
Diseñamos un lenguaje de programación multiparadigma. Parte 2 - Comparación de modelos de construcción en PL / SQL, LINQ y GraphQL Diseñamos
un lenguaje de programación multi-paradigma. Parte 3 - Descripción general de los lenguajes de representación del conocimiento
Diseñamos un lenguaje de programación multiparadigma. Parte 4 - Construcciones básicas del lenguaje de modelado