speculative

https://github.com/borkdude/speculative
borkdude 2018-11-11T10:23:19.000600Z

that has changed: https://github.com/clj-commons/secretary

borkdude 2018-11-11T11:27:46.002100Z

I believe the current merge-with spec was too general and accepted inputs like (merge-with assoc {:a 1} [:a :b]) which isn’t valid in clj + cljs. This PR updates the spec: https://github.com/slipset/speculative/pull/100 I couldn’t come up with an example where we needed associative? instead of map?

borkdude 2018-11-11T14:54:43.002700Z

oh wow, secretary now works on a cljs version from 2014, thanks mfikes 🙂

mfikes 2018-11-11T14:55:50.003100Z

I tried updating its dependencies... that's a much bigger project 🙂

borkdude 2018-11-11T16:00:56.004400Z

so (seqable? nil) was false in v339 of clojurescript, but this got fixed in v439. I suggest not accounting for bugs in spec itself and always assume the newest version.

borkdude 2018-11-11T16:01:01.004600Z

https://github.com/slipset/speculative/pull/103

borkdude 2018-11-11T16:01:54.005200Z

or maybe we could do a goog-define/version thing like @mfikes suggested earlier. not sure how that works

borkdude 2018-11-11T16:05:56.007Z

we could do something like:

(when  (compare-fn *clojurescript-version ...)
  (defn seqable? [v] (or (nil? v) (clojure.core/seqable? v))))
?

mfikes 2018-11-11T16:24:40.009400Z

If you ever need to compare versions you can do something like

(goog.string/compareVersions *clojurescript-version* "1.10.339")

mfikes 2018-11-11T16:24:59.009800Z

It would be super nice if there were a way to further use this to DCE

mfikes 2018-11-11T16:25:47.010100Z

This probably has to be a runtime thing, though

mfikes 2018-11-11T16:28:23.011300Z

So, if you want to use that, you can compose predicates like pos?, nat-int?, etc. with the result to take decisions at runtime.

borkdude 2018-11-11T16:29:33.011900Z

ah, meanwhile I came up with:

