PreviousUpNext

5.4.25  Experimental Object Oriented Programming Support

Background

Object oriented programming ("oop") in ML family languages has a long and vexed history.

Authorities as eminent as Robert Harper, co-author of The Definition of Standard ML have concluded that SML is better off without oop. The standard SML programming style solves problems in a quite different style which works just fine; trying to mix oop and ML is like trying to mix oil and water.

On the other hand eminent researcher Xavier Leroy has successfully implemented a flavor of oop in Ocaml and reports that it yields significant benefits in parts of the OCaml compiler implementation. Ocaml users use oop in only about ten percent of their programs, however; most of the time they seem content to stick to traditional ML programming style.

Similarly, eminent SML researcher John H Reppy has collaborated with Kathleen Fisher on the experimental language Moby which combines ML, oop and concurrent programming support.

Much of the design problem revolves around how to reconcile oop semantics with the ML type system. Subclassing is difficult to describe in type terms without subtyping, but adding subtypes to the ML type system is a major increment in complexity which can easily push the type deduction problem over the undecidability cliff.

This release of Mythryl contains a first-cut implementation of the approach to oop in ML outlined by Bernard Berthomieu in his March 2000 paper OO Programming Styles in ML. The focus of this paper is upon supporting oop without changes to the type system or core language; in principle, all the techniques he describes can be coded up by hand without any changes to the compiler at all. This provides complete confidence that no violence is being done to the core semantics of the language. The resulting code is however quite unwieldy; compiler support to autogenerate most of the code can reduce by 90application programming.

Berthomieu’s core idea is to express objects as tuples with one unspecified component which may be filled in by subclasses.

For example, we might have an object with state

    Self(X) = (Int, String, Float, X);

where the Int, String and Float components are the local state and the wildcard X component can be defined by subclasses any way they please. The subclasses will in turn provide a Y component for their subclasses to define, and so forth. The state for a given object then becomes a chain of tuples with one link for the class plus one for each of its superclasses.

Methods for a given class may then be written to take arguments of type Self(X), taking advantage of Mythryl’s don’t-care parametric polymorphism to operate equally well on instances of the class itself or of any subclass of it; state components belonging to subclasses are simply ignored.

This idea becomes fairly complicated by the time it is worked out in detail while providing implementation hiding, late binding, method overriding and so forth; I refer the really interested reader to Berthomieu’s paper, and turn instead to the user’s eye view of the current Mythryl implementation which is based upon his "simple dispatch with embedded methods" approach, section 4.2 and appendix A2.3.2.

A Mythryl class is a Mythryl package with special sauce; all regular package functionality is supported, plus additional constructs specific to object-oriented programming. Internally the compiler converts a class into a standard package during typechecking; the rest of the compiler knows nothing about oop.

A simple stand-alone counter class

Let us start by re-implementing the simple counter class from the "roll your own oop" tutorial:

    #!/usr/bin/mythryl
    class counter {

        # Declare our object's state field,
        # an integer counter:
        # 
        field my  Ref(Int)  counter = REF 0;

        # Define a message which returns the
        # value of our object's counter:
        #
        message fun    Self(X) -> Int
            get self
                =
                *(self->counter);

        # Define a message which increments the
        # value of our object's counter:
        #
        message fun    Self(X) -> Void
            increment self
                =
                self->counter := *(self->counter) + 1;

        # Define a message which resets the
        # value of our object's counter:
        #
        message fun    Self(X) -> Void
            reset self
                =
                self->counter := 0;


        # Define a nice function for creating instances
        # of this class.  The make__object() function
        # is autosynthesized by the compiler:
        #
        fun new ()
            =
            make__object ((), ());
    };


    # Demonstration of counter use:
    print "\n";

    include package   counter;

    counter = new ();      printf "State of counter after creation  d=%d\n" (get counter);
    increment counter;     printf "State of counter after increment d=%d\n" (get counter);
    reset counter;         printf "State of counter after reset     d=%d\n" (get counter);

When run this will produce as before

    linux$ ./my-script

    State of counter after creation  d=0
    State of counter after increment d=1
    State of counter after reset     d=0

Points to note:

