Skip to content

Instantly share code, notes, and snippets.

@CmdrDats
Last active August 29, 2015 14:13
Show Gist options
  • Save CmdrDats/28e79c364ec68f4699bf to your computer and use it in GitHub Desktop.
Save CmdrDats/28e79c364ec68f4699bf to your computer and use it in GitHub Desktop.
FX example, pulled apart.

Feature Expressions - are they answering the problem correctly?

Introduction

At its core, FX (feature expressions) allow writing a set of forms which provide different implementations depending on the 'mode' in which compilation is done.

While FX is great for convenience, the tradeoff is that it strongly encourages conflating the notion of 'how' (platform specific instructions) something is done with 'what' (logical operation) is being done.

The problem

FX lets you can now shoehorn all the different implementations (currently java & javascript, but probably CLR down the line) into a single function, walk away and pretend its a fully portable solution.

First we need to clarify the 'how' and 'what'. In this sense, 'how' is answered by asking 'how is this being done? which platform-specific instructions are we carrying out in order to achieve a logical operation'. This is distinct from from the question of 'what', which is asking 'what, in a logical sense, would I like to achieve?'

Take something like string concatenation. The 'how' in something like imperative c would be 'initialize an array of size n+m, where n is size of s1 and m is size s2, copy the characters of s1 and s2 into that array and return it.', the 'what' is to ask 'I have s1 and s2, please give me a single string which is the concatenation of the two'. If we had to speak about the implementation at this level of abstraction we bury our actual intent.

Concrete example

See before.clj which I've snippeted out of a real version of the Prismatic Schema library.

In the (extend-protocol Schema..) form, should we be caring whether it needs to extend a 'function' or a 'Class' because of platform? My argument is, no, we're trying to extend the logical notion of a class and add functions to it. I shouldn't care about platform and I should also not be forcing my readers/maintainers to have to reason about it.

See the alternate implementation in the other files. This is what code would look like if feature expressions were limited to the namespace form. You would then be required to think about the smaller parts that form your cross-platform building blocks and abstract from there, instead of simply inlining them everywhere.

The same goes for the this-class and not-instance? functions..

Don't like it? Don't use it.

Often I see this touted as a way to appease people opposed to the implementation to FX: 'if you don't like it, don't use it'. That is a bit of a strawman argument based on the naive assumption that you only work on the code that you actually wrote. In a world of open-source projects where we actively encourage people to consume and contribute, this is not even vaguely the case.

This is where the problems I've outlined with FX particularly start manifesting - when you need to reason about code written by another developer. Especially when you are not looking to consume it in other platforms, you have to apply a mind-filter in order to read the code and in my experience, #+ does not pattern match very well in your head.

Summary

Inlining abstractions directly into the code with FX forces readers of the code to spin up a new mental thread that needs to filter and overlay the context of the code in order to see the code for the platform you're currently thinking about.

FX simply introduces a layer of complexity that could be entirely removed by either limiting it for use only in the namespace declaration. I do think there are even cleaner solutions if we pull things apart a bit more, but that's not the purpose of this writeup.

Additionally, one cannot simply assume that because the feature is optional, it is harmless. It is not.

(extend-protocol Schema
#+clj Class
#+cljs function
(walker [this]
(let [class-walker (if-let [more-schema (utils/class-schema this)]
;; do extra validation for records and such
(subschema-walker more-schema)
identity)]
(clojure.core/fn [x]
(or (when #+clj (not (instance? this x))
#+cljs (or (nil? x)
(not (or (identical? this (.-constructor x))
(js* "~{} instanceof ~{}" x this))))
(macros/validation-error this x (list 'instance? this (utils/value-name x))))
(class-walker x)))))
(explain [this]
(if-let [more-schema (utils/class-schema this)]
(explain more-schema)
#+clj (symbol (.getName ^Class this)) #+cljs this)))
(ns example-lib
#+clj (:require [example-lib.impl :as])
#+cljs (:require [example-lib.impljs :refer-macros [extend-class])
)
(extend-class Schema
(walker [this]
(let [class-walker (if-let [more-schema (utils/class-schema this)]
;; do extra validation for records and such
(subschema-walker more-schema)
identity)]
(clojure.core/fn [x]
(or (when (not-instance? this x)
(macros/validation-error this x (list 'instance? this (utils/value-name x))))
(class-walker x)))))
(explain [this]
(if-let [more-schema (utils/class-schema this)]
(explain more-schema)
(this-class this))))
(defmacro extend-class [n & body]
`(extend-protocol ~n Class ~@body))
(defn this-class [o]
(symbol (.getName ^Class o)))
(defn not-instance? [o x]
(not (instance? o x)))
;; Yes, defmacro needs to be in clj file, blah blah. Assume everything is in its right place....
(defmacro extend-class [n & body]
`(extend-protocol ~n function ~@body))
(defn this-class [o] o)
(defn not-instance [o x]
(or (nil? x)
(not (or (identical? o (.-constructor x))
(js* "~{} instanceof ~{}" x o)))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment