In Maple 2021, it is now possible to use object:-method(arg)
notation. This makes is
easier to use OOP in maple. To do this, use _self
as follows
restart; module person() option object; local name::string:=""; local age::integer:=0; export get_name::static:=proc(_self,$) return _self:-name; end proc; export set_name::static:=proc(_self,name::string,$) _self:-name:=name; end proc; export get_age::static:=proc(_self,$) return _self:-age; end proc; export set_age::static:=proc(_self,age::integer,$) _self:-age:=age; end proc; end module;
And now make an object and use it as follows
o:=Object(person) o := Object<<1846759887808>> o:-get_name(); "" o:-get_age(); 0 o:-set_name("joe doe"); o:-get_name(); "joe doe"
Add ModuleCopy
proc in the class. This will automatically be called to initialize the
object.
Here is an example
restart; module ODE() option object; local ode:=NULL; local y::symbol; local x::symbol; local sol; export ModuleCopy::static := proc( _self::ODE, proto::ODE, ode, func, $ ) print("Initilizing object with with args: ", [args]); _self:-ode:= ode; _self:-y:=op(0,func); _self:-x:=op(1,func); end proc; export dsolve::static:=proc(_self,$) _self:-sol := :-dsolve(ode,y(x)); end proc; export get_sol::static:=proc(_self,$) return sol; end proc; end module;
And now make an object and use it as follows
o:=Object(ODE, diff(y(x),x)+y(x)=sin(x), y(x)); o:-dsolve(): o:-get_sol(); #y(x) = -1/2*cos(x) + 1/2*sin(x) + exp(-x)*_C1
So a constructor just makes it easier to initialize the object without having to make a
number of set()
calls to initialize each memeber data.
This is done using overload
with different ModuleCopy
proc in the class.
Here is an example. Lets make a constructor that takes an ode and initial conditions, and one that only takes an ode with no initial conditions.
restart; module ODE() option object; local ode:=NULL; local y::symbol; local x::symbol; local ic:=NULL; local sol; export ModuleCopy::static:= overload( [ proc( _self::ODE, proto::ODE, ode, func, $ ) option overload; _self:-ode:= ode; _self:-y:=op(0,func); _self:-x:=op(1,func); end proc, proc( _self::ODE, proto::ODE, ode, func, ic, $ ) option overload; _self:-ode:= ode; _self:-y:=op(0,func); _self:-x:=op(1,func); _self:-ic :=ic; end proc ] ); export dsolve::static:=proc(_self,$) if evalb(ic=NULL) then sol := :-dsolve(ode,y(x)); else sol := :-dsolve([ode,ic],y(x)); fi; end proc; export get_sol::static:=proc(_self,$) return sol; end proc; end module;
And now use it as follows
o:=Object(ODE, diff(y(x),x)+y(x)=sin(x), y(x), y(0)=0); o:-dsolve(): o:-get_sol(); #y(x) = -1/2*cos(x) + 1/2*sin(x) + 1/2*exp(-x) o:=Object(ODE, diff(y(x),x)+y(x)=sin(x), y(x)); o:-dsolve(): o:-get_sol(); #y(x) = -1/2*cos(x) + 1/2*sin(x) + exp(-x)*_C1
In the child class you want to extend from the parent class, add option object(ParentName);
Here is an example
restart; module ODE() option object; local ode; export set_ode::static:=proc(_self,ode,$) _self:-ode :=ode; end proc; export get_ode::static:=proc(_self,$) return _self:-ode; end proc; end module; #create class/module which extends the above module second_order_ode() option object(ODE); export get_ode_order::static:=proc(_self,$) return 2; end proc; end module;
In the above second_order_ode
inherts all local variables and functions in the ODE
class and
adds new proc. Use as follows
o:=Object(second_order_ode); #create an object instance o:-set_ode(diff(y(x),x)=sin(x)); o:-get_ode(); o:-get_ode_order();
Note that the child class can not have its own variable with the same name as the parent
class. This is limitation. in C++
for example, local variables in extended class overrides the
same named variable in the parent class.
Even if the variable have different type, Maple will not allow overriding. For example, this will fail
restart; module ODE() option object; local ode; local id::integer; export set_ode::static:=proc(_self,ode,$) print("Enter ode::set_ode"); _self:-ode :=ode; end proc; export get_ode::static:=proc(_self,$) return _self:-ode; end proc; end module; module second_order_ode() option object(ODE); local id::string; export get_ode_order::static:=proc(_self,$) return 2; end proc; end module; Error, (in second_order_ode) local `id` is declared more than once
There might be a way to handle this, i.e. to somehow exlicitly tell Maple to override parant class proc or variable name in the child. I do not know now. The above is using Maple 2021.1
This is called polymorphism in OOP. This is a base class animal_class which has
make_sound
method. This method acts just as a place holder (interface), which the
extending class must extends (override) with an actual implementation.
The class is extended to make cat class and implementation is made.
module animal_class() option object; export make_sound::static:=proc(_self,$) error("Not implemented, must be overriden"); end proc; end module; %--------------- module cat_class() option object(animal_class); make_sound::static:=proc(_self,$) #note. DO NOT USE export print("mewooo"); end proc; end module;
And now
o:=Object(animal_class); o:-make_sound(); Error, (in anonymous procedure) Not implemented, must be overriden
The above is by design. As the animal_class is meant to be extended to be usable.
my_cat:=Object(cat_class); my_cat:-make_sound(); "mewooo"
So the base class can have number of methods, which are all meant to be be have its implementation provided by an extending class. Each class which extends the base class must provide implementation.
Once a base class is extended, all methods in the base class become part of the extending class. So to call a base class method just use same way as if calling any other method in the extending class itself.
Here is an example.
module person() option object; local age::integer:=100; export ModuleCopy::static:= proc(_self,proto::person, age::integer,$) _self:-age := age; end proc; local base_class_method::static:=proc(_self,$) print("In base class method..."); end proc; end module; #---- extend the above class module young_person() option object(person); export process::static:=proc(_self,$) print("In young_person process"); _self:-base_class_method(); end proc; end module;
Here is an example of usage
o:=Object(young_person,20); o:-process() "In young_person process" "In base class method..."
The above in Maple 2023.1
A Maple Object can be used a record type in other languags, such as Ada or Pascal. This example shows how to define a local type inside a proc and use it as record.
restart; foo:=proc(the_name::string,the_age::integer)::person_type; local module person_type() #this acts as a record type option object; export the_name::string; export the_age::integer; end module; local person::person_type:=Object(person_type); person:-the_name:=the_name; person:-the_age:=the_age; return person; end proc; o:=foo("joe doe",100); o:-the_name; "joe doe" o:-the_age; 100
In the above person
is local variable of type person_type
. In the above example, the local
variable was returned back to user. But this is just an example. One can declare such
variables and just use them internally inside the proc only. This method helps one organize
related variables into one record/struct like type. The type can also be made global if
needed.
Suppose we have list L1 of objects and we want to copy this list to another list, say
L2. If we just do L2 = L1
then this will not make an actual copy as any changes
to L1 are still reflected in L2. The above only copies the reference to the same
list.
To make a physical copy, need to use the copy
command as follows
restart; module person_type() option object; export the_name::string:="doe joe"; export the_age::integer:=100; end module; L1:=[]; for N from 1 to 5 do o:=Object(person_type); o:-the_name:=convert(N,string); L1:= [ op(L1), o]; od: L2:=map(Z->copy(Z),L1):
Now making any changes to L1 will not affect L2. If we just did L2 = L1
then both will
share same content which is not what we wanted.
This is a basic example of using OOP in Maple to implement an ode solver. There is a base module called ode_base_class (I will be using class instead of module, as this is more common in OOP)
This program will for now support first order and second order ode’s only.
The base ode class will contain the basic operations and data which is common to both first order and second order ode’s.
Next, we will make a first order ode class, and second order ode class. Both of these will extend the base ode class.
Next, we will have more ode classes. For example, for first order ode, there will be linear first order ode class, and separable first order ode class, and Bernoulli ode class and so on. Each one these classess will extend the first order ode class which in turn extends the base ode class.
Same for second order ode’s. There will be second order constant coefficients ode class, and second order variable coefficient ode class and so on. Each one of these classes will extend the second order ode class which in turn extends the base ode class.
Let base_ode_class
be the base of an ode class which will contain all the neccessary
methods that are generic and applicable to an ode of any order and type.
These can be the ode expression itself, its order and any other data and operations which applicable to any ode type.
Now we want to create a first order class. This will have its own operations and private variables that are specific and make sense only to any first order ode.
Then these will be the first order separable ode class, which has methods that implement solving the ode using separable method and has other methods which makes sense only for first order separable ode. The following diagram is partial illustration of the is-A relation among possible classes.
First we define the base ode class and define private variables and method that are common to any ode type.
restart; module base_ode_class() option object; local the_ode; local the_order::integer; export get_ode::static:=proc(_self,$) RETURN(_self:-the_ode); end proc; export get_order::static:=proc(_self,$) RETURN(_self:-the_order); end proc; end module;
Note that the base ode class does not have constructor. Since it is meant to be extended only.
The following is the first order ode class.
module first_order_ode_class() option object(base_ode_class); local initial_conditions; local solution_found; export get_IC::static:=proc(_self,$) RETURN(_self:-initial_conditions); end proc; export get_solution::static:=proc(_self,$) RETURN(_self:-solution_found); end proc; export verify_solution::static:=proc(_self,$)::truefalse; #code to verify if the solution found is valid or not #using odetest() end proc; end module;
The following is the first order separable ode class which extends the above first order ode class.
module first_order_separable_ode_class() option object(first_order_ode_class); local f,g; #ode of form y'=f(x)*g(y) export ModuleCopy::static:= proc(_self,proto::first_order_separable_ode_class, ode, IC,$) _self:-the_ode := ode; _self:-initial_conditions := IC; end proc; export dsolve::static:=proc(_self,$) #solve the ode for now we will use Maple but in my code #I have my own solver ofcourse. _self:-solution_found:= :-dsolve([_self:-the_ode, _self:-initial_conditions]); end proc; end module;
In the above, when we create a instance of first_order_separable_ode_class
then it now
have the whole chain of classes into one. i.e. first order separable class extending the first
order class which in turn extends the base ode class. For example
o:=Object(first_order_separable_ode_class,diff(y(x),x)=3*sin(x)*y(x),y(0)=1) o:-dsolve(); o:-get_solution() #y(x) = exp(3)*exp(-3*cos(x)) o:-get_ode() #diff(y(x), x) = 3*sin(x)*y(x)
The above calls will all work, even though the first order separable class has no
method in it called get_ode
but it extends a class which does, hence it works as
it.
Now we will do the same for second order ode’s.
Advantage of this design, is that methods in base classes that are being extended can be reused by any class which extends them. Only methods that applies and specific to the lower level classes need to be implemented.
As we add more specific solvers, we just have to extend the base classes and the new solvers
just need to implement its own specific dsolve
and any specific methods and data that it
needs itself.
Ofcourse in practice the above design is not complete as is. The user should not have to specify which class to instantiate, as user does not care what the ode type or class it is. They just want to to do
o:=Object(ode_class,diff(y(x),x)=3*sin(x)*y(x),y(0)=1) o:-dsolve(); o:-get_solution()
To solve this problem we have to make a factory method which is called to make the correct instance of the class and return that to the user. The factory method figures out the type of ode and it creates the correct instance of the correct class and returns that. So the call will become
o := make_ode_object( diff(y(x),x)=3*sin(x)*y(x), y(0)=1) o:-dsolve(); o:-get_solution()
The function make_ode_object
above is the main interface the user will call to make an ode
object.
This will be explained next with examples. One possibility is to make the factory function a global function or better, a public method in a utility module. For now, it is given here as stadalone function for illustration. The user calls this method to make an object of the correct instance of the ode. Here is complete implementation of all the above including the factory method.
#factory method. Makes objects for users make_ode_object:=proc(ode::`=`,func::function(name)) local x,y,the_order; y:=op(0,func); x:=op(1,func); the_order := PDEtools:-difforder(ode,x); if the_order=1 then RETURN(first_order_ode_class:-make_ode_object(ode,func)); elif the_order=2 then #RETURN(second_order_ode_class:-make_ode_object(ode,func)); #implement later NULL; else error "Only first and second order ode's are currently supported"; fi; end proc: ######################### module base_ode_class() option object; local the_ode; local the_order::integer; #methods bound to the object export get_ode::static:=proc(_self,$) RETURN(_self:-the_ode); end proc; export get_order::static:=proc(_self,$) RETURN(_self:-the_order); end proc; end module: ################## module first_order_ode_class() option object(base_ode_class); local initial_conditions; local solution_found; #public factory method not bound to the object. export make_ode_object:=proc(ode::`=`,func::function(name)) local x,y,ode_type::string; y:=op(0,func); x:=op(1,func); ode_type:="separable"; #code here which determined first order ode type if ode_type="separable" then RETURN( Object(first_order_separable_ode_class,ode,func)); elif ode_type="linear" then RETURN( Object(first_order_linear_ode_class,ode,func)); fi; #more ode types added here end proc; #methods bound to the object export get_IC::static:=proc(_self,$) RETURN(_self:-initial_conditions); end proc; export get_solution::static:=proc(_self,$) RETURN(_self:-solution_found); end proc; export verify_solution::static:=proc(_self,$)::truefalse; #code to verify if the solution found is valid or not #using odetest() end proc; end module: ################## module first_order_separable_ode_class() option object(first_order_ode_class); local f,g; #ode of form y'=f(x)*g(y) export ModuleCopy::static:= proc(_self,proto::first_order_separable_ode_class,ode,func::function(name),$) _self:-the_ode := ode; end proc; export dsolve::static:=proc(_self,$) #print("Enter first_order_separable_ode_class:-dsolve"); #solve the ode for now we will use Maple but in my code #I have my own solver ofcourse. _self:-solution_found:= :-dsolve(_self:-the_ode); NULL; end proc; end module:
It is used as follows
o:=make_ode_object(diff(y(x),x)=sin(x)*y(x),y(x)); o:-dsolve(); o:-get_solution(); y(x) = c__1 exp(-cos(x))
Here, I will start making a complete small OOP ode solver in Maple.
At each step more classes are added and enhanced until we get a fully working small ode solver based on OOP design that solves a first and second order ode, this is to show how it all works. Another solvers can be added later by simply extending the base class.
The base class is called Base_ode_class
. There will be Second_order_ode_class
and
First_order_ode_class
and these classes extend the base ode class. We can later add
higher_order_ode_class
.
Next, there are different classes which extend these. There is First_order_linear_ode_class
and First_order_separable_ode_class
and so on, and these extend the
First_order_ode_class
.
For example, if a user wanted to solve a first order ode which happend to be say
separable, then object of class First_order_separable_ode_class
will be created and
used.
Since the user does not know and should not know what object to create, then the factory class will be used. The factory class is what the user initially calls to make the ode object.
It is the factory class which determines which type of class to instantiate based on the ode given.
The factory class is singleton (standard module in Maple, not of type object),
which has the make_ode
method which is called by the user. This method parses
the ode and determines its order and then based on the order determine which
subclass to use, and then instantiate this and returns the correct object to the user to
use.
This object will have the dsolve
method and other methods the user can use on the
object.
The make_ode
method in the factory module accepts only the ode itself the function such as
\(y(x)\). A typical use is given below
ode := ODE_factoy_class:-make_ode( diff(y(x),x)=sin(x), y(x) ); ode:-set_IC(....); ode:-set_hint("the hint"); ..... ode:-dsolve(); #solves the ode ode:-is_solved(); #checks if ode was successfully solved ode:-verify_sol(); #verifies the solution using maple odetest() ode:-is_sol_verified(); #asks if solution is verified ode:-get_number_of_sol(); #returns number of solutions, 1 or 2 etc... ode:-get_sol(); #returns the solutions found in list #and more method ...
Examples at the end will show how all the above works on actual odes’s.
The initial call to make an ode does not have initial conditions, or hint and any other parameters. This is so to keep the call simple. As the factory method only makes the concrete ode object.
Additional methods are then used to add more information if needed by using the returned object itself, such as initial conditions, and hint and so on before calling the dsolve method on the object.
Here is a very basic setup which include the base ode class and extended to first order two subclasses for now.
restart; ODE_factory_class :=module() #notice, normal module. No option object. export make_ode:=proc(ode::`=`,func::function(name),$) local dep_variables_found::list,item; local y::symbol; local x::symbol; local ode_order::integer; if nops(func)<>1 then error("Parsing error, dependent variable must contain one argument, found ", func); fi; y:=op(0,func); x:=op(1,func); if not has(ode,y) then error ("Supplied ode ",ode," has no ",y); fi; if not has(ode,x) then error ("Supplied ode ",ode," has no ",x); fi; if not has(ode,func) then error ("Supplied ode ",ode," has no ",func); fi; ode_order := PDEtools:-difforder(ode,x); #this will check that the dependent variable will show with #SAME argument in the ode. i.e. if y(x) and y(t) show up in same ode, it #will throw exception, which is what we want. try dep_variables_found := PDEtools:-Library:-GetDepVars([y],ode); catch: error lastexception; end try; #now go over dep_variables_found and check the independent #variable is same as x i.e. ode can be y'(z)+y(z)=0 but function is y(x). for item in dep_variables_found do if not type(item,function) then error("Parsing error. Expected ",func," found ",item," in ode"); else if op(1,item) <> x then error("Parsing error. Expected ",func," found ",item," in ode"); fi; fi; od; #now go over all indents in ode and check that y only shows as y(x) and not as just y #as the PDEtools:-Library:-GetDepVars([_self:-y],ode) code above does not detect this. #i.e. it does not check y'(x)+y=0 if numelems(indets(ode,identical(y))) > 0 then error("Parsing error, Can not have ",y," with no argument inside ",ode); fi; if ode_order=1 then RETURN(make_first_order_ode(ode,y,x)); elif ode_order=2 then RETURN(make_second_order_ode(ode,y,x)); else RETURN(make_higher_order_ode(ode,y,x)); fi; end proc; local make_first_order_ode:=proc(ode::`=`,y::symbol,x::symbol) #decide on what specific type the ode is, and make instant of it if first_order_ode_quadrature_class:-is_quadrature(ode,y,x) then RETURN(Object(first_order_ode_quadrature_class,ode,y,x)); elif first_order_ode_linear_class:-is_linear(ode,y,x) then RETURN(Object(first_order_ode_linear_class,ode,y,x)); fi; #and so on end proc; local make_second_order_ode:=proc(ode::`=`,y::symbol,x::symbol) #decide on what specific type the ode is, and make instant of it #same as for first order end proc; end module; #------------------------- module solution_class() option object; local the_solution; local is_verified_solution::truefalse:=false; local is_implicit_solution::truefalse:=false; export ModuleCopy::static:= proc(_self,proto::solution_class,the_solution::`=`,is_implicit_solution::truefalse,$) _self:-the_solution:=the_solution; _self:-is_implicit_solution:=is_implicit_solution; end proc; export get_solution::static:=proc(_self,$) RETURN(_self:-the_solution); end proc; export is_verified::static:=proc(_self,$) RETURN(_self:-is_verified_solution); end proc; export is_implicit::static:=proc(_self,$) RETURN(_self:-is_implicit_solution); end proc; export is_explicit::static:=proc(_self,$) RETURN(not(_self:-is_implicit_solution)); end proc; export verify_solution::static:= overload( [ proc(_self, ode::`=`,$) option overload; local stat; stat:= odetest(_self:-the_solution,ode); if stat=0 then _self:-is_verified_solution:=true; else if simplify(stat)=0 then _self:-is_verified_solution:=true; else _self:-is_verified_solution:=false; fi; fi; end, proc(_self, ode::`=`,IC::list,$) option overload; local stat; stat:= odetest([_self:-the_solution,IC],ode); if stat=[0,0] then _self:-is_verified_solution:=true; else if simplify(stat)=[0,0] then _self:-is_verified_solution:=true; else _self:-is_verified_solution:=false; fi; fi; end ]); end module: #------------------------- module ODE_base_class() option object; local y::symbol; local x::symbol; local func::function(name); #y(x) local ode::`=`; local ode_order::posint; local IC::list:=[]; local parsed_IC::list:=[]; local the_hint::string:=""; local solutions_found::list(solution_class):=[]; #exported getters methods export get_ode::static:=proc(_self,$) RETURN(_self:-ode); end proc; export get_x::static:=proc(_self,$) RETURN(_self:-x); end proc; export get_y::static:=proc(_self,$) RETURN(_self:-y); end proc; export get_ode_order::static:=proc(_self,$) RETURN(_self:-ode_order); end proc; export get_IC::static:=proc(_self,$) RETURN(_self:-IC); end proc; export get_parsed_IC::static:=proc(_self,$) RETURN(_self:-parsed_IC); end proc; export get_sol::static:=proc(_self,$) local L:=Array(1..0): local sol; for sol in _self:-solutions_found do L ,= sol:-get_solution(); od; RETURN(convert(L,list)); end proc; #exported setters methods export set_hint::static:=proc(_self,hint::string,$) #add code to check if hint is valid _self:-the_hint:=hint; end proc; end module; #------------------------- module first_order_ode_quadrature_class() option object(ODE_base_class); local f,g; #ode of form y'=f(x)*g(y) #this method is not an object method. It is part of the module but does #not have _self. It is called by the factory class to find if the ode #is of this type first export is_quadrature:=proc(ode::`=`,y::symbol,x::symbol)::truefalse; RETURN(true); #for now end proc; export ModuleCopy::static:= proc(_self,proto::first_order_ode_quadrature_class,ode::`=`,y::symbol,x::symbol,$) _self:-ode := ode; _self:-y := y; _self:-x := x; _self:-func := _self:-y(_self:-x); _self:-ode_order :=1; end proc; export dsolve::static:=proc(_self,$) local sol,o; #print("Enter first_order_ode_quadrature_class:-dsolve"); #solve the ode for now we will use Maple but in my code #I have my own solver ofcourse. sol:= :-dsolve(_self:-ode,_self:-func); o:=Object(solution_class,sol,false); _self:-solutions_found:= [o]; NULL; end proc; end module: #------------------------- module first_order_ode_linear_class() option object(ODE_base_class); local f,g; #ode of form y'=f(x)*g(y) #this method is not an object method. It is part of the module but does #not have _self. It is called by the factory class to find if the ode #is of this type first export is_linear:=proc(ode::`=`,y::symbol,x::symbol)::truefalse; RETURN(true); #for now end proc; export ModuleCopy::static:= proc(_self,proto::first_order_ode_linear_class,ode::`=`,y::symbol,x::symbol,$) _self:-ode := ode; _self:-y := y; _self:-x := x; _self:-func := _self:-y(_self:-x); _self:-ode_order :=1; end proc; export dsolve::static:=proc(_self,$) local sol,o; sol:= :-dsolve(_self:-ode,_self:-func); o:=Object(solution_class,sol,false); _self:-solutions_found[1]:= [o]: end proc: end module:
Example usage is
o:=ODE_factory_class:-make_ode(diff(y(x),x)=x,y(x)) o:-get_ode() d --- y(x) = x dx o:-dsolve(); o:-get_sol() [ 1 2 ] [y(x) = - x + c__1] [ 2 ]