Hello James,

Thanks for your answer. I do understand your point. Pure functions are
easier to reason about and my use of dynamic here breaks that purity. I'm
not doing it lightly. It already happened to me, that one of those
functions that was running inside the transaction, was not passed the
transaction connection and instead got the global one and the failure was
silent and very hard to debug, and this was with a project that has less
than 200 lines of code. I'm trying to find patterns that will work when
this project has 200k lines of code.

For me, the thing is, I have a traditional relational database here, this
is already far from pure. For example, calling (db/create-user "
pup...@pupeno.com") twice will not only not return the same thing the
second time, it'll actually raise an exception the second time. Also, the
database connection is *global state* unless each function creates its own
connection, which would be terrible. So, this global state also breaks
functional purity.

The problem with the second aspect of breaking purity as far as I can see
is this: at some point, this global state has to be picked up and used, so
at some point a function will *not* get a database connection passed to it
but *will* access the database by using this global connection. I haven't
advanced this project enough to say this with 100% certainty, but, I think
there's going to be more than one function like that and at some point I'll
need to have one inside the other so I need them to be composable. Let me
show you a naive example:

db/create-user is the low level database function that creates a record in
the user table
user/create is the function used to create a user, it takes care of, for
example, encrypting the password.
account/register is the function to register a new user, it takes care of
creating a user but also validation, sending a welcome email and so on.

So each function calls the predecessor there and would pass the database
connection, account/register, being the entry point, would grab it from the
global state so it doesn't get a connection passed to it. So far, a lot of
it looks like pure functions (let's ignore the fact that a database breaks
that purity). The problem arises when I get another function,
account/invite, that is used to register a bunch of people one after the
other, so that account/invite would call account/register many times. The
problem is that account/invite *can't* start a transaction and have
account/register and all its inner functions use that transaction when that
makes a lot of sense.

To make account/register composable it needs to accept an optional database
connection and use that one if it's present, or the global one if it's not.
Every time a function does that there's a high risk of picking the wrong
database and account/invite and account/register shouldn't be dealing with
database connection management. That feels to me like lower level details
leaked into higher level abstractions.

Now, I know this is a naive example and you could push the grabbing of the
global connection higher and higher, as long as the example is naive and
simple like this, but it does represent what in my experience is the
reality of web application development at least in another languages and I
haven't seen anything to make me think Clojure will be radically different
here (at least when using a patterns such as compojure).

So yes, it's not purely function but with a database that's already
impossible and if I wanted purely functional I would probably be using
Haskell instead of Clojure. What I like about Clojure is this:

Clojure is a practical language that recognizes the occasional need to
> maintain a persistent reference to a changing value and provides 4 distinct
> mechanisms for doing so in a controlled manner - Vars, Refs, Agents and
> Atoms.


I'm just trying to be practical here. But I'm new and I'm not sure if an
atom that is a dynamic var has some hidden issues that I'm not seeing
(other than the fact of it being state that changes and that I have to
manage explicitly because the language is not protecting me from shooting
myself in the foot with it).

Does it make sense?



On 31 July 2015 at 03:16, James Reeves <ja...@booleanknot.com> wrote:

> On 31 July 2015 at 01:44, J. Pablo Fernández <pup...@pupeno.com> wrote:
>>
>> I found passing around the database connection to each function that uses
>> it very error prone when you are using transactions as passing the wrong
>> one could mean a query runs outside the transaction when in the source code
>> it is inside the with-db-transaction function. So I ended up defining the
>> db namespace like this:
>>
>> (ns db)
>>
>> (defonce ^:dynamic conn (atom nil))
>>
>> (defn connect!
>>   (reset conn (generate-new-connection)))
>>
>> (defn run-query
>>   [query] (run-query query @conn)
>>   [query conn] (run-the-query-in-connection query conn))
>>
>
> This style of code is generally considered to be unidiomatic in Clojure.
> The reason for this is that it significantly increases complexity, and
> Clojure is about reducing complexity where possible.
>
> Consider a function like:
>
>   (defn find-by-id [conn id]
>     (sql/query conn ["SELECT * FROM foo WHERE id = ?" id]))
>
> The output of this function is affected by its arguments (and by the state
> of the database the connection is associated with), which is passed by its
> caller.
>
> Now consider a function like:
>
>   (defn find-by-id [id]
>     (sql/query @conn ["SELECT * FROM foo WHERE id = ?" id]))
>
> The output of this function is affected by its arguments... and by
> anything that touches the global conn var, which could literally be
> anything in your program, in any namespace, in any function, in any thread.
>
> The more ways in which a function has, the more "complex" it is. This is
> why Clojure prefers immutable data over mutable data, and why function
> arguments are generally preferred over dynamic vars.
>
> The problem of accidentally calling a database connection directly inside
> a transaction is a difficult one, but I don't think the solution is to add
> more complexity. An alternative solution would be to take the original
> database connection out of scope, by moving your transaction code to a
> separate function:
>
>    (defn do-things* [tx]
>      (do-foo tx)
>      (do bar tx)
>      (do baz tx))
>
>    (defn do-things [db-spec]
>      (sql/with-db-transaction [tx db-spec]
>        (do-things* tx)))
>
> If this is still too prone to error, you could also automate this pattern
> with a function:
>
>   (defn wrap-transaction [f]
>     (fn [db-spec & args]
>       (sql/with-db-transaction [tx db-spec]
>         (apply f tx args))))
>
>   (def do-things
>     (wrap-transaction do-things*))
>
> - James
>
> --
> You received this message because you are subscribed to the Google
> Groups "Clojure" group.
> To post to this group, send email to clojure@googlegroups.com
> Note that posts from new members are moderated - please be patient with
> your first post.
> To unsubscribe from this group, send email to
> clojure+unsubscr...@googlegroups.com
> For more options, visit this group at
> http://groups.google.com/group/clojure?hl=en
> ---
> You received this message because you are subscribed to a topic in the
> Google Groups "Clojure" group.
> To unsubscribe from this topic, visit
> https://groups.google.com/d/topic/clojure/fRi554wbPSk/unsubscribe.
> To unsubscribe from this group and all its topics, send an email to
> clojure+unsubscr...@googlegroups.com.
> For more options, visit https://groups.google.com/d/optout.
>



-- 
J. Pablo Fernández <pup...@pupeno.com> (http://pupeno.com)

-- 
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clojure@googlegroups.com
Note that posts from new members are moderated - please be patient with your 
first post.
To unsubscribe from this group, send email to
clojure+unsubscr...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
--- 
You received this message because you are subscribed to the Google Groups 
"Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to clojure+unsubscr...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to