Defining several similar classes by inheritance

objects
ppxlib
#1

Dear OCaml community,

Let say I have two classes a and b that define methods a#m and b#m (we can suppose that these methods overload the method O#m from a common ancestor o of the classes a and b if it helps).

I can define a class a_enriched, where I overload again the method m for composing its result with a given function f.

class a_enriched f = object
  inherit a as super

  method! m = f super#m
end

I can do the same with b_enriched.

class b_enriched f = object
  inherit b as super

  method! m = f super#m
end

The code that defines a_enriched and b_enriched is the same apart from the name of the classes.

Can I define the classes a_enriched and b_enriched without repeating most of their definitions? In my actual settings, I don’t have only one single method m but several methods to overload, and I would like be able to write things more concisely.


To give more context if it can help to find the correct solution, the two classes A and B are called in my particular use case lift_expr and lift_pattern, and derive from “lifters” obtained with the traverse_lift deriver from ppxlib.

class lift_expr loc = object(self)
  inherit [Parsetree.expression] Clang__bindings.lift
  ...
end

class lift_pattern loc = object(self)
  inherit [Parsetree.pattern] Clang__bindings.lift
  ...
end

and the code in ... is substantially different between the two classes.

My question comes from the need to enrich the behavior of both lifters by open recursion. Something like:

class lift_expr_enriched f loc = object
  inherit lift_expr loc as super

  method! qual_type e =
    match e with
    | <thing of interest>(x) -> f x
    | _ -> super#qual_type e
end

class lift_pattern_enriched f loc = object
  inherit lift_pattern loc as super

  method! qual_type e =
    match e with
    | <thing of interest>(x) -> f x
    | _ -> super#qual_type e
end

and I would like to write most of the duplicated code once.

Thank you!

#2

I found a solution by defining the enrichment in a functor: the enriched class should precisely match a given class type (I wonder if there exists a more general solution), but it fulfils my needs.

class type ['a] t = object
  method m : 'a list
end

class a = object
  method m = [42]
end

class b = object
  method m = ["foo"]
end

module type S = sig
  type t

  class c : [t] t
end

module Enrich (X : S) = struct
  class c = object
    inherit X.c as super

    method! m = super#m @ super#m
  end
end

module A_enriched = Enrich (struct type t = int class c = a end)

module B_enriched = Enrich (struct type t = string class c = b end)

class a_enriched = A_enriched.c

class b_enriched = B_enriched.c
#3

If I understood you correctly, then you’re not really using the open recursion in the functionality that you’re mixing in to your overloaded methods. I.e., the f function doesn’t really call any methods. You’re just using overriding as a way of adding this behavior.

If that is true, then there is a general solution, which unfortunately will require changing the base classes, i.e., the lift_expr and lift_pattern classes.

The idea is simple, the class methods instead of returning a concrete type shall instead be parametrized in the return type and become a functor, e.g.,

module Lift_expr(M : S) : sig
    class base : object
       method qual_type : expr -> 'a M.t
       ...
    end
end

Then, when you need to add an extra behavioral aspect to your computation, you can pass a different module M which can have arbitrary (bounded by the signature S behavior). You won’t need any inheritance.

Concerning the signature S, it could be just

module type T1 = sig
   type 'a t
end

But it won’t give you a lot of freedom. For example, if you will later decide to add some context aspect, or even some state, then you will not be able to do this. Therefore you may stick to Applicative or even to Monad. The latter will enable a lot of freedom, so that you can even change the execution order, add non-determinism. Moreover, you can even use Monad transformers to compose those behavioral aspects.

For a complete example of this solution, consider looking into BAP Evaluators Framework. It uses classes parametrized into monads.

Take a note, that they are deprecated, as classes usually make composability nearly impossible1, so we switched to more composable solutions which don’t involve any inheritance. Therefore, if you’re in control of the library, my advice would be to stick away from classes.


1) When you inherit from a class, you have to reason about all ancestors, as well as keep in mind all possible descendants. Therefore, if your class hierarchy is open, it is impossible to write roubst code, as you can’t really assume any behavior from any method. This limits applicability of classes into very small hierarchies, which are closed and used locally, inside a module and are hidden beyond the abstraction wall.

#4

Thank you very much, @ivg, for such a detailed explanations!

Unfortunately, it will not be easy to change the base classes: they are generated by the traverse_lift PPX deriver from ppxlib.

I think that there should at least be some constructors for values of type 'a t in order to be able to return a value of type 'a M.t in the methods of class base.

For my particular case, I solved the problem with a functor taking as parameter a module carrying the class to enrich.

module type Lifter = sig
  type t

  class lifter : object
    inherit [t] Clangml_lift.lift
    inherit [t] Ppxlib_traverse_builtins.std_lifters
  end
end

module Remove_placeholder (X : Lifter) = struct
  class lifter subst_payload map = object
    inherit X.lifter as super

    method expr (expr : Clang.Ast.expr) = ...

    method qual_type (qual_type : Clang.Ast.qual_type) = ...
  end
end

The concerns I have with the generality of this approach (even if it works in my case) is that the type of lifter in the signature Lifter is fully specified. It seems strange for me there seems to be no kind of subtyping available at the level of class types (we cannot call the functor Remove_placeholder with a class with more methods than those specified in the signature).

Interestingly, my problem has a nice solution in C++.

template<typename T, T f(T), class X>
class Derive : public X {
public:
  T m() { return f(X::m()); }
};

which can be used this way:

class C {
public:
  virtual int m() { return 1; }
};

static int f(int x) {
  return x * 2;
}

int
main(int argc, char* argv[])
{
  C c;
  Derive<int, f, C> c_prime;
  ...
}

What prevents to do that in OCaml is that there seems to be no way to parameterize a class by another class (since classes are not first-class values), and even if we may do that with a functor, this method is rigid in the type of the class.

#5

This makes sense, since templates in C++ operate as macros, i.e., on a syntactic level, and therefore,

template<typename T, T f(T), class X>
class Derive : public X {
public:
  T m() { return f(X::m()); }
};

Is a macro that generates a new module with different types every time you’re applying it. And only once a template is instantiated to a new module it is being typed and manifested to an object.

Contrary, despite looking syntactically close, OCaml functors are values, not patterns for generating code. You can, of course, implement the same approach in OCaml using ppx rewriters. But it will have the same issue as C++, instead of depending on abstractions you will be depending on syntactic constructs, i.e., instead of quantifying on what a module is doing your’re quantifying on how it looks.

#6

Indeed, there is no typing before instantiating the template, so it makes thing easier for C++.

However, I am not able to construct a counter-example that justifies the following restriction for class types (OCaml manual, 7.9.1, § Class body type).

Furthermore, all methods either virtual or public present in the class body must also be present in the class body type (on the other hand, some instance variables and concrete private methods may be omitted).

I would have liked to have a functor that accepts as argument a module with a class c containing at least a method m, and possibly other methods: it does not seem to be possible. (Again, it is more a theoretical question, I don’t need it actually in my case.)