Skip to content

multi-generic#2431

Draft
kusumotonorio wants to merge 43 commits intofactor:masterfrom
kusumotonorio:multi-generic
Draft

multi-generic#2431
kusumotonorio wants to merge 43 commits intofactor:masterfrom
kusumotonorio:multi-generic

Conversation

@kusumotonorio
Copy link
Copy Markdown
Contributor

@kusumotonorio kusumotonorio commented Feb 17, 2021

This is one of the implementations that adds multi-methods to Factor.
This is an extension of the already existing multi-methods. In addition, it includes the generic functionality.
It declares methods with a unified syntax for generic words that dispatch with a single stack parameter, dispatch with a single hook variable, mathematical double-dispatch, and dispatch with a combination of stack parameters and hook variables.

Basic syntax

Multi-dispatchable generic word declaration

MGENERIC: foo ( hookvar1 hookvar2 ... | in1 in2 ... -- out1 out2 ... )

MGENERIC: bar ( in1 in2 ... -- out1 out2 ... )

Dispatching by hook variables is optional. When not in use, hook variables and the partition | are not written.
To declare a mathematical generic word equivalent to the one declared by MATH:, write mathematical.

example:

MGENERIC: my-plus ( a b -- c ) mathematical

Defining methods for multi-dispatch support

MM: foo ( hookvar1: hv1-class1 hookvar2: vh2-class1 ... | in1: in1-class1 in2: in2-class1 ... -- out11 out2 ... )
    def1 ;

MM:: foo ( hookvar1: hv1-class2 hookvar2: vh2-class2 ... | in1: in1-class2 in2: in2-class2 ... -- out11 out2 ... )
    def2 ;
....

Local variables can be used in the definition by MM::.

Literal representation of multi-dispatchable methods

MM\ foo ( hookvar1: hv1-class hookvar2: vh2-class ... | in1: in1-class-c in2: in2-class-d ... -- out11 out2 ... )

Example

IN: scratchpad 
USE: multi-generic
MIXIN: thing
SINGLETON: paper    INSTANCE: paper thing
SINGLETON: scissors INSTANCE: scissors thing
SINGLETON: rock     INSTANCE: rock thing

MGENERIC: beats? ( obj1 obj2 -- ? )

MM: beats? ( obj1: paper obj2: scissors -- ? ) 2drop t ;
MM: beats? ( :scissors :rock -- ? )  2drop t ;
MM: beats? ( :rock :paper -- ? )  2drop t ;
MM: beats? ( :thing :thing -- ? )  2drop f ;

IN: scratchpad \ beats? methods [ second ] map .
{
    MM\ beats? ( obj1: paper obj2: scissors -- ? )
    MM\ beats? ( :thing :thing -- ? )
    MM\ beats? ( :scissors :rock -- ? )
    MM\ beats? ( :rock :paper -- ? )
}
IN: scratchpad MM\ beats? ( o1: paper o2: scissors -- ? ) def>> .
[ 2drop t ]
IN: scratchpad 
USING: io namespaces multi-generic ;

SYMBOL: transport
TUPLE: land-transport ;
TUPLE: air-transport ;

MGENERIC: deliver ( transport | destination -- )
MM: deliver ( transport: land-transport | destination -- ) "Land delivery to " write print ;
MM: deliver ( transport: air-transport  | destination -- ) "Air delivery to " write print ;

T{ air-transport } transport set
"New York City" deliver

Air delivery to New York City

Other features include

  • Dispatching with a single stack parameter, dispatching with a single hook variable, and mathematical dispatching are handled with the same logic as generic, so their execution speed is expected to be the same.
  • Even mathematical generic words can be dispatched by non-numeric objects.
  • The call-next-multi-method can be used to allow differential programming through class inheritance.
  • If the output of a method stack-effects declaration has class-specific information, the typed feature guarantees the class of the output and adds optimization information.
  • If the class of some parameters used for dispatching can be estimated, it has a partial inline feature to generate compile-time dispatched code for the known parameters if possible.

…tened. Improve partial-inline of math-dispatch.
Added `bootstrap-word` to `object`.
The specific order of the methods is
  - First, the hook variables are in the order they were written
  - Next, stack parameters in order from top side

* Disabled checking for generics in covariant-tuple dispatch.
@kusumotonorio
Copy link
Copy Markdown
Contributor Author

kusumotonorio commented Mar 4, 2021

I changed the sort order of methods in multi-generic to be different from multi-methods.

IN: scratchpad USE: multi-generic
IN: scratchpad 
TUPLE: t1 ;
TUPLE: t2 ;
TUPLE: t3 ;

TUPLE: t4 < t1 ;
TUPLE: t5 < t2 ;
TUPLE: t6 < t3 ;

TUPLE: t7 < t4 ;
TUPLE: t8 < t5 ;

MGENERIC: m-nc ( o1 o2 o3 -- no )
MM:: m-nc ( :t1 :object :object -- No.1 ) 1 ;
MM:: m-nc ( :t7 :t5 :object -- No.2 ) 2 ;
MM:: m-nc ( :t4 :object object -- No.3 ) 3 ;
MM:: m-nc ( :object :t8 :t6 -- No.4 ) 4 ;
MM:: m-nc ( :object :object :object -- No.5 ) 5 ;
MM:: m-nc ( :object :t2 :object -- No.6 ) 6 ;
MM:: m-nc ( :object :object :t3 -- No.7 ) 7 ;
MM:: m-nc ( :t4 :t5 :t6 -- No.8 ) 8 ;
MM:: m-nc ( :object :t5 :object -- No.9 ) 9 ;
IN: scratchpad \ m-nc methods sort-methods [ second ] map .
{
    MM\ m-nc ( :object :t8 :t6 -- No.4 )
    MM\ m-nc ( :t4 :t5 :t6 -- No.8 )
    MM\ m-nc ( :object :object :t3 -- No.7 )
    MM\ m-nc ( :t7 :t5 :object -- No.2 )
    MM\ m-nc ( :object :t5 :object -- No.9 )
    MM\ m-nc ( :object :t2 :object -- No.6 )
    MM\ m-nc ( :t4 :object object -- No.3 )
    MM\ m-nc ( :t1 :object :object -- No.1 )
    MM\ m-nc ( :object :object :object -- No.5 )
}

Sorting with sort-single-methods (equivalent to sort-methods in generic.single) whitch uses <=> in covariant-tuple dispatch looks like this:

MGENERIC: m-c ( o1 o2 o3 -- no ) cached-multi
MM:: m-c ( :t1 :object :object -- NO.1 ) 1 ;
MM:: m-c ( :t7 :t5 :object -- No.2 ) 2 ;
MM:: m-c ( :t4 :object object -- No.3 ) 3 ;
MM:: m-c ( :object :t8 :t6 -- No.4 ) 4 ;
MM:: m-c ( :object :object :object -- No.5 ) 5 ;
MM:: m-c ( :object :t2 :object -- No.6 ) 6 ;
MM:: m-c ( :object :object :t3 -- No.7 ) 7 ;
MM:: m-c ( :t4 :t5 :t6 -- No.8 ) 8 ;
MM:: m-c ( :object :t5 :object -- No.9 ) 9 ;
IN: scratchpad \ m-c "dispatch-type" word-prop methods>> sort-single-methods [ second ] map .
{
    MM\ m-c ( :object :object :object -- No.5 )
    MM\ m-c ( :t1 :object :object -- NO.1 )
    MM\ m-c ( :t4 :object object -- No.3 )
    MM\ m-c ( :object :t2 :object -- No.6 )
    MM\ m-c ( :object :t5 :object -- No.9 )
    MM\ m-c ( :t7 :t5 :object -- No.2 )
    MM\ m-c ( :object :object :t3 -- No.7 )
    MM\ m-c ( :t4 :t5 :t6 -- No.8 )
    MM\ m-c ( :object :t8 :t6 -- No.4 )
}

They are the same in that they are focused on the specificity of the top of the stack, but the order is reversed.

@mrjbq7
Copy link
Copy Markdown
Member

mrjbq7 commented Mar 4, 2021 via email

@kusumotonorio
Copy link
Copy Markdown
Contributor Author

kusumotonorio commented Mar 4, 2021

Since the method is checked for conformity in the sorted order, how it is sorted may be important for execution.

For example, let's define a set of generics and methods like the above in multi-generic and multi-methods, and put t1, t5, and t6 tuples on the stack to call those generics.

IN: scratchpad t1 new t5 new t6 new m-nc .
7

