Skip to content

Commit

Permalink
[#279, #319] defprotocol and defrecord
Browse files Browse the repository at this point in the history
  • Loading branch information
borkdude authored Jun 9, 2020
1 parent 4f4a871 commit 10e84c0
Show file tree
Hide file tree
Showing 8 changed files with 328 additions and 1 deletion.
6 changes: 6 additions & 0 deletions reflection.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,11 @@
"allPublicMethods":true,
"allPublicFields": true,
"allPublicConstructors": true
},
{
"name":"java.lang.Number",
"allPublicMethods":true,
"allPublicFields": true,
"allPublicConstructors": true
}
]
27 changes: 26 additions & 1 deletion src/sci/impl/namespaces.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
[sci.impl.macros :as macros]
[sci.impl.multimethods :as mm]
[sci.impl.parser :as parser]
[sci.impl.protocols :as protocols]
[sci.impl.records :as records]
[sci.impl.utils :as utils]
[sci.impl.vars :as vars])
#?(:cljs (:require-macros [sci.impl.namespaces :refer [copy-var copy-core-var]])))
Expand Down Expand Up @@ -572,6 +574,27 @@
'remove-method (copy-core-var remove-method)
'remove-all-methods (copy-core-var remove-all-methods)
;; end multimethods
;; protocols
'defprotocol (with-meta protocols/defprotocol
{:sci/macro true
:sci.impl/op :needs-ctx})
'extend (with-meta protocols/extend
{:sci.impl/op :needs-ctx})
'extends? protocols/extends?
'extend-type (with-meta protocols/extend-type
{:sci/macro true
:sci.impl/op :needs-ctx})
'extend-protocol (with-meta protocols/extend-protocol
{:sci/macro true
:sci.impl/op :needs-ctx})
'-reified-methods #(protocols/getMethods %)
'-reified protocols/->Reified
'reify (with-meta protocols/reify
{:sci/macro true
:sci.impl/op :needs-ctx})
'protocol-type-impl protocols/type-impl
'satisfies? protocols/satisfies?
;; end protocols
'.. (macrofy double-dot)
'= (copy-core-var =)
'< (copy-core-var <)
Expand Down Expand Up @@ -658,6 +681,9 @@
'dedupe (copy-core-var dedupe)
'defn- (macrofy defn-*)
'defonce (macrofy defonce*)
'defrecord (with-meta records/defrecord
{:sci/macro true
:sci.impl/op :needs-ctx})
'delay (macrofy delay*)
#?@(:clj ['deliver (copy-core-var deliver)])
'deref (copy-core-var deref)
Expand Down Expand Up @@ -858,7 +884,6 @@
'repeat (copy-core-var repeat)
'requiring-resolve (with-meta sci-requiring-resolve {:sci.impl/op :needs-ctx})
'run! (copy-core-var run!)
#?@(:clj ['satisfies? (copy-core-var satisfies?)])
'set? (copy-core-var set?)
'sequential? (copy-core-var sequential?)
'select-keys (copy-core-var select-keys)
Expand Down
2 changes: 2 additions & 0 deletions src/sci/impl/opts.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
'java.io.StringWriter java.io.StringWriter
'java.io.StringReader java.io.StringReader
'java.lang.Integer Integer
'java.lang.Number Number
'java.lang.Double Double
'java.lang.ArithmeticException ArithmeticException
'java.lang.Object Object}
Expand All @@ -62,6 +63,7 @@
String java.lang.String
ArithmeticException java.lang.ArithmeticException
Integer java.lang.Integer
Number java.lang.Number
Double java.lang.Double
Object java.lang.Object}
:cljs {}))
Expand Down
114 changes: 114 additions & 0 deletions src/sci/impl/protocols.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
(ns sci.impl.protocols
{:no-doc true}
(:refer-clojure :exclude [defprotocol extend-protocol
extend extend-type reify satisfies?
extends? implements?])
(:require [sci.impl.multimethods :as mms]
[sci.impl.utils :as utils]
[sci.impl.vars :as vars]))

(clojure.core/defprotocol IReified
(getInterface [_])
(getMethods [_]))

(deftype Reified [interface meths]
IReified
(getInterface [_] interface)
(getMethods [_] meths))

