Fork me on GitHub
#clojure
<
2023-12-13
>
Ingy döt Net00:12:42

I just got YAMLScript let and lambda (assuming #(...) is called lambda in Clojure) syntax implemented. I think looks really nice. I've been writing my https://yamlscript.org/posts/advent-2023/index/ and I was going to do tomorrow's post on Let-Over-Lambda in YS. I came up with this example:

$ cat sample/lol.ys 
!yamlscript/v0

defn lol(x):
  y =: 2 * x
  =>: \(% + y)

say:
  lol(10): 4
$ ys --compile sample/lol.ys 
(defn lol [x] (let [y (_* 2 x)] (fn [_1] (+ _1 y))))
(say ((lol 10) 4))
$ ys sample/lol.ys 
24
but my friend said that wasn't let-over-lambda. (I still think it is). So my question is, what do people consider to be let-over-lambda in Clojure?

Ingy döt Net00:12:08

In case you were like wtf is yamlscript, it's a yaml based programming language (written in clojure) that compiles to clojure code and is eval'd using sci. The ys cli is created by having the yamlscript compiler and sci and libraries compiled with graalvm native-image. It also builds a shared library for ffi binding to other langs like say python where it can be used as a normal or highly-dynamic-as-clojure yaml loader. My question here is about the clojure code genned:

$ ys --compile sample/lol.ys 
(defn lol [x] (let [y (_* 2 x)] (fn [_1] (+ _1 y))))
(say ((lol 10) 4))
and whether people would consider that an example of let-over-lambda in clojure or not; and if not, why not...

hifumi12303:12:14

“let over lambda” refers to a specific design pattern for holding state, so your friend is right

Ingy döt Net03:12:35

well what does that look in clojure

Ingy döt Net03:12:53

he said it would look like (let [x 1] (defn a [] x)) which doesn't make sense. at least not without context

hifumi12303:12:27

it helps to use a lisp that supports mutation, but in clojure you can emulate it with atoms like so

(defn make-account [initial-balance]
  (let [balance (atom initial-balance)]
    (fn withdraw [amount]
      (if (< @balance amount)
        :insufficient-funds
        (reset! balance (- @balance amount))))))

(def withdraw (make-account 100))

(withdraw 5)   ;=> 95
(withdraw 5)   ;=> 90
(withdraw 91)  ;=> :insufficient-funds
(withdraw 5)   ;=> 85

Ingy döt Net03:12:37

also this works in ys:

$ cat sample/lol2.ys 
!yamlscript/v0

defn lol(x):
  y =: 2 * x
  defn f(z): z + y

say:
  lol(10): 4
$ ys --compile sample/lol2.ys 
(defn lol [x] (let [y (_* 2 x)] (defn f [z] (_+ z y))))
(say ((lol 10) 4))
$ ys sample/lol2.ys 
24
so is that let over lambda

hifumi12303:12:01

“let over lambda” is more of a design pattern if anything — its a way to create functions that behave like objects, in the sense that the functions hold state

Ingy döt Net03:12:38

my understanding was that lol is let returning a closure around one or more let vars

hifumi12303:12:10

that is how they achieve holding state, but again, the point is doing this to hold state

hifumi12303:12:29

i.e. what makes this design pattern interesting is the fact you can use it to create stateful objects

hifumi12303:12:59

for more inspiration you can look at chapter 3 of SICP, which covers the exact same thing as “let-over-lambda”, but doesnt use a cool sounding name like that

hifumi12303:12:22

we also use this pattern in clojurescript libraries like reagent, where UI components are functions returning HTML as data, and state is introduced by using this exact same pattern:

(defn my-component []
  (let [count (r/atom 0)]
    (fn []
      [:div
       [:p "You clicked " @count " times"]
       [:button {:on-click #(swap! count inc)} "Click me"]]))

oyakushev07:12:12

"State" here implies mutable state that changes from one execution of the function to another, not an effective constant that gets lexically closed over.

Ingy döt Net08:12:30

Yeah I'm starting to see that. I was reading part of https://letoverlambda.com/ and it said > Let Over Lambda > Let over lambda is a nickname given to a lexical closure. Let over lambda more closely mirrors the lisp code used to create closures than does most terminology. In a let over lambda scenario, the last form returned by a let statement is a lambda expression. It literally looks like let is sitting on top of lambda: >

* (let ((x 0))
>     (lambda () x))
So I was like, oh it's just a closure and let makes the new lexical to close over. But them I saw a bit later: >
(let ((counter 0))
>   (lambda () (incf counter)))
> This closure will return 1 the first time it is called, 2 the subsequent time, and so on. One way of thinking about closures is that they are functions with state. These functions are not mathematical functions, but rather procedures, each with a little memory of its own. Sometimes data structures that bundle together code and data are called objects. An object is a collection of procedures and some associated state. Since objects are so closely related to closures, they can often be thought of as one and the same. Which jives more with what you (@U06PNK4HG) and @U0479UCF48H are saying.

hifumi12308:12:29

my original example was gonna be in common lisp precisely because its easier to mutate there compared to clojure 🙂 ended up using clojure anyway since you asked how let-over-lambda would look in practice in clojure

Ingy döt Net08:12:48

let-made-atoms seem to work out perfectly here though

👍 1
Ingy döt Net18:12:37

I think I got LoL right in YS now:

$ cat sample/lol.ys 
!yamlscript/v0

do:
  count =: atom(0)
  counter =: \(swap! count inc)

  say: counter()
  say: counter()
  say: counter()
$ ys sample/lol.ys -c
(do
 (let
  [count (atom 0) counter (fn [] (swap! count inc))]
  (say (counter))
  (say (counter))
  (say (counter))))
$ ys sample/lol.ys 
1
2
3
agree or disagree?

Ingy döt Net18:12:48

(the top level do was necessary here because a =: b gens (def a b) at the top level and (let [a b] ...) when not at top; and LoL examples really should have a let)

hifumi12320:12:38

Seems right to me

pauld14:12:40

Is anyone familiar with maven and/or clojure-maven-plugin? I want to add source outside of the $basedir but that doesn't appear to be possible with this plugin. I have a dev project that references sources in sister project folders.

pauld14:12:45

I'm already doing this in java using the build-helper-maven-plugin which allows for relative paths, but now I want to add clojure source paths.

pauld14:12:14

Another solution would be to alter $basedir, but that doesn't seem possible.

mx200019:12:37

With lein codox all namespaces are listed together on the html page. Is there a way to hide namespace packages and expand only as clicked?

seancorfield20:12:30

Maybe you could do that via HTML transformations? https://github.com/weavejester/codox#html-transformations

👍 1
nonrecursive22:12:23

does anyone have experience using the Athena JDBC driver in a clojure project? I'm having a hard time even just including it in my project, it appears the drivers aren't published in maven?

p-himik22:12:14

You can download the jar and refer to it from your deps.edn.

dpsutton22:12:54

we host them in an s3 bucket

nonrecursive22:12:38

oh my god, you are a life saver. thank you!

nonrecursive22:12:52

this project isn't using deps.edn which is a whole other thing

dpsutton22:12:09

yeah. just adding a jar to the classpath is super annoying in lein

DrLjótsson05:12:15

@U0ETXRFEW magically turned my lein project into a lein project with dependencies managed by deps.edn, but otherwise configured by lein.

roboli22:12:53

Hi again. I'd like to have CLI for my library. Coming from a NodeJS background, I usually run npx command or install packages with -g and have them in my path. For the Clojure world, building a lein plugin would be the best way to go?

p-himik22:12:18

Depends on what tool you want to target. For Lein - yes, probably a plugin. For tools.deps it would be a tool, similar to what https://github.com/seancorfield/deps-new does.

p-himik22:12:55

Another option is to create a stand-alone executable, similar to https://github.com/babashka/neil. You'd have infinite flexibility but you'd have to manage your own installation channels.

Ed22:12:22

There's also this kind of hackery that will let you write a script that runs via the shebang line: https://gist.github.com/ericnormand/6bb4562c4bc578ef223182e3bb1e72c5

Bobbi Towers23:12:22

You can make an npx command using #C029PTWD3HR, fwiw

👀 1
seancorfield23:12:51

If you want to run code that is in the current project, then clojure -M -m some.namespace runs some.namespace/-main -- and you can have -main in as many namespaces as you want; and clojure -X some.namespace/my-fn will run that function, passing a single argument: a hash map of key/value pairs read from the command-line.

seancorfield23:12:20

Aliases in deps.edn would let you shrink those to something like clojure -M:my-cli or clojure -X:my-exec depending on whether you used :main-opts or :exec-fn.

seancorfield23:12:26

If you want to run code from a library, you don't need to "install" it -- you can either specify its "coordinates" on the command-line or hide it behind an alias in your deps.edn file: clojure -Sdeps '{:deps {my-new/library {:mvn/version "RELEASE"}}}' -M -m that.library (or -X that.library/function). But putting aliases into your user deps.edn is a pretty common what to deal with this. This is my user deps.edn file: https://github.com/seancorfield/dot-clojure/blob/develop/deps.edn

roboli01:12:49

Thanks all