I do not suggest that you try understanding the following in detail (although you might find it an interesting and educational exercise), but just to give you some rough idea of what (and how much!) code the compiler is synthesizing for you, here is approximately what the above turns into after the oop constructs have been re-expressed in vanilla Mythryl. I have added explanatory comments. Note that synthesized identifiers by convention incorporate a double underline to reduce risk of unexpected interactions with user-defined identifiers.

NB: The following code depends on support code from src/lib/src/object.pkg and src/lib/src/oop.pkg.

    # class 'counter' expands into package 'counter':
    #
    package counter {

        package dummy__oop__ref = oop;          # Force support library 'oop' to load.
        package dummy__object_ref = object;     # Force support class 'object' to load.

        package super = object;


        # Berthomieu's approach requires that
        # type Full__State(X) be opaque, so
        # we declare it in an internal package
        # which we can strong-seal with an
        # appropriate api (thus making Full__State(X)
        # opaque) and then 'include' back into the
        # main package.  This is a useful general trick:
        #
        package oop__internal
            :  
            api {

                # The full state record for a class consists of
                # its own local state plus a slot of type X for
                # whatever state a subclass of us might want.
                # Here in the API we declare it as an opaque
                # type, which gives us implementation hiding
                # as well as the type abstraction we need to
                # make object typing work properly:
                #
                Full__State(X);

                # The formal type for instances of this class
                # consists of the type for instances of our
                # superclass (which in this case defaults to
                # 'object' since we did not specify one explicitly)
                # with our Full__State(X) record plugged into its
                # subclass slot:
                # 
                Self(X) =  object::Self(Full__State(X));

                # Myself is Self(X) where X has been resolved to
                # a null pointer (no subclass state).  This is used
                # only when someone creates an instance of this
                # class specifically (as opposed to any subclass):
                #
                Myself =  Self(oop::Oop_Null);

                # Our local object state is split between two records,
                # one holding our fields and one holding our methods:
                #
                Object__Fields(X)  =  { counter: Ref(Int) };
                Object__Methods(X) =  { get:       Self(X) -> Int,
                                        increment: Self(X) -> Void, 
                                        reset:     Self(X) -> Void
                                      };

                # For class 'counter' we specified only one field,
                # with a fixed identifier, but in general we may
                # have multiple fields, some of which are initialized
                # to values supplied to 'make__object' rather than
                # specified in the field declaration.  This record
                # defines one entry for each field whose declaration
                # lacks an initializer:
                #
                Initializer__Fields(X) =  { };

                # Now we declare our three message functions:
                #
                get:         Self(X) -> Int; 
                increment:   Self(X) -> Void; 
                reset:       Self(X) -> Void; 

                # pack__object is the general routine for
                # creating an instance of this class, also
                # called by subclasses to create our part
                # of their state:
                # 
                pack__object:   (Initializer__Fields(X), Void) -> X -> Self(X); 

                # make__object is the general routine for
                # creating an instance of this class specifically.
                # it just calls pack__object, specifying a null
                # subclass state:
                #
                make__object:   (Initializer__Fields(X), Void) -> Myself; 

                # unpack__object is the general routine which our
                # subclasses call to get access to their local state:
                #
                unpack__object:   Self(X) -> (X -> Self(X), X); 

                # get__substate is a streamlined version of unpack__object
                # for use when the ability to recompose the object is not
                # needed:
                #
                get__substate:   Self(X) -> X; 

                # get__fields is a local function for
                # getting access to our own field record.
                # It calls super::get__substate():
                #
                get__fields:   Self(X) -> Object__Fields(X); 

                # get__fields is a local function for
                # getting access to our own methods record.
                # It calls super::get__substate():
                #
                get__methods:   Self(X) -> Object__Methods(X); 

                # make_object__fields combines initialization
                # information from declared field initializers
                # and those supplied via make__object to produce
                # a full fields record for a new object:
                # 
                make_object__fields:   Initializer__Fields(X) -> Object__Fields(X); 

                # For each message defined by the user, we
                # define an override function used to specify
                # a replacement method function implementing it:
                #
                override__get:         ((Self(X) -> Int ) -> Self(X) -> Int ) -> Self(X) -> Self(X);
                override__increment:   ((Self(X) -> Void) -> Self(X) -> Void) -> Self(X) -> Self(X);
                override__reset:       ((Self(X) -> Void) -> Self(X) -> Void) -> Self(X) -> Self(X);
            }
            =
            package {

                # Our local object state consists of a pair of records,
                # one for fields, one for methods.  Its type is mutually
                # recursive with that of our other major types:
                #
                Object__State(X)
                    =
                    OBJECT__STATE { object__fields:  Object__Fields(X), 
                                    object__methods: Object__Methods(X)
                                  }
                    withtype Full__State(X) = (Object__State(X), X)
                    also     Self(X) = super::Self(Full__State(X))
                    also     Myself = Self(oop::Oop_Null)
                    also     Object__Methods(X) = { get:       Self(X) -> Int,
                                                    increment: Self(X) -> Void, 
                                                    reset:     Self(X) -> Void
                                                  }
                    also     Object__Fields(X) = { counter: Ref(Int) }
                    also     Initializer__Fields(X) = { };

                # Convenience function to access our fields record:
                #
                fun get__fields (self: Self(X))
                    =
                    {   my (OBJECT__STATE { object__methods, object__fields }, substate)
                            =
                            super::get__substate  self;

                        object__fields;
                     };

                # Convenience function to access our methods record:
                #
                fun get__methods (self: Self(X))
                    =
                    {   my (OBJECT__STATE { object__methods, object__fields }, substate)
                            =
                            super::get__substate  self;

                        object__methods;
                     };

                # We can't make make__object mutually recursive
                # with our method functions because Mythryl doesn't
                # generalize mutually recursive functions, and it
                # is essential that our message and method functions
                # be generalized, and make__object has to be defined
                # after them in order to have them in scope for building
                # the object__methods record, so we have a little hack
                # where we backpatch a pointer to make__object into a
                # reference which is in-scope to the method functions:
                #
                make__object__ref = REF NULL: Ref(Null_Or(((Initializer__Fields(X), Void) -> Myself)));
                fun make__object arg = (the *make__object__ref) arg;

                # Next come the actual method functions supplied by the user:
                # 
                fun get self
                    =
                    *(.counter (get__fields self));

                fun increment self
                    =
                    (.counter (get__fields self))
                        :=
                        *(.counter (get__fields self)) + 1;

                fun reset self
                    =
                    .counter (get__fields self) := 0;

                # With the methods defined, we can now
                # set up our object__methods record:
                #
                object__methods = { get, increment, reset };

                # Next come the synthesized message functions which
                # look up and invoke the method functions via the
                # object__methods record stored in the recipient
                # object.  The fact that all Mythryl functions
                # logically take exactly one argument and return
                # exactly one result makes life easy for us here:
                #
                fun get (self: Self(X))
                    =
                    {    object__methods = get__methods self;
                         object__methods.get self;
                    };
                fun increment (self: Self(X))
                    =
                    {    object__methods = get__methods self;
                         object__methods.increment self;
                    };
                fun reset (self: Self(X))
                    =
                    {    object__methods = get__methods self;
                         object__methods.reset self;
                    };

                # The synthesized function which constructs
                # the object__fields record for a new object:
                #
                fun make_object__fields (init: Initializer__Fields(X))
                    =
                    { counter => REF 0 };

                # Next the synthesized function to create our
                # portion of a new object.  We use this locally
                # and it also gets invoked by our subclasses:
                #
                fun pack__object (fields_1, fields_0) substate
                    =
                    {   object__fields
                             =
                             make_object__fields  fields_1;

                        self = (super::pack__object ())
                                 ( OBJECT__STATE { object__fields, object__methods },
                                   substate
                                 );
                        self;
                    };

                # Now the function to create an instance specifically
                # of our own class, not of any subclass.  This is just
                # pack__object with a null subclass state:
                #
                fun make__object fields_tuple
                    =
                    pack__object fields_tuple oop::OOP_NULL;

                # Backpatch the above-mentioned reference so that
                # method functions can call make__object:
                #
                                                        my _ =
                make__object__ref
                    :=
                    THE make__object;

                # This function lets our subclass decompose us in a
                # way which allows later recomposition with changes.
                #
                # All the work is done by a helper function from
                # package 'oop':
                #
                fun unpack__object me
                    =
                    oop::unpack_object  (super::unpack__object  me);

                # This is a version of the above which is more efficient
                # because it doesn't do the work needed to allow
                # recomposition:
                #
                fun get__substate me
                    =
                    {   my (state, substate) = super::get__substate me;
                        substate;
                    };

                # Finally, the three synthesized functions
                # which allow our subclasses to override
                # methods inherited from us.  'new_method'
                # is the method which is to replace the
                # existing one.  We pass the existing
                # method function to 'new_method' so that
                # it can use it if desired:
                #
                fun override__get  new_method  me
                    =
                    oop::repack_object
                        (\\ (OBJECT__STATE { object__methods, object__fields })
                            =
                            OBJECT__STATE
                              { object__fields,
                                object__methods => { get       => new_method object__methods.get,       # Update this method.
                                                     increment =>            object__methods.increment, # Copy this method over unchanged.
                                                     reset     =>            object__methods.reset      # Copy this method over unchanged.
                                                   }
                              }
                        )
                        (super::unpack__object me);

                fun override__increment  new_method  me
                    =
                    oop::repack_object
                        (\\ (OBJECT__STATE { object__methods, object__fields })
                            =
                            OBJECT__STATE
                              { object__fields,
                                object__methods => { get       =>            object__methods.get,       # Copy this method over unchanged.
                                                     increment => new_method object__methods.increment, # Update this method.
                                                     reset     =>            object__methods.reset      # Copy this method over unchanged.
                                                   }
                              }
                        )
                        (super::unpack__object me);

                fun override__reset new_method me
                    =
                    oop::repack_object
                        (\\ (OBJECT__STATE { object__methods, object__fields })
                            =
                            OBJECT__STATE
                              { object__fields,
                                object__methods => { get       =>            object__methods.get,       # Copy this method over unchanged.
                                                     increment =>            object__methods.increment, # Copy this method over unchanged. 
                                                     reset     => new_method object__methods.reset      # Update this method.
                                                   }
                              }
                        )
                        (super::unpack__object me);


            };                                  # package oop__internal

        # Import the contents of the above
        # package back into the main 'counter'
        # package, strong-sealed by the API.
        # This makes Full__State(X) fully abstract:
        #
        include package   oop__internal;

        # Remaining user code is left exactly as-is:
        # 
        fun new ()
            =
            make__object ((), ());
    };                                          # package counter

