yamlscript 2024-05-19

I just remembered a cool YS feature that I was reminded of watching an old @alexmiller video on youtube. Alex shows:

(defn exp [x n]
  (apply * (repeat n x)))
In YS you can get apply semantics by splatting (`*` after symbol) any argument:
defn exp(x n):
  v =: repeat(n x)
  .*: v*
compiles to:
$ ys -e '
defn exp(x n):
  v =: repeat(n x)
  .*: v*
' -c
(defn exp [x n] (let [v (repeat n x)] (apply * v)))
and runs as:
ys -e '
defn exp(x n):
  v =: repeat(n x)
  .*: v*
' -e 'exp: 10 3' -p
1000
but wait... there's more!

@phill I read https://stuartsierra.com/2015/04/26/clojure-donts-concat and the linked https://groups.google.com/g/clojure-dev/c/ewBuyloeiFs Could you or someone write a version of concat using that solution? The page links to https://gist.github.com/jondistad/2a4971fe8948ca2f6ba0 but that is now 404. I can't seem to load http://archive.org from where I am...

Unless the lazy aspect is necessary, you can (reduce into [] seq1 seq2 ...)

The lazy aspect is not necessary. I'll give it a try. Cheers.

You might also be able to help me with a related irritation.

Give me a sec to review it

$ ys -e 'for [i (1 .. 5)]: say(i)'
$ ys -e 'each [i (1 .. 5)]: say(i)'
1
2
3
4
5
$
for is lazy so I made up ys::std/each to make it non-lazy. See: https://github.com/yaml/yamlscript/blob/main/core/src/ys/std.clj#L174-L177 What I really want to do is replace clojure::core/for with a ys::std/for that does what each does. But I can't seem to do it. Maybe because clojure::core/for is a macro and I don't know how to make a macro that calls a macro. Dunno why... BTW I've replaced a handful of clojure.core things with ys.std things but I have a ys.clj library so that one can always use clj/foo if they need the real foo https://github.com/yaml/yamlscript/blob/main/core/src/ys/clj.clj

I wonder if you can help me turn ys.std/each into ys.std/for

yes, but I really want it to be called for in YS 😕

I've been trying again. Will post my findings soon

Hey! It works now with for calling doseq instead of clojure.core/for

+ git diff
diff --git a/core/src/ys/std.clj b/core/src/ys/std.clj
index 27675ca..2893ab9 100644
--- a/core/src/ys/std.clj
+++ b/core/src/ys/std.clj
@@ -15,7 +15,8 @@
    [ys.ys :as ys]
    [yamlscript.common :as common]
    [yamlscript.util :as util])
