aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEuAndreh <eu@euandre.org>2021-04-24 17:08:21 -0300
committerEuAndreh <eu@euandre.org>2021-04-24 17:08:21 -0300
commit114d4c1148429f72e2e6aa381e0973120ac564ae (patch)
tree3efc67095fc934a3c0983f218052e1c623950232
parentAdd TIL on Clojure auto-currying (diff)
downloadeuandre.org-114d4c1148429f72e2e6aa381e0973120ac564ae.tar.gz
euandre.org-114d4c1148429f72e2e6aa381e0973120ac564ae.tar.xz
Add TIL on Lisp's :argument-precedence-order
Diffstat (limited to '')
-rw-r--r--_tils/2021-04-24-common-lisp-argument-precedence-order-parameterization-of-a-generic-function.md133
1 files changed, 133 insertions, 0 deletions
diff --git a/_tils/2021-04-24-common-lisp-argument-precedence-order-parameterization-of-a-generic-function.md b/_tils/2021-04-24-common-lisp-argument-precedence-order-parameterization-of-a-generic-function.md
new file mode 100644
index 0000000..67a6799
--- /dev/null
+++ b/_tils/2021-04-24-common-lisp-argument-precedence-order-parameterization-of-a-generic-function.md
@@ -0,0 +1,133 @@
+---
+
+title: Common Lisp argument precedence order parameterization of a generic function
+
+date: 2021-04-24
+
+layout: post
+
+lang: en
+
+ref: common-lisp-argument-precedence-order-parameterization-of-a-generic-function
+
+---
+
+When CLOS dispatches a method, it picks the most specific method definition to the argument list:
+
+```lisp
+
+* (defgeneric a-fn (x))
+#<STANDARD-GENERIC-FUNCTION A-FN (0) {5815ACB9}>
+
+* (defmethod a-fn (x) :default-method)
+#<STANDARD-METHOD A-FN (T) {581DB535}>
+
+* (defmethod a-fn ((x number)) :a-number)
+#<STANDARD-METHOD A-FN (NUMBER) {58241645}>
+
+* (defmethod a-fn ((x (eql 1))) :number-1)
+#<STANDARD-METHOD A-FN ((EQL 1)) {582A7D75}>
+
+* (a-fn nil)
+:DEFAULT-METHOD
+
+* (a-fn "1")
+:DEFAULT-METHOD
+
+* (a-fn 0)
+:A-NUMBER
+
+* (a-fn 1)
+:NUMBER-1
+```
+
+CLOS uses a similar logic when choosing the method from parent classes, when multiple ones are available:
+
+```lisp
+* (defclass class-a () ())
+
+#<STANDARD-CLASS CLASS-A {583E0B25}>
+* (defclass class-b () ())
+
+#<STANDARD-CLASS CLASS-B {583E7F6D}>
+* (defgeneric another-fn (obj))
+
+#<STANDARD-GENERIC-FUNCTION ANOTHER-FN (0) {583DA749}>
+* (defmethod another-fn ((obj class-a)) :class-a)
+; Compiling LAMBDA (.PV-CELL. .NEXT-METHOD-CALL. OBJ):
+; Compiling Top-Level Form:
+
+#<STANDARD-METHOD ANOTHER-FN (CLASS-A) {584523C5}>
+* (defmethod another-fn ((obj class-b)) :class-b)
+; Compiling LAMBDA (.PV-CELL. .NEXT-METHOD-CALL. OBJ):
+; Compiling Top-Level Form:
+
+#<STANDARD-METHOD ANOTHER-FN (CLASS-B) {584B8895}>
+```
+
+Given the above definitions, when inheriting from `class-a` and `class-b`, the order of inheritance matters:
+
+```lisp
+* (defclass class-a-coming-first (class-a class-b) ())
+#<STANDARD-CLASS CLASS-A-COMING-FIRST {584BE6AD}>
+
+* (defclass class-b-coming-first (class-b class-a) ())
+#<STANDARD-CLASS CLASS-B-COMING-FIRST {584C744D}>
+
+* (another-fn (make-instance 'class-a-coming-first))
+:CLASS-A
+
+* (another-fn (make-instance 'class-b-coming-first))
+:CLASS-B
+```
+
+Combining the order of inheritance with generic functions with multiple arguments, CLOS has to make a choice of how to pick a method given two competing definitions, and its default strategy is prioritizing from left to right:
+
+```lisp
+* (defgeneric yet-another-fn (obj1 obj2))
+#<STANDARD-GENERIC-FUNCTION YET-ANOTHER-FN (0) {584D9EC9}>
+
+* (defmethod yet-another-fn ((obj1 class-a) obj2) :first-arg-specialized)
+#<STANDARD-METHOD YET-ANOTHER-FN (CLASS-A T) {5854269D}>
+
+* (defmethod yet-another-fn (obj1 (obj2 class-b)) :second-arg-specialized)
+#<STANDARD-METHOD YET-ANOTHER-FN (T CLASS-B) {585AAAAD}>
+
+* (yet-another-fn (make-instance 'class-a) (make-instance 'class-b))
+:FIRST-ARG-SPECIALIZED
+```
+
+CLOS has to make a choice between the first and the second definition of `yet-another-fn`, but its choice is just a heuristic.
+What if we want to the choice to be based on the second argument first?
+
+For that, we use the `:argument-precedence-order` option when declaring a generic function:
+
+```lisp
+* (defgeneric yet-another-fn (obj1 obj2) (:argument-precedence-order obj2 obj1))
+#<STANDARD-GENERIC-FUNCTION YET-ANOTHER-FN (2) {584D9EC9}>
+
+* (yet-another-fn (make-instance 'class-a) (make-instance 'class-b))
+:SECOND-ARG-SPECIALIZED
+```
+
+I liked that the `:argument-precedence-order` option exists.
+We shouldn't have to change the arguments from `(obj1 obj2)` to `(obj2 obj1)` just to make CLOS pick the method that we want.
+We can configure its default behaviour if desired, and keep the order of arguments however it best fits the generic function.
+
+## Comparison with Clojure
+
+Clojure has an equivalent, when using `defmulti`.
+
+Since when declaring a multi-method with `defmulti` we must define the dispatch function, Clojure uses it to pick the method definition.
+Since the dispatch function is required, there is no need for a default behaviour, such as left-to-right.
+
+## Conclusion
+
+Making the argument precedence order configurable for generic functions but not for class definitions makes a lot of sense.
+
+When declaring a class, we can choose the precedence order, and that is about it.
+But when defining a generic function, the order of argumentws is more important to the function semantics, and the argument precedence being left-to-right is just the default behaviour.
+
+One shouldn't change the order of arguments of a generic function for the sake of tailoring it to the CLOS priority ranking algorithm, but doing it for a class definition is just fine.
+
+TIL.