As you can see, the fully expanded form of counter is ten times longer than the class form; while it is possible to implement Berthomieu’s oop recipe entirely by hand, it is definitely nice to have the compiler do most of the busywork.

Subclassing

Now we explore subclassing. We will start with a very simple base class with two string-valued fields, plus two methods which return the values of those fields. One field will have a value fixed by a declaration initializer, the other will have a value supplied at object creation time:

    #!/usr/bin/mythryl

    class test {

        field my  String  string_1a = "abc";
        field my  String  string_1b;

        message fun          Self(X) -> String
            get1a self
                =
                self->string_1a;

        message fun         Self(X) -> String
            get1b self
                =
                self->string_1b;

        fun new string_1b
            =
            make__object ({ string_1b }, ());
    };


    # Demonstration:
    print "\n";

    include package   test;

    object_a = new "ABC";
    object_b = new "XYZ";

    printf "get1a object_a == %s\n" (get1a object_a);
    printf "get1a object_b == %s\n" (get1a object_b);

    printf "get1b object_a == %s\n" (get1b object_a);
    printf "get1b object_b == %s\n" (get1b object_b);

When run this produces:

    linux$ ./my-script

    get1a object_a == abc
    get1a object_b == abc
    get1b object_a == ABC
    get1b object_b == XYZ