In multi-generic, the method that returns 7, MM\ m-c ( :object :object :t3 -- No.7 ), was chosen. It is the third from the top in the sort order. Since t6 is a subclass of t3 and is at the top of the stack, it was given more weight in the choice of methods.

Next, we will see if this is the case with multi-methods.

IN: scratchpad t1 new t5 new t6 new mm .
1

It chose the method mm-{ t1 object object }, which returns 1.
It is the fourth method in the multi-methods. The emphasis here is on t1, which is the deepest in the stack.

@mrjbq7
Copy link
Copy Markdown
Member

mrjbq7 commented Mar 5, 2021 via email

@kusumotonorio
Copy link
Copy Markdown
Contributor Author

Meaning is it a question of ordering, or is one preventing the other from being called?

What exactly do you mean by this?

I believe that the choice of methods from either side of the stack is a matter of "specification promise", not "semantic certainty". Once that rule is set, it shouldn't matter which way we go, but we need to be consistent.

@kusumotonorio
Copy link
Copy Markdown
Contributor Author

kusumotonorio commented Mar 6, 2021

I noticed that the function I implemented in multi-generic as call-next-multi-method sometimes does not match its name.
The definition of "next multi-method" in languages with multi-methods is "the method that would have been called if a method did not exist". My implementation of it is "the method that would have been called if the generic word had been called in the method's signature when the method did not exist", and it is possible that they would not be the same when called with the actual parameters. So if we were to give it a more appropriate name, it would be something like call-super-muti-method (if there is such a term).

I made it to mimic the call-next-method in generic. The method it replaces is statically determined at compile time of the method that calls it, but I think to achieve a true call-next-multi-method, the dispatching code would have to be embedded inline and dispatched at runtime.

When I imagine the situation where call-next-multi-method will actually be used, I feel that the current implementation is fine, but it is also undesirable to be different from other languages. I am not sure how it is handled in CLOS, but I found an explanation in R.

http://web.mit.edu/r/current/lib/R/library/methods/html/NextMethod.html

The ‘next’ method (i.e., the first inherited method) is defined to be that method which would have been called if the current method did not exist. This is more-or-less literally what happens: The current method (to be precise, the method with signature given by the defined slot of the method from which callNextMethod is called) is deleted from a copy of the methods for the current generic, and selectMethod is called to find the next method (the result is cached in the method object where the call occurred, so the search typically happens only once per session per combination of argument classes).

The next method is defined from the signature of the current method, not from the actual classes of the arguments. In particular, modifying any of the arguments has no effect on the selection. As a result, the selected next method can be called with invalid arguments if the calling function assigns objects of a different class before the callNextMethod() call. Be careful of any assignments to such arguments.

It seems that R's it has a similar character to my implementation.

I'm trying to figure out what I should do. Here's what I came up with.

  1. use the current implementation with the name call-next-multi-method.
  2. Rename the current implementation to call-super-multi-method.
  3. Stop the current implementation and create a true call-next-multi-method that runs slower.
  4. Rename the current implementation to call-super-multi-method and create a new true call-next-multi-method so that users can use both.

Which one do you think is the best?

@kusumotonorio
Copy link
Copy Markdown
Contributor Author

@timor I noticed that the cache dispatch of covariant-tuple does not make the correct method selection in multi-generic at the moment. I had checked this before and it felt normal then, so I reverted a change ( 2c37179 ) that mimicked your branch and the problem was fixed.

What was happening before this change was that the default method was chosen at any time. In the rock-paper-scissors test, the result was always a loss.
Does your branch dispatch correctly? If so, then my changes are not appropriate.

@timor
Copy link
Copy Markdown
Contributor

timor commented Mar 7, 2021

What was happening before this change was that the default method was chosen at any time. In the rock-paper-scissors test, the result was always a loss.

Yes, I noticed that and that should be fixed in the current branch.

Does your branch dispatch correctly? If so, then my changes are not appropriate.

Can you give an example which currently fails?

@kusumotonorio
Copy link
Copy Markdown
Contributor Author

kusumotonorio commented Mar 7, 2021

I have previously made changes based on your changes timor@3c0c6ed and timor@cee950d .

The one that disabled those changes passes the following test.

MGENERIC: cached-md-beats? ( obj1 obj2 -- ? ) cached-multi

