Free Object Algebras
What's better than a double entendre? A triple entendre. So beware: these algebras are free as in beer.
The idea of object algebras isn't as well known as it should be. I'm going to discuss them briefly, as well as related topics like visitors and the
expression problem. Everything I'll say is well known, except perhaps the fact that the Newspeak programming language automatically gives you the benefits of object algebras without you having to do anything special or even know what object algebras are. It gives you object algebras for free.
Object Algebras were introduced by Bruno Oliveira and William Cook as a solution to the expression problem in languages that don't provide more advanced constructs, like mixins or virtual classes. If you don't know about the expression problem, you can read
the previous post, that explains the problem and how Newspeak deals with it.
An Object Algebra implements a generic factory interface. Instead of factory methods that produce instances of specific classes, the interface uses one or more type parameters. These abstract over what exactly is being manufactured by the factory.
interface Employees<E, M> {
employee(n: String, m: M): E
manager(n: String, rs: List<E>, m: Manager): M
}
The point of the abstraction is that we can use the same interface to produce many different things. Thus, code that used a regular factory to produce (say) a given expression tree, can be used to produce a derivative of that tree - for example, its value or printed representation. The code just abstracts over the "factory".
overseer := <E,M>fun(es: Employees<E, M>): M {
return es.manager('pointy-head-boss', [es.employee('catbert'), es.employee('dilbert')], ceo);
}
Here are two example factories. One is truly a factory for Employee objects.
class EmployeesImpl implements Employees<Employee, Manager> {
employee(n: String, m: Manager): Employee {return new Employee(n, m);}
manager(n: String, rs: List<Employee>, m: Manager): Manager {
return new Manager(n, rs, m);
}
}
The other computes the number of reports of an employee.
class TotalReportsNumber implements Employees<Integer, Integer> {
employee(n: String, m: Manager): Integer {return 0;}
manager(n: String, rs: List<Employee>, m: Manager): Integer {
return sum(map(fun(x) {return x(self) + 1}, rs);}
}
Here are some sample usages
phead: Manager := overseer(EmployeesImpl);
totalReports: Integer := overseer(TotalReportsNumber);
Alternate factories like TotalReportsNumber are very similar to visitors, but don't require our original classes to include a built-in accept() method. So we can easily add new "functions" by defining new factories.
If we want to add types, we just add them, and declare an extended subinterface with added constructor methods for the new types.
interface Consultants <E, M, C> extends Employees<E, M> {
consultant(n: String, m: M): C
}
To process them, we can declare new subclasses of our existing factories with additional functions.
class ConsultantImpl extends EmployeeImpl implements Consultants <Employee, Manager, Consultant> {
consultant(n: String, m: Manager): Consultant {return new Consultant(n, m);}
}
class ConsultantTotalReports extends TotalReportsNumber implements Consultants <Integer, Integer, Integer > {
consultant(n: String, m: Manager): Integer {return -1;}
}
Object Algebras leverage the duality between types and functions. We encode our types/classes as constructor functions: manager() for Manager, employee() for Employee etc. The functions that process these types are encoded as classes (the object algebras), and represented at runtime by instances of these classes. Objects generated by constructors (like overseer) are encoded as functions or closures (such as overseer()) parameterized by factory/object algebra instances.
Object algebras are very clever. I argue they're too clever. What if there was an easier way?
Here's the class EmployeesImpl in Newspeak
Newspeak3
'Root'
class EmployeesImpl new = Object new (
) (
public class Employee name: n <String> manager: m <Manager> = Object new (
|
public name <String> = n.
public overseer <Manager> = m.
|
) (
) : ()
public class Manager name: n <String> reports: rs <List[Employee]> manager: m <Manager> = Object new (
|
public reports <List[Employee]> = rs.
|
) (
) : ()
) : ()
Caveat: The Newspeak version doesn't enforce the types. Typechecking Newspeak is a side project I have never completed, which tells you that isn't trivial (due to class hierarchy inheritance), but also that it is not all that critical.
In Newspeak, nested classes implicitly define accessor methods, so our nested classes already act like the constructor functions.
Now, if we need to add a type, we simply subclass EmployeesImpl.
public class EmployeesAndConsultantsImpl new = EmployeesImpl new (
) (
public class Consultant name: n <String> manager: m <Manager> = Employee name: n
manager: m (
) (
public consultee ^<Manager> = (
^overseer
)
) : (
)
) : (
)
Note that EmployeesAndConsultantsImpl is itself a nested class. Why? Newspeak has no global namespace, so we cannot reference subclass EmployeeImpl as a superclass of a top level class. Hence we cannot define EmployeesAndConsultantsImpl as a top level class. Instead, we define it within a top level class, ConsultantsExtension, that is parameterized with respect to EmployeesImpl. You might say this is awkward, but in reality, it helps ensure the modularity we are seeking.
The absence of a global namespace is essential in an object capability language. Security and modularity are closely related.
Object algebras act as factories for objects, and objects are represented by functions parameterized with respect to their factories - the object algebra that creates them. In our Newspeak code, the object creation code will also be parameterized with respect the factories of those objects - the class(es) that produce them.
If we need to add a function, we define a subclass of EmployeesImpl where each type defined within EmployeesImpl is overridden to extend itself with the new function.
You see the same pattern of parameterizing over the Employee factory used.
public class ReportsTotaler new = EmployeesImpl new (
) (
public class Manager name: n <String> reports: rs <List[Employee]> manager: m <Manager> = super Manager name: n
reports: rs
manager: m (
) (
public totalReportsNumber ^<Integer> = (
^reports inject: 0
into: [:sum <Integer> :x <Employee> | sum + x totalReportsNumber + 1 ]
)
) : (
)
public class Employee name: n <String> manager: m <Manager> = super Employee name: n
manager: m (
) (
public totalReportsNumber ^<Integer> = (
^0
)
) : (
)
) : (
)
And of course, we can define a new class that extended EmployeesAndConsultantImpl with a nested Consultant class that could total its reports.
public class ReportsTotaler new = EmployeesAndConsultantsImpl new (
) (
public class Consultant name: n <String> manager: m <Manager> = super Consultant name: n
manager: m (
) (
public totalReportsNumber ^<Integer> = (
^-1
)
) : (
)
) : (
)
We can compose all of these for a complete solution. The method aCompleteSolution is defined in this document, and has all the necessary parts in scope.
You can inspect the document object using the document menu at the top.
aCompleteSolution = (
|
ec = ConsultantExtension of: (TotalReportExtension of: EmployeesImpl) ReportsTotaler.
ecr = ConsultantReportExtension of: ec EmployeesAndConsultantsImpl.
oa = ecr ReportsTotaler new.
ceo = oa Manager name: 'C.E. Oh' reports: platform collections List new manager: nil.
pointy_head_boss = oa Manager name: 'P.H. Negative' reports: platform collections List new manager: ceo.
catbert = oa Employee name: 'Catbert' manager: pointy_head_boss.
dilbert = oa Employee name: 'Dilbert' manager: pointy_head_boss.
consultant = oa Consultant name: 'Anon' manager: ceo.
|
pointy_head_boss reports add: dilbert; add: catbert.
ceo reports add: pointy_head_boss; add: consultant.
^pointy_head_boss totalReportsNumber.
)
In Newspeak every nested class has a corresponding method. Hence if we define a set of classes inside a single outer class, the outer class is exactly a factory for them, much like an object algebra. Newspeak's modularity means that we naturally write code that accepts instances of the outer class as parameters, and therefore produces different things depending on which outer class we feed it. You don't need visitors, nor do you need object algebras. This sort of modular design happens automatically.