Condividi questa pagina

Impara X in Y minuti

Dove X=Logtalk

Logtalk è un linguaggio di programmazione logica orientata agli oggetti che estende il linguaggio Prolog con le moderne tecniche di Object-Oriented Programming quali incapsulamento, ereditarietà e riutilizzo del codice, senza compromettere le caratteristiche di programmazione dichiarativa del Prolog. Logtalk è implementato in codice altamente portabile e utilizza i più moderni standard di conformità del Prolog rispetto al compilatore backend.

Per mantenere una dimensione ragionevole, questo tutorial presuppone necessariamente che il lettore abbia una conoscenza del linguaggio Prolog ed è inoltre focalizzato esclusivamente sulla descrizione delle caratteristiche object-oriented di Logtalk.

Sintassi

Logtalk utilizza la sintassi standard del linguaggio Prolog con l'aggiunta di un paio di operatori e di alcune direttive per una curva di apprendimento morbida e per assicurare ampia portabilità. Una conseguenza importante è che il codice Prolog può essere facilmente incapsulato in oggetti con poche o nessuna modifica. Inoltre, Logtalk può interpretare come oggetti Logtalk, in modo trasparente, la maggior parte dei moduli Prolog già esistenti.

I principali operatori sono:

Alcune delle più importanti entità e direttive saranno introdotte nelle sezioni successive.

Entità e Ruoli

Logtalk tratta gli oggetti, i protocolli e le categorie come entità di prima classe. I rapporti tra le entità definiscono i patterns of code reuse ossia i modelli di riutilizzo del codice e i roles ossia i ruoli svolti da tali entità. Ad esempio, quando un oggetto istanzia un altro oggetto, il primo oggetto assume il ruolo di istanza e il secondo oggetto assume il ruolo di classe. Una relazione di tipo extends tra due oggetti implica che entrambi gli oggetti svolgano il ruolo di prototipi, in cui uno di loro estende l'altro, che diventa quindi suo prototipo padre.

Definizione di un oggetto

Un oggetto incapsula le dichiarazioni e le definizioni dei predicati. Gli oggetti possono essere creati in modo dinamico, ma di solito sono dichiarati come statici e definiti nel codice sorgente. Un singolo file sorgente può contenere un qualsiasi numero di definizioni di entità. Ecco un semplice oggetto list che definisce un membro pubblico member/2:

:- object(list).

	:- public(member/2).
	member(Head, [Head| _]).
	member(Head, [_| Tail]) :-
		member(Head, Tail).

:- end_object.

Compilazione e il caricamento dei file sorgenti

Supponendo che il codice di cui sopra per l'oggetto list venga salvato in un file list.lgt, esso può essere compilato e caricato utilizzando il predicato predefiniti logtalk_load/1 o la sua abbreviazione {}/1, con il percorso del file come argomento (l'estensione può essere omessa):

?- {list}.
yes

In generale, le entità potrebbero avere dipendenze sulle entità definite in altri file di origine (ad esempio le entità di biblioteca). Per caricare un file e tutte le sue dipendenze, la soluzione consigliata consiste nel definire un file loader che carica tutti i file necessari per un'applicazione. Un file loader è semplicemente un file di origine, in genere denominato loader.lgt, che effettua chiamate ai predicati built-in logtalk_load/1-2, di solito da una direttiva initialization/1 per la portabilità e conformità agli standard. Caricatore file vengono forniti per tutte le librerie, strumenti ed esempi.

Inviare un messaggio ad un oggetto

L'operatore infisso ::/2 è usato per inviare messaggi ad un oggetto. Analogamente al Prolog, è possibile fare backtracking per le soluzioni alternative:

?- list::member(X, [1,2,3]).
X = 1 ;
X = 2 ;
X = 3
yes

Analogamente alla programmazione object-oriented, logtalk consente anche l'Incapsulamento. Un predicato può essere dichiarata pubblico, protetto o privato. Può anche essere local quando non esiste una direttiva specifica per esso all'interno dello scope. Per esempio:

:- object(scopes).

	:- private(bar/0).
	bar.

	local.

:- end_object.

Assumendo che l'oggetto è salvato nel file scopes.lgt:

?- {scopes}.
yes

?- catch(scopes::bar, Error, true).
Error = error(
	permission_error(access, private_predicate, bar/0),
	logtalk(scopes::bar, user)
)
yes

?- catch(scopes::local, Error, true).
Error = error(
	existence_error(predicate_declaration, local/0),
	logtalk(scopes::local, user)
)
yes

Quando il predicato in un messaggio non è noto per l'oggetto (il ruolo dell'oggetto determina le procedure di ricerca), si ha un errore. Per esempio:

?- catch(scopes::unknown, Error, true).
Error = error(
	existence_error(predicate_declaration, unknown/0),
	logtalk(scopes::unknown, user)
)
yes