Points to note:

Now we subclass the above class. Let us start by making a careful distinction between message and method:

A message is defined once by some class, with that definition inherited by all subclasses of that class. Subclasses may however provide their own methods to implement that message.

Mythryl distinguishes carefully between defining a new message and defining a new method for an existing message; it uses different syntax for the two.

The message fun declarations shown in the above examples both define a message and provide a default method for it.

The method fun declarations shown below do not define messages; instead they provide replacement methods for messages inherited from their superclass. A method fun declaration does not include a type declaration; the required type information is obtained from the original message fun declaration.

    #!/usr/bin/mythryl

    class test_class {

        field my  String  string_1a = "1a";
        field my  String  string_1b;

        message fun          Self(X) -> String
            get1a self
                =
                self->string_1a;

        message fun         Self(X) -> String
            get1b self
                =
                self->string_1b;

        fun new string_1b
            =
            make__object ({ string_1b }, ());
    };


    class test_subclass {

        class super = test_class;               # Select test_class as our parent class.

        field my  String  string_2a = "2a";    # Define two new fields of our own.
        field my  String  string_2b;

        message fun          Self(X) -> String  # Define message to access our first field.
            get2a self
                =
                self->string_2a;

        message fun         Self(X) -> String   # Define message to access our second field.
            get2b self
                =
                self->string_2b;

        method fun                              # Override inherited method for test_class::get1a message.
            get1a old_method self
                =
                "<" + (old_method self) + ">";  # Return same result as old method, but wrapped in angle brackets.

        method fun                              # Override inherited method for test_class::get1b message.
            get1b old_method self
                =
                "<" + (old_method self) + ">";


        fun new string_2b string_1b
            =
            make__object ({ string_2b }, { string_1b }, ());
    };


    # Demonstration:


    object_10 = test_class::new "1b-10";
    object_11 = test_class::new "1b-11";

    object_20 = test_subclass::new "2b-20" "1b-20";
    object_21 = test_subclass::new "2b-21" "1b-21";

    print "\n";
    printf "get1a object_10 == %s\n" (test_class::get1a object_10);
    printf "get1a object_11 == %s\n" (test_class::get1a object_11);
    printf "get1a object_20 == %s\n" (test_class::get1a object_20);
    printf "get1a object_21 == %s\n" (test_class::get1a object_21);

    print "\n";
    printf "get1b object_10 == %s\n" (test_class::get1b object_10);
    printf "get1b object_11 == %s\n" (test_class::get1b object_11);
    printf "get1b object_20 == %s\n" (test_class::get1b object_20);
    printf "get1b object_21 == %s\n" (test_class::get1b object_21);

    print "\n";
    printf "get2a object_20 == %s\n" (test_subclass::get2a object_20);
    printf "get2a object_21 == %s\n" (test_subclass::get2a object_21);

    print "\n";
    printf "get2b object_20 == %s\n" (test_subclass::get2b object_20);
    printf "get2b object_21 == %s\n" (test_subclass::get2b object_21);

