Welcome! Please see the About page for a little more info on how this works.

+1 vote
in Multimethods by

The docstring for clojure.core/fn? says "Returns true if x implements Fn, i.e. is an object created via fn.", defined in this commit from 2008.

Multimethods semantically act as functions. They're not collections (maps), they're not pieces of data that work like functions for ease (keywords), they're procedures with behavior and input and output. Protocol methods semantically act like functions as well. However, multimethods don't respond to fn? because they don't implement clojure.lang.Fn, they implement clojure.lang.AFn, whereas protocol methods do respond to fn? because they are created with fn* in the defprotocol macro.

This has led to confusion in my code where I have a sequence of function-like objects and in certain cases I want to select only the "semantically function" objects. It has forced me to write fn+? which is implemented as (or (fn? x) (instance? clojure.lang.MultiFn x)). Now, having written it, it works fine and my immediate problem is solved. However, I think this points to an underlying semantic mismatch between multimethods and their implementation.

It would benefit me and others to have this mismatch cleaned up.

by
Attaching the (long) thread from Slack about this: https://clojurians.slack.com/archives/C053AK3F9/p1728334423532669

1 Answer

0 votes
by

Almost always, you really want ifn? - is that what you want here?

by
Per the Slack thread, no, they want to partition things that are data and things that are "functions". The problem is that ifn? is true for things that are "data" (such as hash maps) and that fn? is false for things that are "functions" (such as multimethods).
by
Well, those aren’t distinct partitions. There are multiple abstractions that overlap in concrete objects.
by
No, `ifn?` is not what I want. I want to know if a given object is a black box of procedural logic or an updateable bit of "static" data. If it's the former, I can use it to perform actions on data. If it's the latter, I can compare against it, I can introspect it, I can select subsets of it. I don't know ahead of time which will be which because the api is designed to not force the user to specify ahead of time.

Here are some concrete examples of code working around this mismatch:

* clojure.test checks if the first element of a list is a function (in `clojure.test/function?`: https://github.com/clojure/clojure/blob/13a2f67b91ab81cd109ea3152fce1ae76d212453/src/clj/clojure/test.clj#L424-L434), which changes whether it considers the call to be a "predicate" or not, which affects how reporting is generated.

* Midje allows for functions to be used as the "expected result", and will call the provided function on the test case to determine if the test passes or fails (docs: https://github.com/marick/Midje/wiki/A-tutorial-introduction#extended-equality-and-checkers, code: https://github.com/marick/Midje/blob/bee206983db22c6dc92044fd7b5b0365bbd44fc6/src/midje/checking/core.clj#L57). It uses `extended-fn?` which is an implementation of `(or (fn? x) (instance? clojure.lang.MultiFn x))` to work around MultiFn not currently implementing Fn.

* Expectations v2 allows for functions to be used as the "expected result", and will call the provided function on the test case to determine in the test passes or fails (docs: https://github.com/clojure-expectations/clojure-test, code: https://github.com/clojure-expectations/clojure-test/blob/9fea7342651e7be1fcd52c85bad599a255228611/src/expectations/clojure/test.cljc#L180-L184). It uses `fn?`, and so this form can't be used with predicate multimethods.

* Compojure has to implement their protocols Renderable and Sendable twice each (once for both clojure.lang.Fn and clojure.lang.MultiFn) to account for this mismatch: https://github.com/weavejester/compojure/blob/22c56a627522f3343026c3a773630713e1e00eae/src/compojure/response.clj#L46-L49 and https://github.com/weavejester/compojure/blob/22c56a627522f3343026c3a773630713e1e00eae/src/compojure/response.clj#L67-L73


* The incredibly popular library Orchard (which underpins CIDER and other clojure tools) counts multimethods as functions for purposes of providing information about symbols to developers: https://github.com/clojure-emacs/orchard/blob/0b3296adf4a38b63cc9b73872d26d6bccb5bc13d/src/orchard/apropos.clj#L73-L79

* The library Dali defines a `function?` predicate that checks if the given object is an `fn?` or MultiFn: https://github.com/stathissideris/dali/blob/761c383e6f0228bc9e155c3dacea4ee89f708629/src/dali/utils.clj#L33-L35

In each of these instances, the goal isn't to find objects that are callable but to differentiate behavior based on whether the object is intended to be used "as data" (somewhat static collections of information that can be introspected, can be printed, etc) or "as function" (black boxes of procedural logic that can close over state).

Alex is right that there are multiple abstractions that overlap here. All objects exist on a continuum between these two points and in Clojure that line is much blurrier than in other languages, given how many of the core data types can be called like functions. (Compare Clojure's map to Python's dictionary.) However, in my opinion, multimethods exist much closer to the "black box of procedural logic" side than the "collection of information" side.

In a counterfactual world where Rich Hickey added the Fn marker interface to MultiFn when he introduced it, I don't think anyone would be asking to remove it now and I think there would certainly be pushback against such a proposal, pointing to the above continuum.

I think that if protocols were implemented in such a way as to not pass `fn?`, then this would bother me less: `fn` and `defn` are `fn?`, "extendable functions" (multimethods and protocols) are not, write your own `extended-fn?` function and move on.

However, it's an implementation detail that protocols use `(fn ...)` instead of, say, being a `class Protocol implements AFn {}` with 20 `public Object invoke(...) {}` methods. There's now a mismatch between the two "extendable functions".
by
"semantically function" is not an abstraction in Clojure. The abstraction is "invokable", which is captured by `ifn?`. Collections are an abstraction captured by `coll?`. Some things are both.

`fn?` is extremely narrow in its definition, as the docstring says, "is an object created via fn". Is a multimethod created by fn? no. Changing this, I suspect, would break somebody somewhere - if `fn?` is true, I expect to be able to cast it to Fn. Protocols do really make fns.

Contemplating something new, maybe it would be possible to have a new predicate like `proc?` that captured procedures but not data. Do all of these libs agree on what that would mean? Presumably collections, keywords, and symbols are not procs. What about vars? What about refs (they're invokable to deref)? What about a defrecord of defatype that implements IFn?

I understand the desire to partition the world for the purposes of analysis, but this is an intentionally gray area.
by
I guess the other option is having MultiFn implement Fn - what I would want to know is, who uses `Fn` and `fn?` now, both inside Clojure and outside, in particular vs using `IFn` and `ifn?`. If 100% of those are consistent with multimethods, then this makes sense.
by
One use of fn? in the Clojure core library is in trampoline, where it is used to distinguish between a continuation thunk and a final value.
by
Thinking about it a little -- a MultiFn probably wouldn't work as a continuation thunk, since the thunk takes no argument, so there is nothing for a MultiFn to dispatch on, right?
by
> who uses `Fn` and `fn?` now, both inside Clojure and outside, in particular vs using `IFn` and `ifn?`

I feel like Noah has listed a bunch of places where this is true above? clojure.test, expectations, midje, cider, maybe compojure too, etc.
by
I didn't look at other usage of IFn or ifn? to compare, but I can do that.
...