#?(:cljs
   (let [cljs-version (mapv js/parseInt
                            (str/split *clojurescript-version* #"\."))]
     (when (pos? (compare [1 10 439] cljs-version))
       (println "redefining seqable?")
       (defn seqable? [v]
         (or (nil? v)
             (clojure.core/seqable? v))))))

mfikes 2018-11-11T16:30:37.013300Z

I guess you should also be prepared the possibilty that *clojurescript-version* is nil in case someone is using a non-built ClojureScript (i.e. as a :local/root or Git Dep)

borkdude 2018-11-11T16:31:45.013700Z

the code of compareVersion sure looks a lot more involved 🙂

borkdude 2018-11-11T16:32:14.014300Z

but I’ll use that, it’s probably more robust

borkdude 2018-11-11T16:32:18.014500Z

and account for nil

mfikes 2018-11-11T16:32:40.014900Z

Yeah, the goog thing can handle weird crap like (goog.string/compareVersions "1b" "1a")

borkdude 2018-11-11T16:32:57.015400Z

and we cannot assume clojurescript won’t ever adopt this

borkdude 2018-11-11T16:35:57.015800Z

how about this then?

(ns speculative.core
  (:refer-clojure :exclude [seqable? reduceable?])
  (:require [clojure.spec.alpha :as s]
            [clojure.spec.gen.alpha :as gen]
            [goog.string]))

#?(:cljs
   (when-let [cljs-version *clojurescript-version*]
     (when (pos? (goog.string/compareVersions "1.10.439" *clojurescript-version*))
       (defn seqable? [v]
         (or (nil? v)
             (clojure.core/seqable? v))))))

mfikes 2018-11-11T16:37:54.016300Z

I guess if you exclude seqable? you may want to define it in the else branches?

borkdude 2018-11-11T16:38:16.016700Z

yeah, I refer to clojure.core/seqable? in the one other place it’s used

borkdude 2018-11-11T16:38:47.016900Z

wait that doesn’t make sense, sorry 🙂

borkdude 2018-11-11T16:39:53.017200Z

(if (pos? (goog.string/compareVersions "1.10.439" *clojurescript-version*))
       (defn seqable? [v]
         (or (nil? v)
             (clojure.core/seqable? v)))
       (def seqable? clojure.core/seqable?))

mfikes 2018-11-11T16:41:50.017900Z

Even though that happens to work, each of the versions passed to that fn are supposed to be either a string or number

borkdude 2018-11-11T16:42:20.018200Z

yeah I have a nil condition around this code

mfikes 2018-11-11T16:43:15.019200Z

Right, then you may not have a seqable? Maybe (and (some? *clojurescript-version*) ...

borkdude 2018-11-11T16:43:20.019400Z

(ns speculative.core
  (:refer-clojure :exclude [seqable? reduceable?])
  (:require [clojure.spec.alpha :as s]
            [clojure.spec.gen.alpha :as gen]
            #?(:cljs [goog.string])))

#?(:cljs
   (if (and *clojurescript-version*
            (pos? (goog.string/compareVersions "1.10.439" *clojurescript-version*)))
     (defn seqable? [v]
       (or (nil? v)
           (clojure.core/seqable? v)))
     (def seqable? clojure.core/seqable?))
   :clj (def seqable? clojure.core/seqable?))

mfikes 2018-11-11T16:43:25.019700Z

Yeah

borkdude 2018-11-11T16:43:26.019800Z

pasted to soon

mfikes 2018-11-11T16:44:11.020400Z

And, if someone depends on and older Git Dep, perhaps all bets are off

borkdude 2018-11-11T16:44:31.020900Z

git dep?

mfikes 2018-11-11T16:44:39.021100Z

(You'd then be forced to check seqable? nil at runtime to detect the behavior).

mfikes 2018-11-11T16:45:41.022600Z

Yes, if you have a deps.edn based project, you can specify any hash of ClojureScript to use, as a Git Dep, so you could be getting the older seqable? or the newer one, and in both cases *clojurescript-version* is nil. Perhaps that's too much to worry about though.

borkdude 2018-11-11T16:45:54.023Z

if they are doing that, they can do a (set! *clojurescript-version* "1.0.0") I guess, maybe with goog-define

mfikes 2018-11-11T16:46:33.023500Z

Yeah, it is just a general problem. If you have version-dependent code, and there is no version, you are a bit stuck.

borkdude 2018-11-11T16:46:52.023900Z

right. we assume it is there, if not, we assume >= 439.

mfikes 2018-11-11T16:47:02.024200Z

Seems reasonable.

borkdude 2018-11-11T16:47:14.024600Z

git deps are a relative new feature anyway. people who are into this must be willing to upgrade 😉

mfikes 2018-11-11T16:52:04.026100Z

Interestingly, if *clojurescript-version* were not simply a def but instead a goog-define, you could use identical? in places where you'd like DCE to occur. But, that would seem to be intractable because you would end up with a gigantic cond listing every single version you want to be conditional on.

borkdude 2018-11-11T16:52:47.026800Z

well, this is only some code at the top of the namespace, not something you pay for each time you run something

mfikes 2018-11-11T16:52:53.027Z

I guess your :else branch could fall back to runtime version checking.

borkdude 2018-11-11T16:53:14.027300Z

runtime version checking?

mfikes 2018-11-11T16:53:21.027500Z

Right... was just thinkng about the general problem.

borkdude 2018-11-11T16:53:51.028500Z

it would be even greater if reader conditionals supported clojure versions 😉

borkdude 2018-11-11T16:54:12.029100Z

or use a C preprocessor 😉

mfikes 2018-11-11T16:54:42.029700Z

Oh, by that I meant....

(cond
  (identical? *clojurescript-version* "1.10.339") :alpha
  (identical? *clojurescript-version* "1.10.439") :beta
  :else (sometthing with (goog.string/compareVersions *clojurescript-version* "1.10.439"))

mfikes 2018-11-11T16:55:27.030700Z

With code like the above, I would expect it to compile down to the keyword constructor for :beta in ClojureScript 1.10.439

mfikes 2018-11-11T16:55:50.031300Z

But, *clojurescript-version* would have to be a goog-define instead of just a lowly def

borkdude 2018-11-11T16:56:28.031800Z

JIRA? 🙂

mfikes 2018-11-11T16:56:52.032200Z

Yeah, if you'd like the feature, it might be worth seeing if David is opposed to it.

borkdude 2018-11-11T16:57:18.032700Z

Well, I don’t think it buys us much performance, this is only a one time check during the loading of the namespace, probably not noticeable

mfikes 2018-11-11T16:57:42.033500Z

Right, and generally we don't find ourselves writing conditional code based on the version of the compiler in use

borkdude 2018-11-11T16:58:07.034Z

I guess that could have been used in secretary as well. iff the goog-define had been there from the start

mfikes 2018-11-11T16:58:48.034300Z

Yeah, we don't have a time machine

borkdude 2018-11-11T16:59:49.035500Z

I guess we could still make the optimization in secretary. (defn map-entry [k v] (if 439 (MapEntry. k v nil) (first {k v])))

mfikes 2018-11-11T17:00:41.036300Z

Yeah, I actually wrote it that way the first time, even using interop to avoid an analysis warning when it compiles the then branch on older versions

mfikes 2018-11-11T17:02:04.036500Z

Something like (fn [k v] ((.. js/cljs -core -->MapEntry) k v nil))

borkdude 2018-11-11T17:02:38.036700Z

hah, right

mfikes 2018-11-11T17:03:29.037100Z

It compiles to the same JavaScript as (fn [k v] (->MapEntry k v nil)), but without involving analysis warnings 🙂

mfikes 2018-11-11T17:05:03.038100Z

I guess a rat hole on that kind of stuff is whether you can eliminate the call that gets generated (you'd want to actually call the MapEntry ctor in a static way if you wanted it to really fly

borkdude 2018-11-11T17:06:16.038600Z

would there have been anything against adding a -key and -val prop to vectors so it didn’t break anything?

mfikes 2018-11-11T17:06:49.038900Z

I don't recall that concept being debated at the time

borkdude 2018-11-11T17:11:31.039300Z

well, right now we better not, since it would complicate the specs between clj and cljs more 😉

mfikes 2018-11-11T17:11:56.039700Z

Yeah, if we can get away with it, it is better to align with Clojure and not look back

borkdude 2018-11-11T22:14:49.040600Z

I think one problem with secretary running on newer version of clojure is that the macro produces bad code for paths that are actually not taken. E.g.:

user=>   (s/defroute #"/([a-z]+)/search" [letters {:keys [query-params]}]
  #_=>       [letters query-params])

Syntax error macroexpanding clojure.core/let at (form-init2041262501447576588.clj:1:1).
{:keys [query-params]} - failed: ident? at: [:bindings :form :map-destructure :keys] spec: :clojure.core.specs.alpha/keys
{:keys [letters {:keys [query-params]}]} - failed: simple-symbol? at: [:bindings :form :local-symbol] spec: :clojure.core.specs.alpha/local-name
{:keys [letters {:keys [query-params]}]} - failed: vector? at: [:bindings :form :seq-destructure] spec: :clojure.core.specs.alpha/seq-binding-form

borkdude 2018-11-11T22:15:07.040800Z

user=> (macroexpand ' (s/defroute #"/([a-z]+)/search" [letters {:keys [query-params]}]
  #_=>   #_=>       [letters query-params]))
(let* [action__11080__auto__ (clojure.core/fn [params__11081__auto__] (clojure.core/cond (clojure.core/map? params__11081__auto__) (do (clojure.core/let [{:keys [letters {:keys [query-params]}]} params__11081__auto__] [letters query-params])) (clojure.core/vector? params__11081__auto__) (clojure.core/let [[letters {:keys [query-params]}] params__11081__auto__] [letters query-params])))] (secretary.core/add-route! #"/([a-z]+)/search" action__11080__auto__) (fn [& args__11079__auto__] (clojure.core/apply secretary.core/render-route* #"/([a-z]+)/search" args__11079__auto__)))
user=> (let [{:keys [letters {:keys [query-param]}]} {:letters {:query-param 1}}] [letters query-param])

borkdude 2018-11-11T22:15:53.041500Z

it’s making something like this: (let [{:keys [letters {:keys [query-param]}]} {:letters {:query-param 1}}] [letters query-param]) which was fine on clojure 1.6.0, but in clojure 1.9.0 it’s rejected

mfikes 2018-11-11T22:25:40.041900Z

A minimal repro of that kind of problem: (let [{:keys [{}]} {}])

borkdude 2018-11-11T22:27:16.042200Z

yep

borkdude 2018-11-11T22:28:11.042700Z

I’m not sure how it works, because including it in a project with 439 does “just work”

borkdude 2018-11-11T22:28:38.043400Z

or maybe it just fails with that one example. yeah, I think that’s it, because I have one specific test failing when I upgrade things

mfikes 2018-11-11T22:29:15.044100Z

Maybe it is a bad test, with {:keys [query-params]} on the right? (I don't know what defroute would want.)

borkdude 2018-11-11T22:30:50.044600Z

yeah, maybe the test is incorrect.

mfikes 2018-11-11T22:32:09.045100Z

This subsequent test passes interestingly:

(is (= (s/dispatch! "/abc/search?flavor=pineapple&walnuts=true")
           ["abc" {:flavor "pineapple" :walnuts "true"}])))

borkdude 2018-11-11T22:40:38.046500Z

I never got that far. When I run lein run-tests when commenting out the failing test, I get:

$ lein run-tests
Compiling ClojureScript...
Compiling ["target/js/test.js"] from ["src/" "test/"]...
Can't open 'Successfully compiled ["target/js/test.js"] in 9.499 seconds.'
Successfully compiled ["target/js/test.js"] in 9.499 seconds.

borkdude 2018-11-11T22:59:19.047400Z

better luck with tools.deps / cljs.main

borkdude 2018-11-11T23:10:35.047800Z

if I define the test as:

(s/defroute #"/([a-z]+)/search" {:as params}
      (let [[letters {:keys [query-params]}] params]
        [letters query-params]))
it works

mfikes 2018-11-11T23:20:10.048800Z

And code just like that test is in the documentation here (2nd example down) https://github.com/clj-commons/secretary#query-parameters

mfikes 2018-11-11T23:20:57.049400Z

I guess there should be a secretary channel to discuss this kind of stuff 🙂