Un punto fondamentale da capire è che le direttive che specificano il predicato nello scope specificano la semantica di chiamata (calling) del predicato, e non la semantica di definizione (definition). Ad esempio, se un oggetto ha il ruolo di una classe e dichiara un predicato privato, tale predicato può essere definito nelle sue sottoclassi e nelle istanze ma può essere chiamato solo nelle sue istanza (from) dalla classe.

Definizione e implementazione di un protocollo

Un Protocollo contiene le dichiarazioni dei predicati che possono essere implementati da un qualsivoglia numero di oggetti e categorie:

:- protocol(listp).

	:- public(member/2).

:- end_protocol.

:- object(list,
	implements(listp)).

	member(Head, [Head| _]).
	member(Head, [_| Tail]) :-
		member(Head, Tail).

:- end_object.

Lo scope dei predicati di un protocollo può essere ristretto usando implementazioni protected e private. Ad esempio:

:- object(stack,
	implements(private::listp)).

:- end_object.

Difatti, tutte le relazioni tra entità (nella direttiva di apertura di un entità) possono essere definite come public (default), protected, o private.

Prototipi

Un oggetto senza una istanza o senza una relazione di specializzazione con un altro oggetto interpreta il ruolo di prototipo. Un prototipo può estendere un altro oggetto, il suo prototipo genitore.

% clyde, our prototypical elephant
:- object(clyde).

	:- public(color/1).
	color(grey).

	:- public(number_of_legs/1).
	number_of_legs(4).

:- end_object.

% fred, another elephant, is like clyde, except that he's white
:- object(fred,
	extends(clyde)).

	color(white).

:- end_object.

Per rispondere ad un messaggio inviato ad un oggetto che ha il ruolo di prototipo, si cerca prima una risposta nel prototipo stesso e se il prototipo non sa rispondere si passa all'eventuale prototipo genitore (se esiste):

?- fred::number_of_legs(N).
N = 4
yes

?- fred::color(C).
C = white
yes

Un messaggio è valido se il relativo predicato è dichiarato in un oggetto (e se il mittente è nel campo di applicazione), ma fallirà, piuttosto che lanciare un errore, se il predicato non è definito. Questa è chiamata la closed-world assumption. Ad esempio, si consideri il seguente oggetto, salvato in un file foo.lgt:

:- object(foo).

	:- public(bar/0).

:- end_object.

Caricando il file e cercando di chiamare il predicato bar/0 questo fallisce come previsto. Si noti che ciò è diverso dal chiamare un predicato sconosciuto unknown, che invece genera un errore:

?- {foo}.
yes

?- foo::bar.
no

?- catch(foo::baz, Error, true).
Error = error(
	existence_error(predicate_declaration, baz/0),
	logtalk(foo::baz, user)
)
yes

Classi e istanze

Per definire gli oggetti nei ruoli di classi e/o istanze, un oggetto deve avere almeno un istanziazione o una relazione di specializzazione con un altro oggetto. Gli oggetti che hanno il ruolo di meta-classi possono essere utilizzati quando abbiamo bisogno di usare una classe come se fosse un'istanza. Il seguente esempio mostra come creare dinamicamente nuovi oggetti in fase di esecuzione:

% a simple, generic, metaclass defining a new/2 predicate for its instances
:- object(metaclass,
	instantiates(metaclass)).

	:- public(new/2).
	new(Instance, Clauses) :-
		self(Class),
		create_object(Instance, [instantiates(Class)], [], Clauses).

:- end_object.

% a simple class defining age/1 and name/1 predicate for its instances
:- object(person,
	instantiates(metaclass)).

	:- public([
		age/1, name/1
	]).

	% a default value for age/1
	age(42).

:- end_object.

% a static instance of the class person
:- object(john,
	instantiates(person)).

	name(john).
	age(12).

:- end_object.

Nel rispondere ad un messaggio inviato ad un oggetto ha assunto il ruolo di istanza, tal messaggio viene convalidato partendo dalla sua classe e andando a ritroso nella gerarchia, se necessario, fino alle sue superclassi. Supponendo che il messaggio sia valido, allora si cerca una risposta a partire dall'istanza stessa:

?- person::new(Instance, [name(paulo)]).
Instance = o1
yes

?- o1::name(Name).
Name = paulo
yes

?- o1::age(Age).
Age = 42
yes

?- john::age(Age).
Age = 12
yes

Categorie

Una categoria è un'unità atomica di codice riutilizzabile. Una categoria è usata per incapsulare una insieme coesivo (cohesive) di dichiarazioni e di definizioni di predicato ed è atta ad implementare una singola (single) funzionalità che può essere importata in qualsiasi oggetto. Una categoria può quindi essere concepita come il concetto duale di protocollo. Nel seguente esempio, si definiscono prima le categorie che rappresentano i motori di auto e poi si importano tali categorie negli oggetti auto:

% a protocol describing engine characteristics
:- protocol(carenginep).

	:- public([
		reference/1,
		capacity/1,
		cylinders/1,
		horsepower_rpm/2,
		bore_stroke/2,
		fuel/1
	]).

:- end_protocol.

% a typical engine defined as a category
:- category(classic,
	implements(carenginep)).

	reference('M180.940').
	capacity(2195).
	cylinders(6).
	horsepower_rpm(94, 4800).
	bore_stroke(80, 72.8).
	fuel(gasoline).

:- end_category.

% a souped up version of the previous engine
:- category(sport,
	extends(classic)).

	reference('M180.941').
	horsepower_rpm(HP, RPM) :-
		^^horsepower_rpm(ClassicHP, ClassicRPM),	% "super" call
		HP is truncate(ClassicHP*1.23),
		RPM is truncate(ClassicRPM*0.762).

:- end_category.

% with engines (and other components), we may start "assembling" some cars
:- object(sedan,
	imports(classic)).

:- end_object.

:- object(coupe,
	imports(sport)).

:- end_object.

Le Categorie sono compilate in modo indipendente e, quindi, consentono l'importazione di oggetti da aggiornare mediante il semplice aggiornamento delle categorie importate, senza richiedere pertanto la ricompilazione dell'oggetto. Le Categorie forniscono anche la runtime transparency, cioè il protocollo della categoria si aggiunge al protocollo degli oggetti che importano tale categoria:

?- sedan::current_predicate(Predicate).
Predicate = reference/1 ;
Predicate = capacity/1 ;
Predicate = cylinders/1 ;
Predicate = horsepower_rpm/2 ;
Predicate = bore_stroke/2 ;
Predicate = fuel/1
yes

Hot patching

Le categorie possono essere anche usate per modificare gli oggetti al volo (hot-patch). Una categoria può aggiungere nuovi predicati ad un oggetto e/o sostituire le definizioni dei predicati dell'oggetto. Ad esempio, si consideri il seguente oggetto:

:- object(buggy).

	:- public(p/0).
	p :- write(foo).

:- end_object.

Si supponga che l'oggetto stampi la stringa sbagliata quando riceve il messaggio p/0:

?- {buggy}.
yes

?- buggy::p.
foo
yes

Se il codice sorgente dell'oggetto non è disponibile e bisogna correggere l'applicazione che sta eseguendo il codice dell'oggetto, si può semplicemente definire una categoria che corregge il predicato non corretto:

:- category(patch,
	complements(buggy)).

	% fixed p/0 def
	p :- write(bar).

:- end_category.

Dopo la compilazione e il caricamento della categoria nell'applicazione in esecuzione si ottiene:

?- set_logtalk_flag(complements, allow).
yes

?- {patch}.
yes

?- buggy::p.
bar
yes

Poiché l'hot-patching interrompe forzatamente l'incapsulamento, è possibile impostare il flag del compilatore complementary può essere impostato (a livello globale o per un singolo oggetto) per consentire, limitare o prevenire l'hot-patching.

Oggetti Parametrici e Categorie

Gli oggetti e le categorie possono essere parametrizzati utilizzando come identificativo un compound-term al posto di un atomo. Oggetti e parametri di una categoria sono variabili logiche logical variables condivise con tutti i predicati incapsulati. Ecco un esempio con cerchi geometrici:

:- object(circle(_Radius, _Color)).

	:- public([
		area/1, perimeter/1
	]).

	area(Area) :-
		parameter(1, Radius),
		Area is pi*Radius*Radius.

	perimeter(Perimeter) :-
		parameter(1, Radius),
		Perimeter is 2*pi*Radius.

:- end_object.

Oggetti parametrici possono essere utilizzati come qualsiasi altro oggetto e di solito forniscono i valori da assegnare ai parametri quando si invia un messaggio:

?- circle(1.23, blue)::area(Area).
Area = 4.75291
yes

Gli oggetti parametrici forniscono anche un modo semplice per associare un insieme di predicati con un semplice predicato Prolog. Fatti Prolog possono essere interpretati come oggetti proxy parametrici ( parametric object proxies) quando hanno lo stesso funtore e arietà degli identificatori di oggetti parametrici. Per lavorare con i proxy viene fornita una sintassi maneggevole. Per esempio, si prendano le seguenti clausole per il predicato circle/2:

circle(1.23, blue).
circle(3.71, yellow).
circle(0.39, green).
circle(5.74, black).
circle(8.32, cyan).

Con queste clausole, si può facilmente calcolare, ad esempio, un elenco con le aree di tutti i cerchi:

?- findall(Area, {circle(_, _)}::area(Area), Areas).
Areas = [4.75291, 43.2412, 0.477836, 103.508, 217.468]
yes

In pratica, il costrutto {Goal}::Message prova il goal Goal, instanziando le variabili interne e inviando un messaggio Message al termine risultante.

Eventi and monitor

Logtalk supporta l'event-driven programming mediante la definizione di eventi e di monitor. Un evento è semplicemente l'invio di un messaggio ad un oggetto. Un monitor è un gestore di un evento. L'evento (con l'invio di un messaggio) è un'attività atomica, ed è preceduta da un evento before e da un evento after. Il monitor gestisce tali eventi mediante i predicati, before/3 e after/3, che sono chiamati rispettivamente prima e dopo il verificarsi dell'evento. Un monitor può inoltre interrogare, registrare e cancellare un evento nel registro eventi a livello di sistema il quale che associa gli eventi con i monitor. Ad esempio, un semplice tracer per ogni messaggio inviato utilizzando il costrutto ::/2 può essere definito come:

:- object(tracer,
	implements(monitoring)).    % built-in protocol for event handlers

	:- initialization(define_events(_, _, _, _, tracer)).

	before(Object, Message, Sender) :-
		write('call: '), writeq(Object), write(' <-- '), writeq(Message),
		write(' from '), writeq(Sender), nl.

	after(Object, Message, Sender) :-
		write('exit: '), writeq(Object), write(' <-- '), writeq(Message),
		write(' from '), writeq(Sender), nl.

:- end_object.

Supponendo che l'oggetto tracer e l'oggetto list definito in precedenza siano stati già compilati e caricati, si possono osservare i gestori di eventi in azione durante l'invio di un messaggio:

?- set_logtalk_flag(events, allow).
yes

?- list::member(X, [1,2,3]).

call: list <-- member(X, [1,2,3]) from user
exit: list <-- member(1, [1,2,3]) from user
X = 1 ;
exit: list <-- member(2, [1,2,3]) from user
X = 2 ;
exit: list <-- member(3, [1,2,3]) from user
X = 3
yes

Gli eventi possono essere impostati e cancellati dinamicamente in fase di esecuzione chiamando i predicati predefiniti define_events/5 e abolish_events/5.

La programmazione event-driven può essere vista come una forma di computational reflection. Si noti però che gli eventi sono generati solo quando si utilizza il costrutto di controllo per l'invio di messaggi ::/2.

Espressioni lambda

Logtalk supporta anche le espressioni lambda. I parametri della espressioni lambda sono rappresentati mediante una lista con l'operatore infisso (>>)/2 che collega i parametri alla relativa lambda espressione. Ecco alcuni semplici esempi che utilizzano la libreria meta.

?- {meta(loader)}.
yes

?- meta::map([X,Y]>>(Y is 2*X), [1,2,3], Ys).
Ys = [2,4,6]
yes

Logtalk supporta anche il currying:

?- meta::map([X]>>([Y]>>(Y is 2*X)), [1,2,3], Ys).
Ys = [2,4,6]
yes

Infine, le variabili libere Lambda possono essere espresso usando la sintassi estesa {Free1, ...}/[Parameter1, ...]>>Lambda.

Macro

I Termini e goal nel file sorgente possono essere estesi al momento della compilazione specificando una hook ad un oggetto (hook object) che definisce le regole di riscrittura dei termini e riscrittura dei quesiti. Ad esempio, si consideri il seguente oggetto semplice, salvato nel file source.lgt:

:- object(source).

	:- public(bar/1).
	bar(X) :- foo(X).

	foo(a). foo(b). foo(c).

:- end_object.

Si supponga il seguente hook all'oggetto, salvato nel file my_macros.lgt, che estende le clausole e chiama il predicato locale foo/1:

:- object(my_macros,
	implements(expanding)).    % built-in protocol for expanding predicates

	term_expansion(foo(Char), baz(Code)) :-
		char_code(Char, Code). % standard built-in predicate

	goal_expansion(foo(X), baz(X)).

:- end_object.

Dopo aver caricato il file contenente la macro, si può espandere il nostro file sorgente usando il flag del compilatore hook:

?- logtalk_load(my_macros), logtalk_load(source, [hook(my_macros)]).
yes

?- source::bar(X).
X = 97 ;
X = 98 ;
X = 99
true

La libreria Logtalk fornisce infine il supporto per combinare hook agli oggetti utilizzando diversi modi (ad esempio, definendo una pipeline di espansioni).

Maggiori informazioni

Visita il Sito web di Logtalk (en) per maggiori informazioni.


Hai un suggerimento? Oppure una correzione? Apri un issue sul GitHub, o fa un pull request da solo!

In origine contribuita da Paulo Moura, e modificata da 2 contributore(i).