-  (:refer-clojure :exclude [num
+  (:refer-clojure :exclude [for
+                            num
                             print
                             when]))
 
@@ -171,11 +172,16 @@
 (defn dirname [& args]
   (apply util/dirname args))
 
-(defmacro each [bindings & body]
+(defmacro for [bindings & body]
   `(do
-     (doall (for [~@bindings] (do ~@body)))
+     (doall (clojure.core/doseq [~@bindings] (do ~@body)))
      nil))
 
+(comment
+  (macroexpand-1 '(for [x (range 10)] (println x)))
+  ;; (do (clojure.core/doall (clojure.core/doseq [x (range 10)] (do (println x)))) nil)
+  )
+
 (defn err [& xs]
   (binding [*out* *err*]
     (apply clojure.core/print xs)

When I replaced doseq above with for I got a stack overflow.

No idea why I didn't try doseq this way before... Thanks a lot!!

But now when I try to add

(intern 'ys.clj 'for       clojure.core/for)
to clj.clj I get
Can't take value of a macro: #'clojure.core/for

Weird, I tried:

(intern 'ys.clj 'for       (clojure.core/resolve 'clojure.core/for))
and that compiles for I get a runtime error:
Error: Could not resolve symbol: i

Can you not intern a macro?

ok now I get:

$ YS -e 'clj/for [i (1 .. 5)]: println(i)' -c
(clj/for [i (rng 1 5)] (println i))
$
looks good
$ YS -e 'clj/for [i (1 .. 5)]: println(i)'
$
looks good. lazy, no output
$ YS -e 'clj/for [i (1 .. 5)]: println(i)' -p
Error: Method nth on class clojure.lang.LongRange$LongChunk not allowed!
wtf? (`-p` prints evaluation value)

that's with

+(intern 'ys.clj
+  (clojure.core/with-meta 'for {:macro true})
+  @#'clojure.core/for)

diff is now

diff --git a/core/src/yamlscript/runtime.clj b/core/src/yamlscript/runtime.clj
index 52076ba..6d905ce 100644
--- a/core/src/yamlscript/runtime.clj
+++ b/core/src/yamlscript/runtime.clj
@@ -122,7 +122,9 @@
      :classes (classes-map
                 '[clojure.lang.Atom
                   clojure.lang.Fn
+                  clojure.lang.IChunk
                   clojure.lang.Keyword
+                  clojure.lang.LongRange
                   clojure.lang.Range
                   clojure.lang.Seqable
                   clojure.lang.Sequential
diff --git a/core/src/ys/clj.clj b/core/src/ys/clj.clj
index 854f763..703e72a 100644
--- a/core/src/ys/clj.clj
+++ b/core/src/ys/clj.clj
@@ -9,6 +9,9 @@
   (:refer-clojure :only [intern]))
 
 (intern 'ys.clj 'compile   clojure.core/compile)
+(intern 'ys.clj
+  (clojure.core/with-meta 'for {:macro true})
+  @#'clojure.core/for)
 (intern 'ys.clj 'load      clojure.core/load)
 (intern 'ys.clj 'load-file clojure.core/load-file)
 (intern 'ys.clj 'num       clojure.core/num)
diff --git a/core/src/ys/std.clj b/core/src/ys/std.clj
index 27675ca..67a761e 100644
--- a/core/src/ys/std.clj
+++ b/core/src/ys/std.clj
@@ -15,7 +15,8 @@
    [ys.ys :as ys]
    [yamlscript.common :as common]
    [yamlscript.util :as util])
-  (:refer-clojure :exclude [num
+  (:refer-clojure :exclude [for
+                            num
                             print
                             when]))
 
@@ -171,10 +172,9 @@
 (defn dirname [& args]
   (apply util/dirname args))
 
-(defmacro each [bindings & body]
-  `(do
-     (doall (for [~@bindings] (do ~@body)))
-     nil))
+(intern 'ys.std (with-meta 'each {:macro true}) @#'clojure.core/doseq)
+
+(intern 'ys.std (with-meta 'for {:macro true}) @#'clojure.core/doseq)
 
 (defn err [& xs]
   (binding [*out* *err*]

getting:

$ YS -e 'for [i (range 1 6)]: println(i)' -p
1
2
3
4
5
nil
$ YS -e 'each [i (range 1 6)]: println(i)' -p
1
2
3
4
5
nil
$ YS -e 'clj/for [i (range 1 6)]: println(i)' -p
Error: Method nth on class clojure.lang.LongRange$LongChunk not allowed!

I'll ask in #sci

Well.. Clojure's for is amazingly useful for Generating Stuff (well, generating sequences) from combinations... it comes up all the time in shell scripts and I would expect maybe yaml too... (for [environment ["test" "staging" "north"] service ["ingress" "egress" "digress" "congress"] :when (oblate? environment service)] (str environment "-" service ".log")) and so forth. It would be a shame to use the word to mean something else and then later reinvent it under a third name. True, 'why does my for loop not work [for side effects]?' is a FAQ around here. But I look at it this way... people are bound to hit their head on something on their way in the door. Might as well hit it on for and get it over with. The word Clojure puts around imperatives is (do ...) - it's sometimes silent, as inside defn or when - so doseq is salubrious.

I see your point. I actually was trying to make my for be a non-lazy doseq that returned its evaluation like for does but I was unable to.

Unlike apply you you don't need the splat in the last arg position, and you can also use multiple splats!

$ ys -e '
defn exp(x n):
  v =: repeat(n x)
  .*: 4 v* 2 v*
' -c
(defn exp [x n] (let [v (repeat n x)] (apply * 4 (concat v [2] v))))
runs:
$ ys -e '
defn exp(x n):
  v =: repeat(n x)
  .*: 4 v* 2 v*
' -e 'exp: 10 3' -p
8000000

Reminder the . in .* is ignored. Without it it would be invalid YAML (thus invalid YS)

There's one problem I uncovered though... This doesn't compile/work as expected:

$ ys -e '
defn exp(x n):
  .*: repeat(n x)*
' -c
(defn exp [x n] (* (repeat n x) *))
ie The splat * must currently follow a symbol. 😞

But I expect it will work in the next YS release!!! 🙂

In conclusion I think splatting is up there with a =: b lets in keeping YS code very clean; esp given that let and apply are so constantly used in Clojure coding.

Example:

$ ys -e '
defn foo(xs):
  y =: bar(xs*)
  inc: y
  z =: baz(xs* xs*)
  if (y > z): y z
' -c | zprint '{:width 30}'
(defn foo
  [xs]
  (let [y (apply bar xs)]
    (inc y)
    (let [z (apply baz
              (concat xs xs))]
      (if (> y z) y z))))

Nifty! But (slightly off-topic) beware of compiling to concat! https://stuartsierra.com/2015/04/26/clojure-donts-concat

Good to know. Thanks!

Nobody has ever reviewed my Clojure so if you're feeling ambitious 😜

Btw I only use concat when the splat is not in the final position

@danielmartincraig continuing a bit on that "lesson" from the other day where these were equivalent...

a: b c d
a b: c d
this is the "normal" case but not the rule. Consider cond... It begs to be written:
cond:
  a > 5: b
  a > 10: c
  :else: d

I forgot to mention:

=>: a b c d
a: b c d
a key of => is ignored. This is quite important as you'll often need to write a key pair but only be concerned with a single value.
$ cat a.ys 
!yamlscript/v0

say:
  case ENV.USER %:
    ."alice": 1
    ."bob": 2
    ."ingy": 3
    =>: 999
or:
$ cat b.ys 
!yamlscript/v0

defn add(a b):
  c =: a + b
  if (c = 0):
    die: 'Zero is evil'
  =>: c

Interesting CLI point. You can use b.ys above by adding a -e expression after it:

$ ys b.ys -e 'add: 3 3' -p
6
$ ys b.ys -e 'add: 3 -3' -p
Error: Zero is evil

Let's see it compiled:

$ ys -ce '
cond:
  a > 5: b
  a > 10: c
  :else: d
'
(cond (> a 5) b (> a 10) c :else d)

compare to:

$ ys -ce '
foo:
  a > 5: b
  a > 10: c
  :else: d
'
(foo ((> a 5) b) ((> a 10) c) (:else d))

compare to:

$ ys -ce '
foo %:
  a > 5: b
  a > 10: c
  :else: d
'
(foo (> a 5) b (> a 10) c :else d)

Note the %: . It gives you cond semantics

but I special case cond so you don't have to

It's part of a work in progress for YS to have a macro like system where anyone can define how a YAML structure/content gets turned into a specific clojure AST

for now some things are hard coded but not for long

... ;; end of mini lesson for today

I swear I'll write those docs soon

🚀 2

Thanks much appreciated!