Thursday, May 5, 2011

Visitor Design Pattern in OCaml

I am attempting to implement the Visitor Design Pattern using OCaml's OO constructs and type system and am running into problems upon instantiation of an Element.

class virtual ['hrRep] employee = object 
 method virtual receiveEvaluation : 'hrRep -> unit
 method virtual getName : string
end;;

class ['hrRep] accountant myName = object (self : 'a)
 inherit ['hrRep]employee
 val name = myName
 method receiveEvaluation rep = rep#visitAccountant self
 method getName = name
end;;

class ['hrRep] salesman myName = object (self : 'a)
 inherit ['hrRep]employee
 val name = myName
 method receiveEvaluation rep = rep#visitSalesman self
 method getName = name
end;;

class virtual ['accountant, 'salesman] hrRep = object (self)
 method virtual visitSalesman : 'salesman -> unit
 method virtual visitAccountant : 'accountant -> unit
end;;

class ['employee, 'salesman] lowerLevelHRRep = object (self) inherit ['employee, 'salesman]hrRep
 method visitSalesman s = print_endline ("Visiting salesman "^s#getName)
 method visitAccountant a = print_endline ("Visiting accountant "^a#getName)
end;;

let s1 : (< visitSalesman : 'a -> unit>) salesman = new salesman "Bob";;
let a1 : (< visitAccountant : 'a -> unit>) accountant = new accountant "Mary";;
let s2 : (< visitSalesman : 'a -> unit>) salesman = new salesman "Sue";;

let h1 : (< getName : string>, < getName : string>) lowerLevelHRRep = new lowerLevelHRRep;;
s1#receiveEvaluation h1;;

The error I get upon compilation is,

"The type of this expression, unit; _.. > salesman as 'a, contains type variables that cannot be generalized"

, however, the code compiles minus the line instantiating the salesman. How do I go about instantiating the salesman while maintaining the classes' functionality?


Edit: Error received with call to receiveEvaluation: This expression has type (, < getName : string>) lowerLevelHRRep but is here used with type unit > as 'a. The second object type has no method visitAccountant

From stackoverflow
  • EDIT - Separated the answer in 3 main points: the resolution of the initial compile error, a recursive solution, and a parametrized solution

    Resolution of the compile error

    Note that your code works fine in the top level:

    # let s = new salesman ();;
    val s : < visitSalesman : 'a -> unit; _.. > salesman as 'a = <obj>
    

    This kind of compile error is generally solved by adding a type annotation to help the compiler figuring the type. As the top level kindly told us what it was, we can modify the instantiation:

    let s : (< visitSalesman : 'a -> unit>) salesman = new salesman ();;
    

    And this compiles!

    A recursive solution

    It is possible to reduce complexity by using recursive classes. This totally removes the need for parametrized classes, but means that all objects need to be defined in the same source file.

    class virtual employee =
    object
      method virtual receiveEvaluation:(hrrep -> unit)
    end
    
    and accountant = 
    object(self)
      inherit employee
      method receiveEvaluation:(hrrep -> unit) = fun rep -> rep#visitAccountant (self :> accountant)
    end
    
    and salesman = 
    object (self)
      inherit employee
      method receiveEvaluation:(hrrep -> unit) = fun rep -> rep#visitSalesman (self :> salesman)
    end
    
    and hrrep = 
    object
      method visitSalesman:(salesman -> unit) = fun s -> print_endline ("Visiting salesman")
      method visitAccountant:(accountant -> unit) = fun s -> print_endline ("Visiting accountant")
    end
    
    let s = new salesman;;
    let e = (s :> employee);;
    let v = new hrrep;;
    
    e#receiveEvaluation v;;
    

    This prints "Visiting salesman". The coercion to employee is just to make this closer to a real world scenario.

    A parametrized solution

    Looking at the problem again, I think it is not necessary to have a parametrized hrRep, because at this moment, all other types are known. By just making the employee class parametrized, I get this:

    class virtual ['a] employee = 
    object
      method virtual receiveEvaluation : 'a -> unit
      method virtual getName : string
    end
    
    class ['a] accountant name =
    object(self)
      inherit ['a] employee
      val name = name
      method receiveEvaluation rep = rep#visitAccountant self
      method getName = "A " ^ name
    end
    
    class ['a] salesman name =
    object(self)
      inherit ['a] employee
      val name = name
      method receiveEvaluation rep = rep#visitSalesman self
      method getName = "S " ^ name
    end
    
    class virtual hrRep = 
    object
      method virtual visitAccountant : hrRep accountant -> unit
      method virtual visitSalesman : hrRep salesman -> unit
    end
    
    class lowerLevelHRRep =
    object
      inherit hrRep
      method visitAccountant a = print_endline ("Visiting accountant " ^ a#getName)
      method visitSalesman s = print_endline ("Visiting salesman " ^ s#getName)
    end;;
    
    let bob = new salesman "Bob";;
    let mary = new accountant "Mary";;
    let sue = new salesman "Sue";;
    let h = new lowerLevelHRRep;;
    bob#receiveEvaluation h;;
    mary#receiveEvaluation h;;
    sue#receiveEvaluation h;;
    

    This returns:

    Visiting salesman S Bob

    Visiting accountant A Mary

    Visiting salesman S Sue

    The advantage of this solution is that employees do not need to know about the visitor, and therefore can be defined in their own compilation units, leading to cleaner code and less recompilation to do when adding new types of employees.

    Mat : That seems to have resolved that compilation issue, but I get a similar one when I try to compile with a call to a function in a salesman object. How do I go about calling the function? Thanks again!
    small_duck : Not sure I understand this issue, can you post some code? Also, I added a hopefully simpler (albeit limited) solution using recursive definitions. Hope it helps!
    Mat : Yours is a much more elegant solution to accomplish the same thing. I've updated the code in the original question if you believe there is a means to accomplish what I asked in the revision.
    small_duck : The problems might have been due to the parametrization of hrRep, I posted a parametrized solution on employee only that seems to do the trick.

0 comments:

Post a Comment