When run this produces:

    linux$ ./my-script

    get1a object_10 == 1a
    get1a object_11 == 1a
    get1a object_20 == <1a>
    get1a object_21 == <1a>

    get1b object_10 == 1b-10
    get1b object_11 == 1b-11
    get1b object_20 == <1b-20>
    get1b object_21 == <1b-21>

    get2a object_20 == 2a
    get2a object_21 == 2a

    get2b object_20 == 2b-20
    get2b object_21 == 2b-21

Points to note:

Object equality

Now let us consider object equality. Class object defines an equal message which may be used to compare pairs of objects for equality. In general any class which adds new state and which is going to be compared for equality probably wants to override the inherited equal method with one which takes the added state into account. Here is an example:

    #!/usr/bin/mythryl

    class test_class {

        field my  String  string1;

        method fun                         # Self(X) -> Self(X) -> Bool
            equal old_method a b
                =
                a->string1 == b->string1;       # Ignore inherited method; test_class instances are equal if their string1 fields are.

        fun new string1
            =
            make__object ({ string1 }, ());
    };


    class test_subclass {

        class super = test_class;

        field my  String  string2;

        method fun          # Self(X) -> Self(X) -> Bool
            equal superclass_equal a b
                =
                (superclass_equal a b)          # Require that inherited equality method return TRUE.
                and
                a->string2 == b->string2;       # Require in addition that our own string2 fields compare equal.

        fun new string1 string2
            =
            make__object ({ string2 }, { string1 }, ());
    };


    # Demonstration:


    object_1a = test_class::new "a";
    object_1b = test_class::new "b";

    object_2aa = test_subclass::new "a" "a";
    object_2ab = test_subclass::new "a" "b";
    object_2ba = test_subclass::new "b" "a";
    object_2bb = test_subclass::new "b" "b";

    print "\n";
    printf "object::equal object_1a object_1a == %B\n" (object::equal object_1a object_1a);
    printf "object::equal object_1a object_1b == %B\n" (object::equal object_1a object_1b);
    printf "object::equal object_1b object_1b == %B\n" (object::equal object_1b object_1b);

    print "\n";
    printf "object::equal object_2aa object_2aa == %B\n" (object::equal object_2aa object_2aa);
    printf "object::equal object_2aa object_2ab == %B\n" (object::equal object_2aa object_2ab);
    printf "object::equal object_2aa object_2ba == %B\n" (object::equal object_2aa object_2ba);
    printf "object::equal object_2aa object_2bb == %B\n" (object::equal object_2aa object_2bb);

    printf "object::equal object_2ab object_2aa == %B\n" (object::equal object_2ab object_2aa);
    printf "object::equal object_2ab object_2ab == %B\n" (object::equal object_2ab object_2ab);
    printf "object::equal object_2ab object_2ba == %B\n" (object::equal object_2ab object_2ba);
    printf "object::equal object_2ab object_2bb == %B\n" (object::equal object_2ab object_2bb);

    printf "object::equal object_2ba object_2aa == %B\n" (object::equal object_2ba object_2aa);
    printf "object::equal object_2ba object_2ab == %B\n" (object::equal object_2ba object_2ab);
    printf "object::equal object_2ba object_2ba == %B\n" (object::equal object_2ba object_2ba);
    printf "object::equal object_2ba object_2bb == %B\n" (object::equal object_2ba object_2bb);

    printf "object::equal object_2bb object_2aa == %B\n" (object::equal object_2bb object_2aa);
    printf "object::equal object_2bb object_2ab == %B\n" (object::equal object_2bb object_2ab);
    printf "object::equal object_2bb object_2ba == %B\n" (object::equal object_2bb object_2ba);
    printf "object::equal object_2bb object_2bb == %B\n" (object::equal object_2bb object_2bb);

When run this produces:

    linux$ ./my-script

    object::equal object_1a object_1a == TRUE
    object::equal object_1a object_1b == FALSE
    object::equal object_1b object_1b == TRUE

    object::equal object_2aa object_2aa == TRUE
    object::equal object_2aa object_2ab == FALSE
    object::equal object_2aa object_2ba == FALSE
    object::equal object_2aa object_2bb == FALSE
    object::equal object_2ab object_2aa == FALSE
    object::equal object_2ab object_2ab == TRUE
    object::equal object_2ab object_2ba == FALSE
    object::equal object_2ab object_2bb == FALSE
    object::equal object_2ba object_2aa == FALSE
    object::equal object_2ba object_2ab == FALSE
    object::equal object_2ba object_2ba == TRUE
    object::equal object_2ba object_2bb == FALSE
    object::equal object_2bb object_2aa == FALSE
    object::equal object_2bb object_2ab == FALSE
    object::equal object_2bb object_2ba == FALSE
    object::equal object_2bb object_2bb == TRUE

Defining a new binary message

Finally, let us demonstrate defining and using a new binary message. We define a simple class which contains a single string field, together with a binary message concatenate which returns the contatenation of the strings from two objects of that class, then we define a subclass with an additional string field which overrides the concatenate method to include its own fields in the result:

    #!/usr/bin/mythryl

    class test_class {

        field my  String  string;

         message fun
             Self(X) -> Self(X) -> String
             concatenate a b
                 =
                 a->string + b->string;

        fun new string
            =
            make__object ({ string }, ());
    };


    class test_subclass {

        class super = test_class;

        field my  String  string;

        method fun
            concatenate superclass_method a b
                =
                (superclass_method a b)
                + "," +
                (a->string + b->string);

        fun new string1 string2
            =
            make__object ({ string => string2 }, { string => string1 }, ());
    };


    # Demonstration:

    object_1a = test_class::new "a";
    object_1b = test_class::new "b";

    object_2ca = test_subclass::new "c" "a";
    object_2cb = test_subclass::new "c" "b";
    object_2da = test_subclass::new "d" "a";
    object_2db = test_subclass::new "d" "b";

    print "\n";
    printf "test_class::concatenate object_1a object_1a == %s\n" (test_class::concatenate object_1a object_1a);
    printf "test_class::concatenate object_1a object_1b == %s\n" (test_class::concatenate object_1a object_1b);
    printf "test_class::concatenate object_1b object_1b == %s\n" (test_class::concatenate object_1b object_1b);

    print "\n";
    printf "test_class::concatenate object_2ca object_2ca == %s\n" (test_class::concatenate object_2ca object_2ca);
    printf "test_class::concatenate object_2ca object_2cb == %s\n" (test_class::concatenate object_2ca object_2cb);
    printf "test_class::concatenate object_2ca object_2da == %s\n" (test_class::concatenate object_2ca object_2da);
    printf "test_class::concatenate object_2ca object_2db == %s\n" (test_class::concatenate object_2ca object_2db);

    printf "test_class::concatenate object_2cb object_2ca == %s\n" (test_class::concatenate object_2cb object_2ca);
    printf "test_class::concatenate object_2cb object_2cb == %s\n" (test_class::concatenate object_2cb object_2cb);
    printf "test_class::concatenate object_2cb object_2da == %s\n" (test_class::concatenate object_2cb object_2da);
    printf "test_class::concatenate object_2cb object_2db == %s\n" (test_class::concatenate object_2cb object_2db);

    printf "test_class::concatenate object_2da object_2ca == %s\n" (test_class::concatenate object_2da object_2ca);
    printf "test_class::concatenate object_2da object_2cb == %s\n" (test_class::concatenate object_2da object_2cb);
    printf "test_class::concatenate object_2da object_2da == %s\n" (test_class::concatenate object_2da object_2da);
    printf "test_class::concatenate object_2da object_2db == %s\n" (test_class::concatenate object_2da object_2db);

    printf "test_class::concatenate object_2db object_2ca == %s\n" (test_class::concatenate object_2db object_2ca);
    printf "test_class::concatenate object_2db object_2cb == %s\n" (test_class::concatenate object_2db object_2cb);
    printf "test_class::concatenate object_2db object_2da == %s\n" (test_class::concatenate object_2db object_2da);
    printf "test_class::concatenate object_2db object_2db == %s\n" (test_class::concatenate object_2db object_2db);

When run this yields:

    linux$ ./my-script

    test_class::concatenate object_1a object_1a == aa
    test_class::concatenate object_1a object_1b == ab
    test_class::concatenate object_1b object_1b == bb

    test_class::concatenate object_2ca object_2ca == cc,aa
    test_class::concatenate object_2ca object_2cb == cc,ab
    test_class::concatenate object_2ca object_2da == cd,aa
    test_class::concatenate object_2ca object_2db == cd,ab
    test_class::concatenate object_2cb object_2ca == cc,ba
    test_class::concatenate object_2cb object_2cb == cc,bb
    test_class::concatenate object_2cb object_2da == cd,ba
    test_class::concatenate object_2cb object_2db == cd,bb
    test_class::concatenate object_2da object_2ca == dc,aa
    test_class::concatenate object_2da object_2cb == dc,ab
    test_class::concatenate object_2da object_2da == dd,aa
    test_class::concatenate object_2da object_2db == dd,ab
    test_class::concatenate object_2db object_2ca == dc,ba
    test_class::concatenate object_2db object_2cb == dc,bb
    test_class::concatenate object_2db object_2da == dd,ba
    test_class::concatenate object_2db object_2db == dd,bb

So far, so good. Unfortunately, now we hit a roadblock; Berthomieu’s appendix A2.3.2 "simple dynamic dispatch" approach effectively requires that all objects in a given method call belong to the same class. This makes it quite difficult to implement objects which contain objects of other classes, and more generally dynamic heterogeneous object hierarchies.

This is a serious deficiency; it rules out probably at least half of the potential applications for Mythryl oop.

Berthomieu gives a solution to this in appendix A2.3.3 of his paper, involving "folding" (wrapping) objects to hide their types from the type system in this situation. Implementing that looks like the next logical step on the Mythryl oop front.

Source Code

Oop functionality is implemented by src/lib/compiler/front/typer/main/expand-oop-syntax.pkg, with assists from src/lib/compiler/front/typer/main/expand-oop-syntax-junk.pkg, src/lib/compiler/front/typer/main/oop-collect-methods-and-fields.pkg and src/lib/compiler/front/typer/main/oop-rewrite-declaration.pkg. The main point of invocation is from typecheck_named_packages in src/lib/compiler/front/typer/main/type-package-language-g.pkg.

Conclusion

When in doubt, it makes sense to try the simplest solution first.

Berthomieu’s oop approach provides the simplest solution I know of for the problem of providing basic object oriented programming support in Mythryl.

Whether this approach will prove adequate in practice is not yet clear to me.

This facility should currently be regarded as highly experimental and subject to change or deletion in future releases.


Comments and suggestions to: bugs@mythryl.org

PreviousUpNext