MM:: cached-md-beats? ( obj1: paper obj2: scissors -- ?: boolean ) obj1 obj2 2drop t ;
MM:: cached-md-beats? ( :scissors :rock -- ?: boolean ) t ;
MM:: cached-md-beats? ( :rock :paper -- ?: boolean ) t ;
MM:: cached-md-beats? ( :thing :thing -- ?: boolean ) f ;


{ f t f  f f t  t f f } [
    paper paper       cached-md-beats?
    paper scissors    cached-md-beats?
    paper rock        cached-md-beats?

    scissors paper    cached-md-beats?
    scissors scissors cached-md-beats?
    scissors rock     cached-md-beats?

    rock paper        cached-md-beats?
    rock scissors     cached-md-beats?
    rock rock         cached-md-beats?
] unit-test

Enabling these changes does not give the expected result.

resource:extra/multi-generic/multi-generic-tests.factor: 233

Unit Test: {
    { f t f f f t t f f }
    [
        paper paper cached-md-beats?
        paper scissors cached-md-beats?
        paper rock cached-md-beats?
        scissors paper cached-md-beats?
        scissors scissors cached-md-beats?
        scissors rock cached-md-beats?
        rock paper cached-md-beats?
        rock scissors cached-md-beats?
        rock rock cached-md-beats?
    ]
}

=== Expected:
f
t
f
f
f
t
t
f
f
=== Got:
f
f
f
f
f
f
f
f
f

I left inline in the definition of the generic for that test, so I was late to notice this problem.

@timor
Copy link
Copy Markdown
Contributor

timor commented Mar 8, 2021

@kusumotonorio It seems to work correctly with my latest code. Can you point me to a commit that I can check out to reproduce the problem?

@kusumotonorio
Copy link
Copy Markdown
Contributor Author

@timor
Hmmm, I downloaded the branch you made ( https://github.com/timor/factor/tree/multi-generic ), recreated the image and tried it, but still cached-md-beats? returns f in any combination.
Now I'm trying to make my branch equivalent to yours.

@timor
Copy link
Copy Markdown
Contributor

timor commented Mar 9, 2021

@timor
Hmmm, I downloaded the branch you made ( https://github.com/timor/factor/tree/multi-generic ), recreated the image and tried it, but still cached-md-beats? returns f in any combination.
Now I'm trying to make my branch equivalent to yours.

Oh sorry, that was not what I meant with "my latest code", I was referring to the other PR (#2430). If you point me to a commit to check out, I am happy to take a look at what could be the difference.

@kusumotonorio
Copy link
Copy Markdown
Contributor Author

I have always wanted to run your PR under myself, and have tried several times, but unfortunately have not yet succeeded in doing so.
Maybe I should make some changes to master and replicate that branch of yours.

@kusumotonorio
Copy link
Copy Markdown
Contributor Author

#2431 (comment)

I noticed that the function I implemented in multi-generic as call-next-multi-method sometimes does not match its name.

Right now, the method selection priority of multi-generic is (probably temporarily) set to left-to-right (bottom-to-top), which makes it easier to compare with CLOS. So, I did an experiment to see what differences there are between the two, and recorded it here.

(defclass t0 () ())
(defclass t1 (t0) ())
(defclass t2 (t1) ())
(defclass t3 (t2) ())

(defmethod m-cl ((c1 t3) (c2 t0) (c3 t2)) (format t "No.1-") (call-next-method))
(defmethod m-cl ((c1 t2) (c2 t2) (c3 t2)) (format t "No.2-") (call-next-method))
(defmethod m-cl ((c1 t2) (c2 t0) (c3 t0)) (format t "No.3-") (call-next-method))
(defmethod m-cl ((c1 t0) (c2 t0) (c3 t0)) (format t "No.4~%"))

* (m-cl (make-instance 't3) (make-instance 't2) (make-instance 't0))
No.3-No.4
NIL
* (m-cl (make-instance 't2) (make-instance 't2) (make-instance 't2))
No.2-No.3-No.4
NIL
* (m-cl (make-instance 't3) (make-instance 't2) (make-instance 't2))
No.1-No.2-No.3-No.4
NIL
USE: multi-generic

TUPLE: t0 ;
TUPLE: t1 < t0 ;
TUPLE: t2 < t1 ;
TUPLE: t3 < t2 ;

MGENERIC: mg ( o1 o2 o3 -- )
MM: mg ( :t3 :t0 :t2 -- ) "No.1-" write call-next-multi-method ;
MM: mg ( :t2 :t2 :t2 -- ) "No.2-" write call-next-multi-method ;
MM: mg ( :t2 :t0 :t0 -- ) "No.3-" write call-next-multi-method ;
MM: mg ( :t0 :t0 :t0 -- ) 3drop "No.4" write nl ;
IN: scratchpad t3 new t2 new t0 new mg
No.3-No.4
IN: scratchpad t2 new t2 new t2 new mg
No.2-No.3-No.4
IN: scratchpad t3 new t2 new t2 new mg
No.1-No.3-No.4

In the last example of multi-generic, the second method was not included in the call-next-multi-method chain because the second parameter of the second method is not a superclass of the corresponding parameter of the first method.

@timor
Copy link
Copy Markdown
Contributor

timor commented Mar 13, 2021

@kusumotonorio Wow, that's really interesting! I always thought that Factor and CLOS have the same semantics there. The documentation of call-next-method states that it is an error to call a next method with something that would not have been able to dispatch on the current method, and inserts corresponding checks before calling the next method. I wonder whether this kind of restriction is consistent in the multiple-dispatch case.

I think the difference is checking the objects at the statically determined next-method call vs. performing something like successive dispatch, deciding anew what the next applicable method is at the next-method call site, but kind of excluding dispatching to the present one again? Would that mean that it was possible to create infinte loops in the CLOS case?

It seems that CLOS computes the precedence order of all applicable methods at the first call site, based on the present argument classes. Factor does not do that, since it does not have the information of what the generic function was called with when dispatching again in a nested call-next-method.

@timor
Copy link
Copy Markdown
Contributor

timor commented Mar 13, 2021

I just checked the behavior with my covariant tuple branch. This is one of the cases where the dispatch is ambiguous. A compiler "warning" error is generated after defining all methods: T{ ambiguous-method-specializations { classes { D{ t3 t2 t2 } } } } which informs about the problematic case. It still compiles successfully, and when run, exhibits the same behavior as multi-generic.

@kusumotonorio
Copy link
Copy Markdown
Contributor Author

kusumotonorio commented Mar 13, 2021

Factor does not do that, since it does not have the information of what the generic function was called with when dispatching again in a nested call-next-method.

I think you're right.
In a single parameter dispatch with generic, it would work just fine as a call-next-method, but in multi-dispatch, there could be a case where this would make a difference. However, it is doubtful that we will ever see such a combination of methods in real-world programming as I have experimented with.

@kusumotonorio
Copy link
Copy Markdown
Contributor Author

kusumotonorio commented Mar 16, 2021

In The Common Lisp Cookbook, call-next-method is described as follows:

During the execution of a method, the remaining applicable methods are still accessible, via the local function call-next-method. This function has lexical scope within the body of a method but indefinite extent. It invokes the next most specific method, and returns whatever value that method returned.

If we take the phrase "the next most specific method" to mean "the method with the next most specific specializer for the current method's specializer", we can use call-next-method or call-next-multi-metod could be used.

@kusumotonorio
Copy link
Copy Markdown
Contributor Author

kusumotonorio commented Mar 16, 2021

The current version of multi-generic does what I want it to do, but unfortunately it is not complete enough for practical use. What I consider to be the biggest problem at the moment is the redefinition of generic words and the behavior of multi-generic when reloading.

I need to understand what I have to do when I redefine a Factor word, but since multi-generic has two origins, multi-methods and generic, it seems to me that they are treated differently in the vocabularies I'm not sure what to do.

@timor
Copy link
Copy Markdown
Contributor

timor commented Mar 16, 2021

The current version of multi-generic does what I want it to do, but unfortunately it is not complete enough for practical use. What I consider to be the biggest problem at the moment is the redefinition of generic words and the behavior of multi-generic when reloading.

Can you give an example of what the problematic behavior is?

@kusumotonorio
Copy link
Copy Markdown
Contributor Author

@timor Thank you for your interest. I will answer with a better idea of what the problem is. Please give me some time.

M: anonymous-complement (classes-intersect?)
class>> class<= not ;

: 2any? ( ... seq1 seq2 quot: ( ... elt1 elt2 -- ... ? ) -- ... ? )
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is now in sequences vocab

! exclusive.
M: covariant-tuple (classes-intersect?)
covariant-classes
[ classes-intersect? ] 2all? ;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this supposed to be 2any??

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants