42
votes

I have seen many Clojure programmers enthusiastic about the new core.async library and, though it seems very interesting, I am having a hard time seeing how it conforms to Clojure principles, so I have these questions:

  1. It uses mutable state everywhere, as the function names suggest by having an exclamation mark, like alt!, put!, >!, and others. If you put or take a value from a channel, that channel is modified inplace. Isn't it contrary to Clojure philosophy of preferring immutable data-structures, pure functions and so on? Or is core.async made to be used only where mutable things could not be avoided at all?
  2. Since "go" is a macro (thus modifying code structure) and ensures "<!" is used directly in a go-block, it is not possible to use "<!" inside another function, like this:

    (defn take-and-print [c]
     (println (<! c)))
    
    (def ch (chan 1))
    (>!! ch 123)
    
    (go (take-and-print ch))
    
    Assert failed: <! used not in (go ...) block
    

    It seems to me that this prevents simplicity and composability. Why is it not a problem?

  3. Maybe as a consequence of the previous two issues, a lot of code with core.async uses lower-level constructs such as loop/recur instead of map/filter/reduce. Isn't it a step backwards?

Where am I missing the point?

Thanks in advance.

6
Your first point misses Clojure's distinction between references and values. You don't mutate values in core.async.deprecated
I think his(her) point is that while mutable functions exist in Clojure they are not supposed to be used commonly and are there as an escape hatch in case you really need them.SCdF
@vemv True, you don't mutate the values you put into channels. But you do mutate the state of the channels themselves.aeuhuea
Which is consistent with atoms, refs, agents and such. Expressing change over time is a core Clojure proposition.deprecated
Regarding my third question, it seems Rich Hickey has just created many new higher-level functions in core.async, including map, filter and reduce for channels. See what he comitted yesterday: github.com/clojure/core.async/commit/…aeuhuea

6 Answers

38
votes

The first concern - yes the core operations are side effects. However channels don't have the problems normally associated with mutable references as they don't represent a "place" - channels are opaque, you cannot inspect them, in fact you can't even query whether a channel is closed or not beyond reading nil.

The second concern - doing anything more than shallow yield would mean whole program transformation. This is a tradeoff and I think a reasonable one. The level of composition is channels not go blocks and they compose just fine.

The final concern, you can easily do Rx style map/filter/reduce operations over channels and people have already done so.

16
votes

The limitation of the go macro (its locality) is also a feature: it enforces source code locality of stateful operations.

11
votes
  1. it's somewhat the other way around, Core.async can only be used in systems where Immutability is the norm. So Clojure's principles enable core.async rather than the inverse.

  2. This is a limitation, happens in other place in clojure as well, the limitation of anonymous functions not composing with the % symbol seems to at least present the same idea. Not that finding another case of a limitation makes it better of course.

  3. I have not seen this my self, though this would be a step backwards if you where attempting to take code that is simple, and clean when expressed in one way and then express it in a way that is ... not that way...

5
votes

Rich Hickey said in one of the blip.tv lectures that Clojure is "85 % functional". I like to see core.async as one part of the other 15%. Core.async is great for solid user-interaction among other things which would have been done by promises, delays and other things, likely in a more messy way.

2
votes

Every programm has two parts, one part that is always non function talk data in, spit it out and so on. For this part we know have core.async, it is ture that core.async has mutable things, but note two things. The state of the channels is basiclly managed by the library. The things you put on it are, what on might call flowstate. Meaning that when you put something like a channel you do not expect to come back at it, or even change it.

Core.async is nice to manage this part of your programm. For the rest, all the transformation and calculation on your data, clojure tries its best to give you good tools to do it functionally.

It seems to me that this prevents simplicity and composability. Why is it not a problem?

There are two worlds, the sync and the async world. You can put stuff, or take stuff out of the async wrold with put! and take! but other then that (and maybe some other function) these to worlds are seperated from each other. Clojure does not want to become a completly async language. The functions and data transformation are what need to be composable.

Maybe as a consequence of the previous two issues, a lot of code with core.async uses lower-level constructs such as loop/recur instead of map/filter/reduce. Isn't it a step backwards

Operation like that over channels will be possible. Core.async is still young and not all possible construct and functions have been written yet.

But in generally if you have large data transformations you dont really want to do them in async world, you want to have them in a collection and then throw something like the reducres framework at it.

The main thing to understand is this, core.async is not the new standard, its just one more thing that helps you programm. Sometimes you need STM, sometimes Agents, sometimes CSP it depends and clojure wants to give you all the options.

One reason people like core.async is because it helps with some stuff that is otherwise really hard to handle, like dealing with callbacks.

-1
votes

perhaps a possible solution to use (<! c) outside go macro could be done with macro and its macro expansion time :

This is my example:

(ns fourclojure.asynco
      (require [clojure.core.async :as async :refer :all]))

(defmacro runtime--fn [the-fn the-value]
  `(~the-fn ~the-value)
  )
(defmacro call-fn [ the-fn]
  `(runtime--fn ~the-fn (<! my-chan))
  )

(def my-chan (chan))

(defn  read-channel [the-fn]
  (go
  (loop []
    (call-fn the-fn)
    (recur)
    )
  ))

(defn paint []
  (put! my-chan "paint!")
  )

And to test it:

(read-channel print)
(repeatedly 50 paint)

I've tried this solution in a nested go and also works. But I'm not sure if it could be a correct path