Skip to content

Instantly share code, notes, and snippets.

@cgrand
Created May 3, 2021 16:24
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cgrand/401448b5c702bcf2631f828b8b52c210 to your computer and use it in GitHub Desktop.
Save cgrand/401448b5c702bcf2631f828b8b52c210 to your computer and use it in GitHub Desktop.

Quick notes on how the fallback pseudo-type is used in cljd (ClojureDart).

Like cljs, cljd is built on protocols. Like in clj, each protocol is backed by an interface for the fast path. Let's call this interface the direct interface.

Cljd protocols are also backed by a second interface used for extensions to 3rd-party classes. Let's call this interface the extension interface.

Protocols are reified as instances of an IProtocol interface:

// emitted code
abstract class IProtocol extends dc.Object {
dc.dynamic extension(x$1, );
dc.dynamic satisfies(x$2, );
}

At static call sites of a protocol method (let's take -count of Counted as an example) instead of emitting a function call to -count we emit:

if((coll106192$1 is ICounted$iface)){ // check against the direct interface
$if_$1=(coll106192$1 as ICounted$iface).$_count$0(); // on success, cast and call the actual method
}else{
// retrieve extension object and call method on it, passing the actual object as first argument 
$if_$1=ICounted.extensions(coll106192$1, ).$_count$0(coll106192$1, );
}

Now, let's have a look at the cljs implementation of count:

(defn count
  "Returns the number of items in the collection. (count nil) returns
  0.  Also works on strings, arrays, and Maps"
  [coll]
  (if-not (nil? coll)
    (cond
      (implements? ICounted coll)
      (-count coll)

      (array? coll)
      (alength coll)

      (string? coll)
      ^number (.-length coll)

      (implements? ISeqable coll)
      (accumulating-seq-count coll)

      :else (-count coll))
    0))

We see there's an explicit test against an implementation interface and then several fallbacks. In our case it would mean we would check twice against the direct interface: once in user code and once as part of the emitted dart for the -count callsite.

By having the fallback pseudo-type we can move all fallbacks into it, including nil and our count is just:

(defn count [x] (-count x))

We have removed the explicit interface check, this is already a gain but we can go further if we add :inline metadata (like in Clojure):

(defn count
  {:inline (fn [x] `(-count ~x))
   :inline-arities #{1}}
  [x] (-count x))

Now all static calls to count become calls to -count (here it's trivial but for other methods, arguments order are shuffled by the inlining) and are thus "inlined" as a protocol method callsite and not a regular function call.

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