(defn defprotocol [_ _ _ctx protocol-name & signatures]
(let [[docstring signatures]
(let [sig (first signatures)]
(if (string? sig) [sig (rest signatures)]
[nil signatures]))
expansion
`(do
(def ~(with-meta protocol-name
{:doc docstring}) {:methods #{}
:ns *ns*})
~@(map (fn [[method-name & _]]
`(do
(defmulti ~method-name clojure.core/protocol-type-impl)
(defmethod ~method-name :sci.impl.protocols/reified [x# & args#]
(let [methods# (clojure.core/-reified-methods x#)]
(apply (get methods# '~method-name) x# args#)))
#?(:clj (alter-var-root (var ~protocol-name)
update :methods conj ~method-name)
:cljs (def ~protocol-name
(update ~protocol-name :methods conj ~method-name)))))
signatures))]
;; (prn expansion)
expansion))

(defn extend-protocol [_ _ ctx protocol-name & impls]
(let [impls (utils/split-when #(not (seq? %)) impls)
protocol-var (@utils/eval-resolve-state ctx protocol-name)
protocol-ns (-> protocol-var deref :ns)
pns (str (vars/getName protocol-ns))
fq-meth-name #(symbol pns %)
expansion
`(do ~@(map (fn [[type & meths]]
`(do
~@(map (fn [meth]
`(defmethod ~(fq-meth-name (str (first meth)))
~type
~(second meth) ~@(nnext meth)))
meths)))
impls))]
#_(prn expansion)
expansion))

(defn extend [ctx atype & proto+mmaps]
(doseq [[proto mmap] (partition 2 proto+mmaps)
:let [proto-ns (:ns proto)
pns (vars/getName proto-ns)]]
#_(when-not (protocol? proto)
(throw (new #?(:clj IllegalArgumentException
:cljs js/Error)
(str proto " is not a protocol"))))
#_(when (implements? proto atype)
(throw (new #?(:clj IllegalArgumentException
:cljs js/Error)
(str atype " already directly implements " (:on-interface proto) " for protocol:"
(:var proto)))))
(doseq [[fn-name f] mmap]
(let [fn-sym (symbol (name fn-name))
env @(:env ctx)
multi-method-var (get-in env [:namespaces pns fn-sym])
multi-method @multi-method-var]
(mms/multi-fn-add-method-impl multi-method atype f))
)
#_(-reset-methods (vars/alter-var-root (:var proto) assoc-in [:impls atype] mmap))))

(defn extend-type [_ _ ctx type & proto+meths]
(let [proto+meths (utils/split-when #(not (seq? %)) proto+meths)]
`(do ~@(map (fn [[proto & meths]]
(let [protocol-var (@utils/eval-resolve-state ctx proto)
protocol-ns (-> protocol-var deref :ns)
pns (str (vars/getName protocol-ns))
fq-meth-name #(symbol pns %)]
`(do
~@(map (fn [meth]
`(defmethod ~(fq-meth-name (str (first meth)))
~type ~(second meth) ~@(nnext meth)))
meths)))) proto+meths))))

(defn reify [_ _ _ctx interface & meths]
(let [meths (into {} (map (fn [meth]
`['~(first meth) (fn ~(second meth) ~@(nnext meth))])
meths))]
`(clojure.core/-reified ~interface ~meths)))

(defn type-impl [x & _xs]
(or (when (instance? sci.impl.protocols.Reified x)
:sci.impl.protocols/reified)
(some-> x meta :sci.impl/type)
(type x)))

(defn satisfies? [protocol obj]
(boolean (some #(get-method % (type-impl obj)) (:methods protocol))))

(defn extends?
"Returns true if atype extends protocol"
[protocol atype]
(boolean (some #(get-method % atype) (:methods protocol))))
36 changes: 36 additions & 0 deletions src/sci/impl/records.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
(ns sci.impl.records
{:no-doc true}
(:refer-clojure :exclude [defrecord])
(:require [sci.impl.utils :as utils]
[sci.impl.vars :as vars]))

(defn defrecord [_ _ ctx record-name fields & protocol-impls]
(let [factory-fn-sym (symbol (str "->" record-name))
constructor-sym (symbol (str record-name "."))
keys (mapv keyword fields)
protocol-impls (utils/split-when symbol? protocol-impls)
protocol-impls
(mapv (fn [[protocol-name impl]]
(let [protocol-var (@utils/eval-resolve-state ctx protocol-name)
protocol-ns (-> protocol-var deref :ns)
pns (str (vars/getName protocol-ns))
fq-meth-name #(symbol pns %)
args (second impl)
this (first args)
bindings (vec (mapcat (fn [field]
[field (list (keyword field) this)])
fields))]
`(defmethod ~(fq-meth-name (str (first impl))) '~record-name ~(second impl)
(let ~bindings
~@(nnext impl)))))
protocol-impls)]
`(do
;; (prn '~record-name)
(def ~record-name '~record-name)
(defn ~factory-fn-sym [& args#]
(vary-meta (zipmap ~keys args#)
assoc
:sci.impl/record true
:sci.impl/type '~record-name))
(def ~constructor-sym ~factory-fn-sym)
~@protocol-impls)))
11 changes: 11 additions & 0 deletions src/sci/impl/utils.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,14 @@
(def eval-require-state (volatile! nil))
(def eval-use-state (volatile! nil))
(def eval-resolve-state (volatile! nil))

(defn split-when
"Like partition-by but splits collection only when `pred` returns
a truthy value. E.g. `(split-when odd? [1 2 3 4 5]) => ((1 2) (3 4) (5))`"
[pred coll]
(lazy-seq
(when-let [s (seq coll)]
(let [fst (first s)
f (complement pred)
run (cons fst (take-while #(f %) (next s)))]
(cons run (split-when pred (lazy-seq (drop (count run) s))))))))
94 changes: 94 additions & 0 deletions test/sci/protocols_test.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
(ns sci.protocols-test
(:require #?(:cljs [clojure.string :as str])
[clojure.test :refer [deftest is]]
[sci.test-utils :as tu]))

(deftest protocol-test
(let [prog "
(ns foo)
(defprotocol AbstractionA
(foo [obj])
(bar [obj]))
(ns bar)
(defprotocol AbstractionB
\"A cool protocol\"
(fooB [obj x]))
(ns baz)
(require '[foo :as f :refer [AbstractionA]]
'[bar :refer [AbstractionB fooB]])
(extend Number AbstractionA
{:foo (fn [_] :number)
:bar (fn [_] :bar/number)})
(extend-protocol AbstractionA
nil
(foo [s] (str \"foo-A!\"))
(bar [s] (str \"bar-A!\"))
String
(foo [s] (str \"foo-A-\" (.toUpperCase s)))
(bar [s] (str \"bar-A-\" (.toUpperCase s))))
(extend-type Object
AbstractionA
(foo [_] :foo/object)
(bar [_] :bar/object)
AbstractionB
(fooB [_ x] x))
[(f/foo nil)
(f/bar nil)
(f/foo \"Bar\")
(f/bar \"Bar\")
(f/foo 1)
(f/bar 1)
(f/foo {})
(f/bar {})
(fooB {} :fooB/object)
(satisfies? AbstractionA 1)]"
prog #?(:clj prog
:cljs (-> prog
(str/replace "String" "js/String")
(str/replace "Number" "js/Number")
(str/replace "Object" ":default")))]
(is (= ["foo-A!"
"bar-A!"
"foo-A-BAR"
"bar-A-BAR"
:number
:bar/number
:foo/object
:bar/object
:fooB/object
true]
(tu/eval* prog #?(:clj {}
:cljs {:classes {:allow :all
'js #js {:String js/String
:Number js/Number}}}))))))

(deftest docstring-test
(is (= "-------------------------\nuser/Foo\n cool protocol\n" (tu/eval* "
(defprotocol Foo \"cool protocol\" (foo [_]))
(with-out-str (clojure.repl/doc Foo))
" {}))))

(deftest reify-test
(let [prog "
(defprotocol Fruit (subtotal [item]))
(def x (reify Fruit (subtotal [_] 1)))
(subtotal x)"]
(is (= 1 (tu/eval* prog {})))))

(deftest extends-test
(let [prog "
(defprotocol Area (get-area [this]))
(extend-type String Area (get-area [_] 0))
(extends? Area String)"
prog #?(:clj prog
:cljs (-> prog
(str/replace "String" "js/String")))]
(is (true? (tu/eval* prog #?(:clj {}
:cljs {:classes {:allow :all
'js #js {:String js/String}}}))))))
39 changes: 39 additions & 0 deletions test/sci/records_test.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
(ns sci.records-test
(:require [clojure.test :refer [deftest is testing]]
[sci.test-utils :as tu]))

(deftest protocol-test
(let [prog "
(defrecord Foo [a b])
(let [r (->Foo 1 2)]
[(:a r) (:b r)])"]
(is (= [1 2] (tu/eval* prog {}))))
(testing "protocols"
(let [prog "
(ns foo)
(defprotocol Foo (foo [_] 42))
(defprotocol Graph (graph [_]))
(ns bar (:require [foo :as f]))
(defrecord FooRecord [a b]
f/Foo (foo [_] (dec a)))
(defrecord BarRecord [a b]
f/Foo (foo [_] (inc b)))
(extend FooRecord
f/Graph {:graph (fn [x] {:from (:a x) :to (:b x)})})
(let [a (->FooRecord 1 2) b (BarRecord. 1 2)]
[(f/foo a) (f/foo b) (f/graph a) (satisfies? f/Graph a)])"]
(is (= [0 3 {:from 1, :to 2} true] (tu/eval* prog {}))))))

(deftest extends-test
(let [prog "
(defprotocol Area (get-area [this]))
(defrecord Rectangle [width height]
Area
(get-area [this]
(* width height)))
(extends? Area Rectangle)"]
(is (true? (tu/eval* prog {})))))

0 comments on commit 10e84c0

Please sign in to comment.