Skip to content

Instantly share code, notes, and snippets.

@aphyr

aphyr/class.clj Secret

Created November 10, 2023 00:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aphyr/1f5d52a7703369a5cf304b21eeaa2e3e to your computer and use it in GitHub Desktop.
Save aphyr/1f5d52a7703369a5cf304b21eeaa2e3e to your computer and use it in GitHub Desktop.
Bonus material for the "Rewriting the Technical Interview" story
(ns scratch.object.class
"This namespace defines a basic framework for class-based object-oriented
programming, including classes, instances, fields, methods, single
inheritance, and multiple inheritance through abstract classes."
(:refer-clojure :exclude [class])
(:require [clojure [core :as c]
[pprint :refer [pprint]]
[set :as set]
[string :as str]
[walk :as walk]]
[scratch.rewrite :as r :refer [rewrite]]))
;; OO syntax
(defn dot-sym
"Returns the keyword :foo when given '.foo"
[x]
(when (and (symbol? x)
(= \. (first (name x))))
(keyword (subs (name x) 1))))
(defn sym-dot-sym
"Returns the sequence '(foo .bar .baz) given .foo .bar .baz."
[x]
(when (symbol? x)
(let [[sym & more] (remove #{""} (str/split (name x) #"\."))]
(when more
(cons (if (qualified-symbol? x)
(symbol (namespace x) sym)
(symbol sym))
(map symbol (map (partial str ".") more)))))))
(defn plain-sym?
"Is this a boring old symbol?"
[x]
(and (symbol? x)
(not (dot-sym x))))
(def space
"Having trouble fitting else if into nested branches? Try this SPECIAL SPACE
[ ]"
"")
(defn spaced-sym
"Splits up magic-spaced symbols into their parts."
[x]
(when (symbol? x)
(let [parts (str/split (name x) (re-pattern space))]
(when (< 1 (count parts))
(map symbol parts)))))
(defmacro a
"Takes an AST node name (e.g. ::method, ::field=) or a class, and generates a
predicate function checking if a node is that kind of AST node."
[node-type]
(if (keyword? node-type)
`(fn ~'a [term#]
(and (vector? term#)
(= ~node-type (first term#))))
`(fn ~'a [term#]
(instance? ~node-type term#))))
(defmacro either
[& fs]
(let [term (gensym 'term)]
`(fn ~'or* [~term]
(or ~@(map list fs (repeat term))))))
(declare call-method)
(declare get-field)
(declare set-field!)
(defrecord Cond [branches])
(defrecord Elsif [test body])
(defrecord FnCall [fun args])
(def infix (into '{% mod
== =}
(map (juxt identity identity) '[< <= > >= + - / *])))
(defn sym++
"Takes a symbol like i++ and returns 'i"
[x]
(when (symbol? x)
(when-let [[_ v] (re-find #"(.+)\+\+$" (name x))]
(symbol v))))
(def postfixes {"++" inc
"--" dec})
(defn postfix-sym
[x]
(when (symbol? x)
(when-let [p (first (filter (partial str/ends-with? (name x))
(keys postfixes)))]
(list (postfixes p)
(symbol (str/replace (name x) p ""))))))
(defn gen-for
"Takes for expressions separated by a fake semicolon (;) and generates a
loop/recur around them."
[exprs body]
(let [[[var _ init :as start] test change]
(remove '#{(;)} (partition-by #{';} exprs))
body (mapcat identity body)]
; (prn :start start :test test :change change)
`(loop([~var ~init
ret# nil]
if (~@test) {
recur(do(~@change), do(~@body))
} ~'else {
~'return ret#
}))))
(defn braces
"Turns { x y z t } to `(do x y z t)"
[m]
(cons 'do (mapcat identity m)))
(defn do-one
"Is this a (do x)?"
[x]
(when (and (seq? x)
(= 'do (first x))
(= 2 (count x)))
(second x)))
(defmacro oo
"This macro executes any number of expressions in our object-oriented
paradigm. Expressions are resolved as Clojure, except we offer special syntax
for getting fields and calling methods, as per transform-oo."
[& exprs]
(rewrite `(do ~@exprs)
; foo.bar.baz in seqs
[sym-dot-sym s] (sym-dot-sym s)
; foo.bar.baz alone
(sym-dot-sym s) `(do ~@(sym-dot-sym s))
; .method(a,b)
[dot-sym method, seq? args] [[::method (dot-sym method) args]]
; for (init, test, change) { ... }
[#{'for} _, seq? expr, map? body] (gen-for expr body)
; Magic spaces
[spaced-sym s] (spaced-sym s)
; else if (pred) { t }
[#{'else} _, #{'if} _, seq? test, map? body]
[(Elsif. `(do ~@test) (braces body))]
; if (pred) { t }
[#{'if} _, seq? test, map? t] [(Cond. [`(do ~@test) (braces t)])]
; Cond Elsif
[(a Cond) cond, (a Elsif) elsif]
[(update cond :branches conj (:test elsif) (:body elsif))]
; Cond else { ... }
[(a Cond) cond, #{'else} _, map? body]
[(update cond :branches conj :else (braces body))]
; some_fun(a, b)
[plain-sym? fun, seq? args] [(FnCall. fun args)]
; .field =
[dot-sym field, #{'=} _] [[::field= (dot-sym field)]]
; .field
[dot-sym field] [[::field (dot-sym field)]]
; We want methods and fields to bind left-to-right, so they chain
; properly--and they *must* have equal binding precedence. We can't
; run these rules separately.
[any? obj, (either (a ::field) (a ::method)) [type field args]]
[(if (= ::field type)
`(get-field ~obj ~field)
`(call-method ~obj ~field ~@args))]
; ++
[sym++ x] [`(inc ~(sym++ x))]
; Infix
[any? a, infix f, any? b] [(FnCall. (infix f) [a b])]
; obj [:field= :bar] rhs
[any? obj, (a ::field=) [_ field], any? rhs]
[`(set-field! ~obj ~field ~rhs)]
; sym = rhs
[plain-sym? var, #{'=} _, any? rhs, & more]
[`(let [~var ~rhs] ~@more)]
; I *do* so love the ternary (NOT ACTUALLY A :)
[any? test, #{'?} _, any? t, #{'∶} _, any? f] [`(if ~test ~t ~f)]
; Unwrap function calls
((a FnCall) fc) (cons (:fun fc) (:args fc))
; Unwrap Conds
((a Cond) c) `(cond ~@(:branches c))
; Strip return and fake semicolons (0x37e Greek Question Mark)
['#{return ;} _] nil
))
(pprint (macroexpand (quote (oo
;x = 1
;foo.bar
;dog.toys()
;dog.parts.mouth = dog.toys().ball
;prn()
;Person(1)
;my-count = count([1, dog.toys])
;println("Hi! My name is", name.str(),
; "and my bestie is", bff.name.str())
;dog.cute() ? :yes ∶ :no
; if (test expr) { true branch } else { false branch }
;for (i = 1 ; i < 10 ; i++) {
; println(i);
;}
;if (test) {
; true branch
;} else if (test2) {
; other branch
;} else {
; final yay
;}
))))
;(pprint (oo
; for (i = 1 ; i < 10 ; i++) {
; println(i);
; return [:got i];
; }
;))
(pprint (macroexpand '(oo
for (i = 1 ; i < 101 ; i++) {
if (i % 15 == 0) {
println("FizzBuzz");
} else if (i % 3 == 0) {
println("Fizz");
} else if (i % 5 == 0) {
println("Buzz");
} else {
println(i);
}
)))
;(assert false)
;; Algol-style
(defmacro algol
"OO, mildly less terrifying"
[& exprs]
(rewrite `(do ~@exprs)
; for (init, test, change) { ... }
[#{'for} _, seq? expr, map? body] (gen-for expr body)
; Magic spaces
[spaced-sym s] (spaced-sym s)
; else if (pred) { t }
[#{'else} _, #{'if} _, seq? test, map? body]
[(Elsif. `(do ~@test) (braces body))]
; if (pred) { t }
[#{'if} _, seq? test, map? t] [(Cond. [`(do ~@test) (braces t)])]
; Cond Elsif
[(a Cond) cond, (a Elsif) elsif]
[(update cond :branches conj (:test elsif) (:body elsif))]
; Cond else { ... }
[(a Cond) cond, #{'else} _, map? body]
[(update cond :branches conj :else (braces body))]
; some_fun(a, b)
[symbol? fun, seq? args] [(FnCall. fun args)]
; ++
[sym++ x] [`(inc ~(sym++ x))]
; Infix
[any? a, infix f, any? b] [(FnCall. (infix f) [a b])]
; sym = rhs
[symbol? var, #{'=} _, any? rhs, & more]
[`(let [~var ~rhs] ~@more)]
; I *do* so love the ternary (NOT ACTUALLY A :)
[any? test, #{'?} _, any? t, #{'∶} _, any? f] [`(if ~test ~t ~f)]
; Unwrap function calls
((a FnCall) fc) (cons (:fun fc) (:args fc))
; Unwrap Conds
((a Cond) c) `(cond ~@(:branches c))
; Strip return and fake semicolons (0x37e Greek Question Mark)
['#{return ;} _] nil
))
;; Classes
(defn field-opts
"Given a class and a field name, returns the options for that field."
[class field]
(let [field-opts (:field-opts class)]
(assert (contains? (:field-opts class) field)
(str "No such field " field " defined in class " (:name class)))
(get field-opts field)))
(defn mutable?
"Is the given field mutable?"
[class field]
(boolean (:mutable? (field-opts class field))))
(defn instance
"Constructs an instance of a class. Takes the class as the first argument,
and a map of field values as the second."
[class fields]
{:class class
:fields (->> fields
(map (fn [[field value]]
; Wrap mutable fields in atoms.
[field (if (mutable? class field)
(atom value)
value)]))
(into {}))})
; Hat tip to https://gist.github.com/devn/c52a7f5f7cdd45d772a9
(defn gen-nonvariadic-invokes [f]
(for [arity (range 1 21),
:let [args (repeatedly arity gensym)]]
`(~'invoke [~@args] (~f ~@args))))
(defn gen-variadic-invoke [f]
(let [args (repeatedly 22 gensym)]
`(~'invoke [~@args] (apply ~f ~@args))))
(defn gen-apply-to [f]
`(~'applyTo [this# args#] (apply ~f this# args#)))
(defn extend-IFn [f]
`(clojure.lang.IFn
~@(gen-nonvariadic-invokes f)
~(gen-variadic-invoke f)
~(gen-apply-to f)))
(defmacro defrecordfn
"Like defrecord, but accepts a function f before any specs that is
used to implement clojure.lang.IFn. f should accept at least one
argument, 'this'."
[name [& fields] f & opts+specs]
`(defrecord ~name [~@fields]
~@(extend-IFn f)
~@opts+specs))
(defrecordfn Classy [name super fields field-opts methods]
(fn constructor [this & args]
(instance this (zipmap fields args)))
Object
(toString [this]
(c/name name)))
(defn make-class
"A *class* extends a superclass with a sequence of fields which each
instance must have, a map of option maps for those fields, and a map of
method names to functions implementing those methods. Keys in field maps are
keywords, and their values are maps describing those fields, like {:mutable?
true}. `nil` is a legal field map."
[name super fields field-opts methods]
(let [field-opts (merge (zipmap fields (repeat nil))
field-opts)]
(Classy. name super fields field-opts methods)))
(def Top
"This is the top-level class for all objects---including Clojure objects that
don't participate in our little OO party."
(make-class 'Top nil nil nil
{:class #(or (:class %) Top)
:super (fn [_] nil)
:str str
:inspect pr-str
:pp (fn [this]
(println (call-method this :inspect)))}))
(defn get-class
"Returns the class of anything. If an object has a :class, uses that;
otherwise, the class is Top."
[x]
(or (:class x) Top))
(defmacro class
"Works as both defclass and get-class"
([instance]
`(get-class ~instance))
([name & args]
`(defclass ~name ~@args)))
(defn get-field
"Gets the value of a field in the given object."
[obj field-name]
(if (mutable? (class obj) field-name)
@(get (:fields obj) field-name)
(get (:fields obj) field-name)))
(defn set-field!
"Sets the value of a mutable field in the given object. Returns value."
[obj field-name value]
(assert (mutable? (class obj) field-name))
(reset! (get (:fields obj) field-name) value)
value)
(defn all-methods
"Returns all methods available on an object, including via superclasses."
[obj]
(loop [methods #{}
c (class obj)]
(if c
(recur (into methods (keys (:methods c)))
(:super c))
methods)))
(defn resolve-method
"Takes a class, and looks up the given method name on it, checking
superclasses if necessary."
[class method-name]
(if-let [f (-> class :methods (get method-name))]
f
(when-let [super (:super class)]
(resolve-method super method-name))))
(defn call-method-with-class
"Takes a class, an object, and a method name. Looks up the corresponding
method in this class (or superclasses) via resolve-method. Calls that method,
providing the object as the first argument, and passing in any additional
arguments thereafter."
[class obj method-name args]
(if-let [method (resolve-method class method-name)]
(apply method obj args)
(throw (ex-info (str "No such method " method-name
" for object of class " (:name class) ": "
(pr-str obj))
{}))))
(defn call-method
"Takes an object and a method name. Looks up the corresponding method in the
object's class (or superclasses) via resolve-method. Calls that method,
providing the object as the first argument, and passing in any additional
arguments thereafter."
[obj method-name & args]
(call-method-with-class (class obj) obj method-name args))
(def Obj
(make-class
'Obj Top nil nil
{:super (fn super [this]
(update this :class :super))
:inspect
(fn inspect [this]
(let [class (class this)]
; For standard objects, we print out their fields and values.
(c/str "<"
(:name class)
" "
(->> (:fields class)
(map (fn [field]
(c/str (name field)
"="
(-> this
(get-field field)
(call-method :inspect)))))
(str/join " "))
">")))
:str (fn str [this]
(call-method this :inspect))}))
(defn rewrite-method
"Takes a set of instance variable names, as well as method expression like
(greet [name] ...) and rewrites it to [:greet (fn greet [this name] ...).
References to instance variables are rewritten to `this.varname`. This *does*
prevent you from binding field names in let bindings etc, but this is a
quick-and-dirty example and we're trying to save space."
[instance-vars [method-name arglist body]]
(let [; Allow using map literals for bodies
body (if (map? body)
(mapcat identity body)
body)
; Rewrite variables in instance scope
body (walk/postwalk
(fn [expr]
(if (and (symbol? expr)
(not (qualified-symbol? expr))
; For foo.bar.baz, check to see if foo is a field.
(contains? instance-vars (-> (name expr)
(str/split #"\.")
first
symbol)))
(symbol (str "this." expr))
expr))
body)]
`[~(keyword method-name)
(fn ~method-name [~'this ~@arglist]
(oo ~@body))]))
(defn parse-fields
"Parses field definitions from a series of forms, returning [fields,
field-opts, remaining-forms]."
[forms]
(loop [fields []
field-opts {}
next-field-opts {}
[form & more :as forms] (seq forms)]
(let [next-form (first more)]
(cond (or (empty? forms) ; End of class
(seq? next-form)) ; Arglist impending
[fields field-opts forms]
; We're a modifier
(= 'mutable form)
(recur fields
field-opts
(assoc next-field-opts :mutable? true)
more)
; We're a field
true
(let [field (keyword form)]
(recur (conj fields field)
(assoc field-opts field next-field-opts)
{}
more))))))
; We could define all our classes this way, but it might be convenient to have
; a macro for this.
(defmacro defclass
"Defines a class with the given name, and binds it to the Clojure Var of the
same name. Forms proceed in the following order:
1. Superclass. Provide < Foo to indicate that this class inherits from Foo.
If not provided, defaults to Obj.
2. Fields. A series of symbols. The special symbol 'mutable modifies the
following field name. For instance, `id mutable age mutable gender`
defines three fields: id, age, and gender.
3. Methods. Methods begin with a symbol (the method name) and are followed by
a list of arguments, followed by a list of forms--the method body. Method
arguments omit the receiver, which is bound automatically to `this`. In
method bodies, oo syntax applies, and this class's instance variables and
methods are in local scope. If your body contains an even number--fewer
than 17--of expressions, and does not repeat odd-numbered forms, you may
write it using braces. You may write longer bodies, at the cost (feature?)
of nondeterministic execution order.
(defclass Person < Obj
name;
mutable bff;
make-bffs(friend) {
this.bff = friend;
friend.bff = this;
}
say-hi () (
(println \"Hi! My name is \" name \" and my bestie is \" bff.name));
)"
[name & forms]
(let [; 1. Identify the superclass.
[super forms] (if (= '< (first forms))
[(second forms) (drop 2 forms)]
[Obj forms])
; 2. Parse field definitions
[fields field-opts forms] (parse-fields forms)
; 3. Parse methods. Each method comes in a `name (arglist) body` trio.
methods (partition 3 forms)
; In methods, our local scope will include all fields defined on this
; class itself, and all methods on this class *or* its supers.
instance-vars (->> (concat fields
(map first methods)
(all-methods {:class (eval super)}))
(map symbol)
set)
; Now, transform methods bodies into a map of names to functions.
methods (->> methods
(map (partial rewrite-method instance-vars))
(into {}))]
`(do ; Define class
(def ~name
(make-class '~name
~super
~fields
~field-opts
~methods)))))
(def return
"for ~reasons~"
nil)
;(pprint (clojure.walk/macroexpand-all (quote
(class Person < Obj
name;
mutable bff;
say-hi() {
println("Hi! My name is", name.str(),
"and my bestie is", bff.name.str());
}
make-bffs(friend) {
this.bff = friend;
friend.bff = this;
}
)
;)))
;(pprint (macroexpand '
(oo
kate = Person("Kate", nil);
onika = Person("Onika", nil);
kate.make-bffs(onika);
onika.bff.say-hi();
)
;))
; Sequential defines the basic features of sequential collections: first, rest,
; map, etc.
(declare LazyCons)
(class Sequential
first() { return :not-implemented }
rest() { return :not-implemented }
count() { return :not-implemented }
empty?() {
zero?(count());
}
map(f) {
return empty?() ? this ∶ LazyCons(f(first()), fn([] rest().map(f)));
}
reduce(init, f) {
loop([s this, acc init]
if (s.empty?()) {
return acc;
} else {
recur(s.rest(), f(acc, s.first()));
});
}
clj-seq() {
if (empty?()) {
return nil;
} else {
lazy-seq(
c/cons(first(), rest().clj-seq()));
}
inspect() (
;prn("Inspecting" call-method-with-class(Obj, this, :inspect, []))
c/str("(",
str/join(", " map(fn([x] x.str())).clj-seq()),
")"));
)
(class Empty < Sequential
first() { }
rest() ( (Empty) )
count() ( 0 )
)
(class Cons < Sequential
first;
rest;
count;
first() { return first }
rest() { return rest }
count() { return count }
)
(defn ->Cons
[first rest]
(oo count = inc(rest.count())
Cons(first, rest, count)))
(defn clj->Cons
"Converts a Clojure sequence to a Cons representation."
[s]
(reduce (fn [more x]
(->Cons x more))
(Empty)
(reverse s)))
(class LazyCons < Sequential
first;
rest-fn;
empty?() { return false }
first() { return first }
rest() (
f = rest-fn;
return f();
)
)
(class Array < Sequential
mutable v;
count() {
c/count(v);
}
first() {
c/first(v);
}
rest() {
c/empty?(v) ? Empty() ∶ Array(subvec(v, 1));
}
get(i) {
nth(v, i);
}
set [i value] {
v = assoc(v, i, value);
}
map(f) {
; Our map returns an Array. FOR PERFORMANCE.
Array(c/mapv(f v));
}
reduce(init, f) {
c/reduce(f, init, v);
}
inspect() {
seq-rep = call-method-with-class(Sequential, this, :inspect, [])
c/str("[", subs(seq-rep, 1, dec(c/count(seq-rep))), "]");
}
)
(oo
;l = ->Cons(1, ->Cons(2, Empty()))
;l.pp()
;l.map(inc).pp()
;a = Array([1 2 3])
;a.pp()
;a.get(2).pp()
;a.set(0, 5)
;a.pp()
;a.map(inc).pp()
; Arrays are fast!
n = 10000
coll = range(n)
bigl = clj->Cons(coll)
biga = Array(vec(coll))
time(bigl.map(inc).reduce(0, +).pp())
time(biga.map(inc).reduce(0, +).pp())
time(->>(coll, map(inc), reduce(+, 0)).pp())
)
(ns scratch.rewrite
"The world's shittiest term rewriting system"
(:require [clojure [pprint :refer [pprint]]
[walk :refer [postwalk]]]))
(defn fixed-point
"Applies f repeatedly to x until it converges."
[f x]
(let [x' (f x)]
;(Thread/sleep 3000)
;(prn :fp x')
(if (= x x')
x
(recur f x'))))
(defn rewrite-seq-1
"Scans a sequence once, applying rewrite function f to expr, then (next
expr), then (next (next expr), etc. Returns `expr` with that change. f
returns something falsey when it doesn't want to change that subsequence. Not
recursive."
([f expr]
(rewrite-seq-1 f [] expr))
([f scanned expr]
; (prn :scan expr)
(if (seq expr)
(if-let [expr' (f expr)]
(into scanned expr')
(recur f
(conj scanned (first expr))
(next expr)))
scanned)))
(defn rewrite-term
"Rewrites any term, without recursion."
[f term]
; ah yes, the three genders
; (prn :rewrite-term term (class term))
(cond (map-entry? term) term
(vector? term) (vec (rewrite-seq f term))
(seq? term) (if (seq term)
(seq (rewrite-seq f term))
term)
:else (or (f term) term)))
(defn rewrite-walk-1
"Rewrites term using rewrite-fn f recursively, in one pass."
[f term]
;(prn :walk term)
;(pprint term)
(postwalk (partial rewrite-term f) term))
(defn rewrite-walk
"Rewrite-walk repeatedly until converged."
[term f]
(fixed-point (partial rewrite-walk-1 f) term))
(defn single-rule
"To match non-sequential things, provide an expression with a guard, symbol,
and body expression, and generates a function that replaces things that match
`guard` with `body`, having `name` bound:
'[(guard? term) body]
(fn [term]
(when (guard? term)
body))"
[[[guard term] body]]
`(fn ~'rule [~term]
(when (~guard ~term)
~body)))
(defn seq-rule
"You can pattern match sequences using (guard, sym) binding pairs, and
provide an explicit more pattern to capture the rest of the seq.
[[fx x, fy y, & more] body]
(fn [term]
(when (seqable? term)
(let [[x y & more] term]
(when (and (fx x) (fy y))
body))).
Or, without & more, prepends whatever seq body returns onto the rest of the
term. Something like:
[[fx x, :fy y] body]
(fn [term]
(when (seqable? term)
(let [[x y & more] term]
(when (and (fx x) (fy y))
(concat body more))))"
[[bindings body]]
(let [[bindings [_ more]] (split-with (complement #{'&}) bindings)
more-sym (or more (gensym 'more))
pairs (partition 2 bindings)
term (gensym 'term)
guards (map first pairs)
names (map second pairs)
; TODO: eval guard exprs once, so you can (fn-returning-pred-fn)
; efficiently
guard-exprs (map-indexed (fn [i guard]
`(~guard (nth ~term ~i)))
guards)]
`(fn ~'rule [~term]
(try
(when (and (sequential? ~term)
(<= ~(count guards) (count ~term))
~@guard-exprs)
; Bind names
(let [[~@names ~'& ~more-sym] ~term]
; Return body
~(if more
; Explicit more form
body
; Tack on the rest after whatever body they give us
`(concat ~body ~more-sym))))
(catch RuntimeException e#
(throw (RuntimeException.
(str "Rewrite rule " (pr-str '~bindings)
" threw on term " (pr-str ~term))
e#)))))))
(defn rule
"Takes a rule spec (see single-rule and seq-rule) and generates the
appropriate rule fn."
[rule]
(if (vector? (first rule))
(seq-rule rule)
(single-rule rule)))
(defmacro rewrite
"Takes a term and a series of rewrite rules, and applies those rules to the
term in order."
[expr & rules]
(let [rules (partition 2 rules)
matches (map rule rules)]
`(let [rules# [~@matches]]
(reduce rewrite-walk ~expr rules#))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment