Fork me on GitHub
#beginners
<
2023-01-01
>
Duncan Marsh10:01:32

is it possible to use a string variable as a doc string, or to share a doc string between to functions? I have two fns with the same input but one creates all of the data needed for a side effect and the ohter actually preforms the side effect.

lispyclouds11:01:14

The spec for defn looks for a literal string as the argument for the docstring and having a symbol there would fail it. This could be gotten around with a macro i think. But from a user perspective having the same doc string pretty much means having the same functionality and it sounds like the one doing the side effect is an implementation detail which can be an internal fn. if no one will call the side effecting fn directly, id not worry too much about the same docstring and either not have a docstring for it or just simply say it does the sideeffect for the public fn

❤️ 2
phill11:01:27

defn attaches the docstring as metadata. You should be able to do that yourself (see ^{:doc "..."} on https://clojure.org/reference/metadata) in which case you can compose it from a variable or by calling functions.

2
❤️ 2
skylize13:01:34

(defn foo "docstring for foo" []) (defn ^{:doc (:doc (meta #'foo))} bar []) (meta #'bar) ; => {:doc "docstring for foo", :arglists ([]), ... }

❤️ 2
skylize13:01:00

Though I agree with @U7ERLH6JX regarding the usage you describe.

Duncan Marsh04:01:20

thanks for the advice yall, huge help!

sarna15:01:38

I've been trying to write clojure for some time now, but I'm still struggling with data modeling/representation. for example, I now have two distinct things, each of them has a path. in an OO language I'd have a Foo and Bar class, each with path as an instance variable. in clojure all I have is two maps that look like this: {:path "/some/path"}. how do you distinguish between these? to me it feels silly to put foo/bar in every function/variable name, and it also feels silly to add a key like :type. how does your head not explode when you have to distinguish between similarly-looking maps?

simongray15:01:31

If you have completely distinct, fixed types represented by key-value pairs you can use a Record instead of a map. But ask yourself why it is important to distinguish? Why not just operate on any map with a :path? And I don't think a :type key is silly either. If that value is significant why would it be silly to include it? You're just being explicit about the data you need.

sarna16:01:56

I need it either to be mutable, or have not fixed fields. I could also set nil initially for stuff I'll assoc later in theory :thinking_face: it's not important for the project to distinguish them, but as it is now I find reading the code very challenging sometimes

sarna16:01:36

IMO including :type would be silly because I wouldn't ever actually use it, it'd be just a readability aid

dpsutton16:01:01

can you get a bit more concrete? What are your actual Foo and Bar concepts here?

sarna16:01:08

they're a "workspace" and a "database" (of files), they have distinct paths and different functions work on them

dpsutton16:01:02

and each has different associated information? like other keys in the map?

sarna16:01:59

yeah. but they both have paths and it trips me up :(

sarna16:01:39

ah sorry, one important missing piece: the minimal valid database as well as a workspace is just {:path "some-path"}

dpsutton16:01:50

two options. I’d probably throw a :type keyword on each. If you don’t want to do that, you could use {:workspace "some-path"} and {:database "some-path"} instead of the generic :path keyword

sarna16:01:25

I like the second solution! thank you :)

pavlosmelissinos17:01:23

Or you can use qualified (namespaced) keywords: :workspace/path, :database/path That's by far my favorite approach

seancorfield18:01:25

☝️:skin-tone-2: I was just coming into this thread to suggest qualified keywords if the paths actually have different semantics and you need to treat them differently. Qualified keys provide contextual uniqueness and are very idiomatic in Clojure, esp. if you're also using Spec.

2
sarna18:01:57

this sounds great! but actually I've never used qualified keywords before. is there any guide/blog post that'd explain when to use them? for example, should all keys in my maps be qualified or only path? I found this https://ask.clojure.org/index.php/10380/when-to-use-simple-qualified-keywords but it doesn't answer all my questions

seancorfield18:01:41

@U01UDS4A1K8 If you have a hash map of workspace stuff and a hash map of database stuff, it might make sense to qualify all keys in both. It depends on how distinct the semantics of those key names need to be. This is really about naming and design, so each situation is likely to be different.

sarna19:01:06

ah, I was afraid of "it depends" ;) oh well

pavlosmelissinos20:01:50

My two cents: In contrast to OOP languages, Clojure allows you to mix and match the contents of various "payloads" in your codebase (because it's just data). This, along with the lack of types and a preference for flat representations makes qualified keywords very important in many situations. In the end of the day, it's a question of uniqueness. If your code stops working when you replace a database/workspace with the result of merging them, you should qualify the keywords that cause the conflict.

sarna20:01:14

it's quite mind-bending, and my mind still is refusing to fully switch to this "it's just data!" model :) I understand it's just data when I get it from the outside (like, some JSON) but sometimes I feel I'd still need.. something more like an object

pavlosmelissinos20:01:19

Why would you need that though? I mean what difference does it make?

pavlosmelissinos20:01:48

I mean what would be the benefit of an object

sarna20:01:46

it's easier to wrap my head around it. typically everything's in one place, you know where to search for stuff, the object has some responsibilities etc. when I have maps it's just.. data. and functions can be whatever, in any place

sarna20:01:06

could be the force of habit :)

pavlosmelissinos20:01:38

Yeah, I think it is, to be honest 🙂

pavlosmelissinos21:01:41

The clojure model is very simple. Functions are responsible for handling all the logic. You don't have logic in the data. It's kinda like duck typing (but more flexible) Pass your data to a function and you either get a result, nil or an exception If your data is non-trivial and you need some extra safety, you can use clojure.spec, malli or others to define some constraints that you can then easily verify at runtime

seancorfield21:01:57

"locality" of code is a matter of organization of functions in "obvious" namespaces. I'd recommend picking a standard top-level prefix for namespaces (at work we use ws. for World Singles code) so you can tell it apart from library namespaces, and then have a fairly flat, broad set of namespaces where your business logic is in namespaces that match your business domain. So you might have sarna.domain.workspace and sarna.domain.database for pure business logic and maybe sarna.store.* for persistence and sarna.view.* for rendering templates -- in an HTML app, or sarna.api.* for exposing a REST API or something.

seancorfield21:01:00

We don't need to encapsulate data because it's all immutable, so there's no reason for functions to be colocated with their data because they don't have special "privilege" to access the private (internal) view of that data. No one is going to mutate your data out from under you.

seancorfield21:01:18

When you start off, it's also perfectly fine to have everything in one namespace and then refactor out to more namespaces as the names begin to become more obvious.

seancorfield21:01:47

It's important to remember that, although Clojure namespaces just happen to map to Java packages under the hood, there is no assumed hierarchy in Clojure -- there's no concept of "package access" for example -- so the names of namespaces are purely descriptive and you can organize them however you find most intuitive. In some ways, this freedom is what can be so hard for long-time OO folks to come to terms with.

seancorfield21:01:55

(I think this is one of the things I've come to really like about #C013B7MQHJQ: it "forces" you to have a broad, flat structure with well-thought out names for your namespaces)

sarna10:01:15

this is very very helpful, thanks a ton 🙏

djanus15:01:33

I think it’ll be interesting to people in this channel: I’m offering an individual Clojure workshop for free Check out the https://clojurians.slack.com/archives/C03RZRRMP/p1672587766177759 in #events for more information!

Hans Lux19:01:12

Hello djanus, I just read your thread and realise I am too late to apply for your workshop. If no one has taken your offer though, I would really like to take that opportunity to learn some clojure from you. I started with clojure about a year ago, mainly reading and learning the basics. Now I startet the obligatory todo app and am running in to some issues. I am an experienced OO programmer (20+ years) and am really interested in making some steps forward in clojure. So if that workshop is still available, I'd be happy to be your student 🙂

Robin Singh16:01:26

This function reverses a list. But here I'm depending upon the fact that a new item is appended at the first place in a list. how can i do it by selecting all elements from the original list except the last?

(defn rev
  ([es] (rev es (empty '())))
  ([es new-lis]
   (if (empty? es)
     new-lis
     (recur (rest es) (conj new-lis (first es))))))

dpsutton16:01:47

there are two functions last and butlast that you could use. Note these are terrible functions to use on lists. Lists have quick access to the first element and the rest of the elements. Everything else is a traversal. So the way you are doing it here is the better way to accomplish this.

dpsutton16:01:29

> But here I’m depending upon the fact that a new item is appended at the first place in a list This is a fine thing to depend upon. Lists will always behave like this

Robin Singh16:01:18

Oh okay, thanks if its good to depend on that.

dpsutton16:01:50

So here’s a version that uses the (atrocious) last and butlast functions. I’m calling it rev*:

(defn rev* [l]
  (loop [acc [] l l]
    (if (next l)
      (recur (conj acc (last l)) (butlast l))
      (seq acc))))
And then some simple timing comparisons:
server=> (time (first (rev* (range 10000))))
"Elapsed time: 3898.901542 msecs"
9999
server=> (time (first (rev (range 10000))))
"Elapsed time: 6.107166 msecs"
9999
The last and butlast approach completes in just under 4 seconds. The first and rest version takes 6/1000 seconds

Robin Singh05:01:33

Thats really helpful it's huge.