2
2
from typing import Any , Callable , Generic , Optional , TypeVar
3
3
4
4
from basilisp .lang import map as lmap
5
+ from basilisp .lang import runtime
5
6
from basilisp .lang import symbol as sym
6
- from basilisp .lang .interfaces import IPersistentMap
7
- from basilisp .util import Maybe
7
+ from basilisp .lang .interfaces import IPersistentMap , IPersistentSet , IRef
8
+ from basilisp .lang . set import PersistentSet
8
9
9
10
T = TypeVar ("T" )
10
11
DispatchFunction = Callable [..., T ]
11
12
Method = Callable [..., Any ]
12
13
13
14
15
+ _GLOBAL_HIERARCHY_SYM = sym .symbol ("global-hierarchy" , ns = runtime .CORE_NS )
16
+ _ISA_SYM = sym .symbol ("isa?" , ns = runtime .CORE_NS )
17
+
18
+
14
19
class MultiFunction (Generic [T ]):
15
- __slots__ = ("_name" , "_default" , "_dispatch" , "_lock" , "_methods" )
20
+ __slots__ = (
21
+ "_name" ,
22
+ "_default" ,
23
+ "_dispatch" ,
24
+ "_lock" ,
25
+ "_methods" ,
26
+ "_cache" ,
27
+ "_prefers" ,
28
+ "_hierarchy" ,
29
+ "_cached_hierarchy" ,
30
+ "_isa" ,
31
+ )
16
32
17
33
# pylint:disable=assigning-non-slot
18
34
def __init__ (
19
- self , name : sym .Symbol , dispatch : DispatchFunction , default : T
35
+ self ,
36
+ name : sym .Symbol ,
37
+ dispatch : DispatchFunction ,
38
+ default : T ,
39
+ hierarchy : Optional [IRef ] = None ,
20
40
) -> None :
21
41
self ._name = name
22
42
self ._default = default
23
43
self ._dispatch = dispatch
24
44
self ._lock = threading .Lock ()
25
45
self ._methods : IPersistentMap [T , Method ] = lmap .PersistentMap .empty ()
46
+ self ._cache : IPersistentMap [T , Method ] = lmap .PersistentMap .empty ()
47
+ self ._prefers : IPersistentMap [T , IPersistentSet [T ]] = lmap .PersistentMap .empty ()
48
+ self ._hierarchy : IRef [IPersistentMap ] = hierarchy or runtime .Var .find_safe (
49
+ _GLOBAL_HIERARCHY_SYM
50
+ )
51
+
52
+ if not isinstance (self ._hierarchy , IRef ):
53
+ raise runtime .RuntimeException (
54
+ f"Expected IRef type for :hierarchy; got { type (hierarchy )} "
55
+ )
56
+
57
+ # Maintain a cache of the hierarchy value to detect when the hierarchy
58
+ # has changed. If the hierarchy changes, we need to reset the internal
59
+ # caches.
60
+ self ._cached_hierarchy = self ._hierarchy .deref ()
61
+
62
+ # Fetch some items from basilisp.core that we need to compute the final
63
+ # dispatch method. These cannot be imported statically because that would
64
+ # produce a circular reference between basilisp.core and this module.
65
+ self ._isa = runtime .Var .find_safe (_ISA_SYM )
26
66
27
67
def __call__ (self , * args , ** kwargs ):
28
68
key = self ._dispatch (* args , ** kwargs )
@@ -31,34 +71,109 @@ def __call__(self, *args, **kwargs):
31
71
return method (* args , ** kwargs )
32
72
raise NotImplementedError
33
73
74
+ def _reset_cache (self ):
75
+ """Reset the local cache to the base method mapping.
76
+
77
+ Should be called after methods are added or removed or after preferences are
78
+ altered."""
79
+ # Does not use a lock to avoid lock reentrance
80
+ self ._cache = self ._methods
81
+ self ._cached_hierarchy = self ._hierarchy .deref ()
82
+
83
+ def _is_a (self , tag : T , parent : T ) -> bool :
84
+ """Return True if `tag` can be considered a `parent` type using `isa?`."""
85
+ return bool (self ._isa .value (self ._hierarchy .deref (), tag , parent ))
86
+
87
+ def _has_preference (self , preferred_key : T , other_key : T ) -> bool :
88
+ """Return True if this multimethod has `preferred_key` listed as a preference
89
+ over `other_key`."""
90
+ others = self ._prefers .val_at (preferred_key )
91
+ return others is not None and other_key in others
92
+
93
+ def _precedes (self , tag : T , parent : T ) -> bool :
94
+ """Return True if `tag` should be considered ahead of `parent` for method
95
+ selection."""
96
+ return self ._has_preference (tag , parent ) or self ._is_a (tag , parent )
97
+
34
98
def add_method (self , key : T , method : Method ) -> None :
35
- """Add a new method to this function which will respond for
36
- key returned from the dispatch function."""
99
+ """Add a new method to this function which will respond for key returned from
100
+ the dispatch function."""
37
101
with self ._lock :
38
102
self ._methods = self ._methods .assoc (key , method )
103
+ self ._reset_cache ()
104
+
105
+ def _find_and_cache_method (self , key : T ) -> Optional [Method ]:
106
+ """Find and cache the best method for dispatch value `key`."""
107
+ with self ._lock :
108
+ best_key : Optional [T ] = None
109
+ best_method : Optional [Method ] = None
110
+ for method_key , method in self ._methods .items ():
111
+ if self ._is_a (key , method_key ):
112
+ if best_key is None or self ._precedes (method_key , best_key ):
113
+ best_key , best_method = method_key , method
114
+ if not self ._precedes (best_key , method_key ):
115
+ raise runtime .RuntimeException (
116
+ "Cannot resolve a unique method for dispatch value "
117
+ f"'{ key } '; '{ best_key } ' and '{ method_key } ' both match and "
118
+ "neither is preferred"
119
+ )
120
+
121
+ if best_method is None :
122
+ best_method = self ._methods .val_at (self ._default )
123
+
124
+ if best_method is not None :
125
+ self ._cache = self ._cache .assoc (key , best_method )
126
+
127
+ return best_method
39
128
40
129
def get_method (self , key : T ) -> Optional [Method ]:
41
- """Return the method which would handle this dispatch key or
42
- None if no method defined for this key and no default."""
43
- method_cache = self ._methods
44
- # The 'type: ignore' comment below silences a spurious MyPy error
45
- # about having a return statement in a method which does not return.
46
- return Maybe (method_cache .val_at (key , None )).or_else (
47
- lambda : method_cache .val_at (self ._default , None ) # type: ignore
48
- )
130
+ """Return the method which would handle this dispatch key or None if no method
131
+ defined for this key and no default."""
132
+ if self ._cached_hierarchy != self ._hierarchy .deref ():
133
+ self ._reset_cache ()
134
+
135
+ cached_val = self ._cache .val_at (key )
136
+ if cached_val is not None :
137
+ return cached_val
138
+
139
+ return self ._find_and_cache_method (key )
140
+
141
+ def prefer_method (self , preferred_key : T , other_key : T ) -> None :
142
+ """Update the multimethod to prefer `preferred_key` over `other_key` in cases
143
+ where method selection might be ambiguous between two values."""
144
+ with self ._lock :
145
+ if self ._has_preference ( # pylint: disable=arguments-out-of-order
146
+ other_key , preferred_key
147
+ ):
148
+ raise runtime .RuntimeException (
149
+ f"Cannot set preference for '{ preferred_key } ' over '{ other_key } ' "
150
+ f"due to existing preference for '{ other_key } ' over "
151
+ f"'{ preferred_key } '"
152
+ )
153
+ existing = self ._prefers .val_at (preferred_key , PersistentSet .empty ())
154
+ assert existing is not None
155
+ self ._prefers = self ._prefers .assoc (preferred_key , existing .cons (other_key ))
156
+ self ._reset_cache ()
157
+
158
+ @property
159
+ def prefers (self ):
160
+ """Return a mapping of preferred values to the set of other values."""
161
+ return self ._prefers
49
162
50
163
def remove_method (self , key : T ) -> Optional [Method ]:
51
164
"""Remove the method defined for this key and return it."""
52
165
with self ._lock :
53
166
method = self ._methods .val_at (key , None )
54
167
if method :
55
168
self ._methods = self ._methods .dissoc (key )
169
+ self ._reset_cache ()
56
170
return method
57
171
58
172
def remove_all_methods (self ) -> None :
59
- """Remove all methods defined for this multi-function ."""
173
+ """Remove all methods defined for this multimethod1 ."""
60
174
with self ._lock :
61
175
self ._methods = lmap .PersistentMap .empty ()
176
+ self ._reset_cache ()
62
177
63
178
@property
64
179
def default (self ) -> T :
@@ -67,9 +182,3 @@ def default(self) -> T:
67
182
@property
68
183
def methods (self ) -> IPersistentMap [T , Method ]:
69
184
return self ._methods
70
-
71
-
72
- def multifn (dispatch : DispatchFunction , default = None ) -> MultiFunction [T ]:
73
- """Decorator function which can be used to make Python multi functions."""
74
- name = sym .symbol (dispatch .__qualname__ , ns = dispatch .__module__ )
75
- return MultiFunction (name , dispatch , default )
0 commit comments