Sum types in Clojure? Better to represent as tagged records or as variant vectors?

860 views
Skip to first unread message

Didier

unread,
Aug 22, 2017, 5:52:58 PM8/22/17
to Clojure
I'm reading on http://www.lispcast.com/reduce-complexity-with-variants and have seen Jeanine's talk on her encoding scheme for sum types, but for some reason, I'm not convinced of the benefit over a tagged record encoding. I see that the positional vector or hash-map suggested in the lispcast article has a theoretically smaller number of states, but because it lacks names, it seems at a higher risk of human error. Anyone has thoughts on this? And now that we have specs, I feel like a multi-spec would address concerns of the tagged record encoding by being able to validate that the correct set of keys exist for the given type, while also having the benefit of named fields. What do others think?

Timothy Baldridge

unread,
Aug 22, 2017, 6:04:24 PM8/22/17
to clo...@googlegroups.com
I find the arguments for variants very unconvincing. As you stated with specs and s/keys you can spec the same sort of data, and in a way that's much more open-ended.

For example:

[:person {:name "Tim" :grade "B"}]

What is the type of this data? We would probably guess a :person. But what if we wanted to "cast" it to a :student? Well then we'd have to change the variant tag to :student, but then it would no longer be a person, unless we introduced some sort of inheritance that said all students are people. 

I much prefer spec:

(def ::person (s/keys :req [::name]))
(def ::student (s/keys :req [::grade ::name]))

I'm very much in the camp that longs for a super fast s/valid? implementation so I could go and re-implement multi-methods and protocols on top of spec. Much more flexible, and honestly it more correctly models the real world. 


On Tue, Aug 22, 2017 at 3:52 PM, Didier <did...@gmail.com> wrote:
I'm reading on http://www.lispcast.com/reduce-complexity-with-variants and have seen Jeanine's talk on her encoding scheme for sum types, but for some reason, I'm not convinced of the benefit over a tagged record encoding. I see that the positional vector or hash-map suggested in the lispcast article has a theoretically smaller number of states, but because it lacks names, it seems at a higher risk of human error. Anyone has thoughts on this? And now that we have specs, I feel like a multi-spec would address concerns of the tagged record encoding by being able to validate that the correct set of keys exist for the given type, while also having the benefit of named fields. What do others think?

--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@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+unsubscribe@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+unsubscribe@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.



--
“One of the main causes of the fall of the Roman Empire was that–lacking zero–they had no way to indicate successful termination of their C programs.”
(Robert Firth)

James Reeves

unread,
Aug 22, 2017, 7:44:49 PM8/22/17
to clo...@googlegroups.com
On 22 August 2017 at 23:04, Timothy Baldridge <tbald...@gmail.com> wrote:
I find the arguments for variants very unconvincing. As you stated with specs and s/keys you can spec the same sort of data, and in a way that's much more open-ended.

For example:

[:person {:name "Tim" :grade "B"}]

What is the type of this data? We would probably guess a :person. But what if we wanted to "cast" it to a :student? Well then we'd have to change the variant tag to :student, but then it would no longer be a person, unless we introduced some sort of inheritance that said all students are people.

I don't think that example is a representative use of variants in Clojure. More typically, variants are used like key/value pairings that exist outside a map. For example:

  [:person/name "Tim"]

If you actually have a map, the key/value pairs speak for themselves:

  #:person{:name "Tim", :grade "B"}

We can tell this is a "person" because it adheres to a particular shape, which can be specced out:

  (s/def :person/student (s/keys :req [:person/name :person/grade]))

But if you just have a key/value pairing, the value alone doesn't tell you much:

  "Tim"

Using a vector to represent a key/value seems to fit in well with how Clojure currently works:

  (find m :person/name)
  => [:person/name "Tim"]

But there's no "s/variant" or "s/entry" spec that's as convenient to use as s/keys.

--
James Reeves

Timothy Baldridge

unread,
Aug 22, 2017, 8:18:31 PM8/22/17
to clo...@googlegroups.com
Great, so these approaches are suggesting we wrap every value in a vector or a hash map (as the lisp cast article does). That doesn't really help much at all. Why is:

[[:image/name "img.png"]
 [:encode/width 42]
 [:encode/height 33]]

Any better than

{:image/name "img.png"
 :encode/width 42
 :encode/height 33}

Or perhaps people aren't suggesting vectors of vectors, I don't know. All of it makes no sense to me when I look at in-production Clojure code. I don't pass image names around as bare strings. I pass them around as part of a hashmap where the keys give context to the strings, and also allow me to pass in other data about the parameter. 

Variants and sum types seem to come from OCaml and the like where strict typing makes them much more needed. But in Clojure we prefer heterogeneous collections, so I just don't understand why I would ever want variants. They complicate the code for really no benefit. The complexity they are attempting to solve can be solved by simply using maps and records instead of bare values. 



--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@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+unsubscribe@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+unsubscribe@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Didier

unread,
Aug 22, 2017, 8:50:09 PM8/22/17
to Clojure
Right, I see things the way tbc++ does, but I'm wondering if I'm missing some key insights that make variants better.

I similarly struggle with the difference between (s/or) and (s/multi-spec).

If I were to implement the lispcast example today with spec I'd either use (s/or):

(s/def ::image-type #{:image/in-memory :image/on-disk :image/web})
(s/def ::pixels vector?)
(s/def ::filename string?)
(s/def ::url string?)
(s/def ::image-source
  (s/or :in-memory (s/keys :req-un [::image-type ::pixels])
        :on-disk (s/keys :req-un [::image-type ::filename])
        :web (s/keys :req-un [::image-type ::url])))

I could pair that with the case-of macro from https://github.com/lambdaisland/uniontypes to get some guarantees that I always handle all cases.

Or I would use (s/multi-spec):

(s/def ::image-type #{:image/in-memory :image/on-disk :image/web})
(defmulti image-type ::image-type)

(s/def ::pixels vector?)
(defmethod image-type :image/in-memory [_]
  (s/keys :req-un [::image-type
                   ::pixels]))

(s/def ::filename string?)
(defmethod image-type :image/on-disk [_]
  (s/keys :req-un [::image-type
                   ::filename]))

(s/def ::url string?)
(defmethod image-type :image/web [_]
  (s/keys :req-un [::image-type
                   ::url]))

(s/def ::image-source (s/multi-spec image-type ::image-type))

I'm not sure the practical difference between one or the other, but they both seem like they would cover the lispcast use case perfectly, where I now have a map like so:

{:type :image/in-memory
 :pixels [...]}
;; OR
{:type :image/on-disk
 :filename "/cats.jpg"}
;; OR
{:type :image/web

As all valid ::image-source. The spec restricts the possible state to only 3 and I get a nicer map representation with named values.

So maybe variants were useful pre-spec? But even before spec, I still don't really get why they were any better? They just feel prone to the same human errors that ordered constructors are prone too. And ya, where my record has more then one value, what are you supposed to do?

This:
[[:image/name "img.png"]
 [:encode/width 42]
 [:encode/height 33]]

Or this:
[:image/name "img.png" 42 33]

Or this:
[:image/name ["img.png" 42 33]]

Or this:
[:image/name {:name "img.png"
                        :width 42
                        :height 33}]


??

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+u...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Timothy Baldridge

unread,
Aug 22, 2017, 9:00:21 PM8/22/17
to clo...@googlegroups.com
I think the article is a bit misleading. Variants were never that popular in Clojure. Infact I've never seen them used in production code or the most common Clojure libraries. So I'm a bit perplexed as to why the article recommends them so strongly. 

So I think the answer is, they are a fun thought experiment in Clojure, but are of limited usefulness due to the tools we have available that make them unneeded. 

It's a bit like recommending that new users use actors in Clojure. Sure, you can shoehorn them in, but there's a reason why they aren't the default.

Didier

unread,
Aug 22, 2017, 9:13:38 PM8/22/17
to Clojure
Ya, I guess the article says that some DSL uses them, like hiccup, so maybe its only in the perspective of a DSL you'd need to parse, and not really for modeling business entities.

Like this:
(s/def ::temp (s/or :one string?
                    :two int?))

Will conform to a variant:

=> (s/conform ::temp "one-two")
[:one "one-two"]
=> (s/conform ::temp 12)
[:two 12]

And not to a tagged record like:
{:tag :one
 :val "one-two"}

But, I don't know, that seems like a different and uncommon use case, I don't really see this as using a sum type, just describing data to be parsed, which I guess the vector form can be easier to work with. I think the clojurescript AST is actually using tagged records, and most people find it too verbose because of it.

Anyways, I'd still like to hear if there's a major benefit I'm overseeing, or a detail I'm not understanding, if any.



P.S.: I figured the difference between (s/or) and (s/multi-spec) I think. S/or is like structural-types, my example actually doesn't work, because :filename and :url have the same structure, both string? So conform will not be able to tell them apart. So if you need to know any kind of semantic difference which the shape can not infer, (s/or) won't work, you'll need (s/multi-spec) instead, which are more like nominal-types. Multi-spec I guess can also use any function to select the appropriate spec, so they're more powerful, but often I think the way I'm using it will be most common.

Timothy Baldridge

unread,
Aug 22, 2017, 9:23:59 PM8/22/17
to clo...@googlegroups.com
Nope, ClojureScript uses nested hash maps (try it for yourself here: http://vps124502.vps.ovh.ca/rage/resources/public/). As does tools.analyer. Instaparse and Hiccup use a variant of variants, but funny enough the first thing I ever do when writing code with instaparse is write a converter from it's vector-based parse trees into a nested hashmap AST. 

--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@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

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+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Sean Corfield

unread,
Aug 22, 2017, 9:47:00 PM8/22/17
to clo...@googlegroups.com

I think part of the issue is that the article dates back to mid-2015 and `clojure.spec` wasn’t a thing back then, was it?

 

Variants feel like a solution to a problem for which we have a much better solution _today_ than we did two years ago. The article talks about using core.typed and core.match with variants – because that’s what we had then. I’m fairly sure that if Eric (and Jeanine) were writing their material today, we’d see `clojure.spec` front and center and regular hash maps being used.

 

Sean Corfield -- (970) FOR-SEAN -- (904) 302-SEAN
An Architect's View -- http://corfield.org/

"If you're not annoying somebody, you're not really alive."
-- Margaret Atwood

--

Timothy Baldridge

unread,
Aug 22, 2017, 9:51:43 PM8/22/17
to clo...@googlegroups.com
I think that's true, but even back in 2015, these were the only two sources calling for this approach, and it never really caught on. I even did some experiments with it, but my conclusion was that it was more trouble than it was worth. 

On Tue, Aug 22, 2017 at 7:46 PM, Sean Corfield <se...@corfield.org> wrote:

I think part of the issue is that the article dates back to mid-2015 and `clojure.spec` wasn’t a thing back then, was it?

 

Variants feel like a solution to a problem for which we have a much better solution _today_ than we did two years ago. The article talks about using core.typed and core.match with variants – because that’s what we had then. I’m fairly sure that if Eric (and Jeanine) were writing their material today, we’d see `clojure.spec` front and center and regular hash maps being used.

 

Sean Corfield -- (970) FOR-SEAN -- (904) 302-SEAN
An Architect's View -- http://corfield.org/

"If you're not annoying somebody, you're not really alive."
-- Margaret Atwood

 

From: Timothy Baldridge
Sent: Tuesday, August 22, 2017 6:00 PM
To: clo...@googlegroups.com
Subject: Re: Sum types in Clojure? Better to represent as tagged records or asvariant vectors?

 

I think the article is a bit misleading. Variants were never that popular in Clojure. Infact I've never seen them used in production code or the most common Clojure libraries. So I'm a bit perplexed as to why the article recommends them so strongly. 

 

So I think the answer is, they are a fun thought experiment in Clojure, but are of limited usefulness due to the tools we have available that make them unneeded. 

 

It's a bit like recommending that new users use actors in Clojure. Sure, you can shoehorn them in, but there's a reason why they aren't the default.

--

“One of the main causes of the fall of the Roman Empire was that–lacking zero–they had no way to indicate successful termination of their C programs.”
(Robert Firth)

--

You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@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

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+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

 

--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@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

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+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

James Reeves

unread,
Aug 22, 2017, 10:07:56 PM8/22/17
to clo...@googlegroups.com
On 23 August 2017 at 01:18, Timothy Baldridge <tbald...@gmail.com> wrote:
Great, so these approaches are suggesting we wrap every value in a vector or a hash map (as the lisp cast article does).

What? No, that's not what I'm saying at all.

If you have an unordered collection of key/value pairs where every key is unique, then of course you use a map.

But if you have only one key/value pair, how do you represent that? Or if you want an arbitrarily ordered collection of key/value pairs? Or a collection with repetition?

For example, a series of events that represent a player's moves in a card game:

  [:card/draw :9h]
  [:card/draw :qh]
  [:card/draw :4s]
  [:card/discard :4s]
  [:card/draw :7d]

I've also found it a useful pattern for data access:

  [:data/found "Bob"]
  [:data/not-found]
  [:data/unauthorized]
  [:data/retry-in 600]

A [k v] vector isn't the only way of representing data like this, but it is probably the most concise.

--
James Reeves

Didier

unread,
Aug 22, 2017, 10:48:47 PM8/22/17
to Clojure, ja...@booleanknot.com
I can see it be quick and concise for representing events, but that's also exactly the use case example for multi-spec: https://clojure.org/guides/spec#_multi_spec

What happens if your event needs more data? Maybe draw needs 2 attributes, the card and the deck? Now you have implicit encoding, where what the attributes are for the event are defined by its position in the vector.

Where as you could have just as easily had:

{:event/type :draw :card :4s :deck :player1}

You can make a vector of those for the order of the events.

Yes, you can also use variants, and have:

[:event/draw :4s :player1]

My question is, what's the pros/cons? Its more concise, but barely. Its harder for it to represent optional values, you have to allow nil: [:event/draw :4s nil]. You can mistake the order: [:event/draw :player1 :4s].

I think to represent the type of a single element, I agree with you, but if you've got a compound type, at first glance, they'd appear less practical and more error prone to me. So your second example makes more sense.

Timothy Baldridge

unread,
Aug 22, 2017, 10:58:17 PM8/22/17
to clo...@googlegroups.com
Put let's take that and look at it under a engineering lens a bit: 


>> For example, a series of events that represent a player's moves in a card game:
>>
>>  [:card/draw :9h]

You have an ad-hoc encoding of data here now. Expand it out a bit, and make it machine readable instead of preferring ease of writing and you get:

{:player/action :card/draw
 :card/number :9
 :card/suit :hearts}

Much easier to introspect, and extensible as future data just flows through without existing code breaking because you add extra data. 

Likewise:


>> I've also found it a useful pattern for data access:
>> 
>>  [:data/found "Bob"]

I've worked with systems that have done this, and I really dislike it. Because now I have to zip the inputs of a function with the outputs if I want a composable system usable in pipelines and the like. 

What's much better:

{:op/status :success
 :data/found "Bob"
 :data/key "444-434-3323"
 :server/ip ....}

Now not only do I know what data I got, but where it came from, the key I originally used, etc. 

>>   [:data/retry-in 600]

If this is a web endpoint, I'd really like a URL with it:

{:op/status :busy
 :retry/delay 600
 :retry/url #url "http://...."}

In short variants almost never give me enough information, and when they do, it's encoded in some sort of DSL that I have to parse or enrich if I want nice machine level introspection. 

--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@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+unsubscribe@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+unsubscribe@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

James Reeves

unread,
Aug 22, 2017, 11:15:20 PM8/22/17
to Didier, Clojure
On 23 August 2017 at 03:48, Didier <did...@gmail.com> wrote:
I can see it be quick and concise for representing events, but that's also exactly the use case example for multi-spec: https://clojure.org/guides/spec#_multi_spec

What happens if your event needs more data? Maybe draw needs 2 attributes, the card and the deck? Now you have implicit encoding, where what the attributes are for the event are defined by its position in the vector.

Sometimes it's an advantage to have a deliberately constrained format, particularly when it comes to storage or indexing. Datomic, for instance, is effectively an indexed event log of [e a v t] tuples.

If you're in a situation where the event data can and may expand, then use a map. But I'd argue that there are situations where the scope is limited, or you want a deliberate and hard constraint on what data is in an event.

Another possible situation is if you're parsing map data incrementally. For example, a Ring request map could be read in as a series of key/value pairs:

  GET /example HTTP/1.1

  [:request/method :get]
  [:request/uri "/example"]
  [:request/protocol "HTTP/1.1"]
  [:request/header ["host" "www.example.com"]]

I'm not saying that variants should be favoured over maps in all situations; just that there are situations where you're certain that you need key/value pairings.

--
James Reeves

Timothy Baldridge

unread,
Aug 22, 2017, 11:27:33 PM8/22/17
to clo...@googlegroups.com
But Datomic has E in [e a v] which links multiple [a v] pairs into an entity...which is basically a map. So I don't think that applies here. 


  GET /example HTTP/1.1
  Host: www.example.com

  [:request/method :get]
  [:request/uri "/example"]
  [:request/protocol "HTTP/1.1"]
  [:request/header ["host" "www.example.com"]]

Once again, a ad-hoc encoding. What is "GET", what is "/example". I see that datastructure and all I see are hashmaps. 

Do it the way ring does ;-)

{:method :get
 :uri "..."
 :headers [...]}



--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@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+unsubscribe@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+unsubscribe@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

James Reeves

unread,
Aug 22, 2017, 11:44:28 PM8/22/17
to clo...@googlegroups.com
On 23 August 2017 at 03:58, Timothy Baldridge <tbald...@gmail.com> wrote:
Put let's take that and look at it under a engineering lens a bit: 

>> For example, a series of events that represent a player's moves in a card game:
>>
>>  [:card/draw :9h]

You have an ad-hoc encoding of data here now. Expand it out a bit, and make it machine readable instead of preferring ease of writing and you get:

{:player/action :card/draw
 :card/number :9
 :card/suit :hearts}

Much easier to introspect, and extensible as future data just flows through without existing code breaking because you add extra data. 

But possibly over-engineered. Suppose that the only function we want to produce is:

  (cards-in-hand events)
  => #{:9h :qh :7d}

If we have a function that returns [k], [v] or {k v}, then I have a hard time buying the idea that the input to the function should be more complex than [[k v]].

If the scope of the data is not so constrained, then sure, use a map.

>> I've also found it a useful pattern for data access:
>> 
>>  [:data/found "Bob"]

I've worked with systems that have done this, and I really dislike it. Because now I have to zip the inputs of a function with the outputs if I want a composable system usable in pipelines and the like. 

What's much better:

{:op/status :success
 :data/found "Bob"
 :data/key "444-434-3323"
 :server/ip ....}

Now not only do I know what data I got, but where it came from, the key I originally used, etc. 

Which is useful only if you plan on using that additional data. If you are only ever interested in the key and value, and the function is internal, why dress it up?

--
James Reeves

Timothy Baldridge

unread,
Aug 23, 2017, 12:15:31 AM8/23/17
to clo...@googlegroups.com
Simple: because failing to put it in a map constrains future growth. 

Putting things into a set restricts you to unique values. Putting values into a vector constrains you to a specific ordering. And quite frankly one little mistake like this can cause tons of refactoring down the road. Better to write it once, simply, and extensible.

I can't tell you how many codebases I've worked on where they failed to allow for "extra data" in either the arguments or the return of a function or endpoint and we've had to do major refactoring once a single part of the system needs a configuration flag. 

But I guess I'd flip it around. Why would I ever want: 

[:response val]

when I could have 

{:status :response
 :result val}

The latter is more readable, self-documenting, and extensible. I can't think of a single design benefit the variant variant has over the map.

--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@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+unsubscribe@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+unsubscribe@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

James Reeves

unread,
Aug 23, 2017, 12:21:56 AM8/23/17
to clo...@googlegroups.com
On 23 August 2017 at 04:27, Timothy Baldridge <tbald...@gmail.com> wrote:
But Datomic has E in [e a v] which links multiple [a v] pairs into an entity...which is basically a map. So I don't think that applies here. 

Except that [e a v t] facts in Datomic are ordered and not necessarily unique, and that's my part of my point. A collection/stream of variants (or tuples that contain some manner of [k v] part) are map-like but often have additional properties, such as ordering.
 
  GET /example HTTP/1.1
  Host: www.example.com

  [:request/method :get]
  [:request/uri "/example"]
  [:request/protocol "HTTP/1.1"]
  [:request/header ["host" "www.example.com"]]

Once again, a ad-hoc encoding. What is "GET", what is "/example". I see that datastructure and all I see are hashmaps. 

Do it the way ring does ;-)

{:method :get
 :uri "..."
 :headers [...]}

And what happens if I want to stop processing the request the moment I hit the request method? For example:

  (fn [req]
    (go
      (match (<! req)
        [:request/method :post] (close! req)
        [:request/method :get] (onto-chan req example-response))))

Again, the problem with a map is that it is unordered and fixed. Sometimes it's useful to process a stream of key/value pairs over time.

--
James Reeves

James Reeves

unread,
Aug 23, 2017, 1:01:08 AM8/23/17
to clo...@googlegroups.com
On 23 August 2017 at 05:15, Timothy Baldridge <tbald...@gmail.com> wrote:
Simple: because failing to put it in a map constrains future growth. 

Sometimes that's what you want. A constrained function is a simple function.

Should (find {:a 1} :a) produce {:key :a, :val 1} instead of [:a 1]? No, because it doesn't need to be extensible.

And for that matter, where do we stop? Should:

  {:person/score 89}

Be:

  {:person/score {:val 89}}

Just in case we want to extend it in future?

  {:person/score {:val 89, :max 100}}

Any argument about extensibility around [[k v]] also applies to {k v}.

But I guess I'd flip it around. Why would I ever want: 

[:response val]

when I could have 

{:status :response
 :result val}

Well, going purely by syntax, it's more concise, (IMO) more readable, easier to match and destruct, and intrinsically compatible with "into" like functions:

  (def latest-values (async/into {} ch))

I don't see how you can say {k v} is somehow fine, but a stream of [k v] pairs over time is somehow bad.

--
James Reeves

Colin Yates

unread,
Aug 23, 2017, 5:20:58 AM8/23/17
to clo...@googlegroups.com
Just a casual bystander here, and this is fascinating to read. The
question screaming out to be answered is what problem are you trying
to solve with your choice of encoding? It seems both James and Timothy
have different requirements, both of which are valid but incompatible
with each other.

Nothing is simple and everything is a decision, and the more explicit
we can make those decisions the better we all becomes as software
engineers..

Anyway, I had better get back to work where I model everything as a
map, just because... ;).
> --
> You received this message because you are subscribed to the Google
> Groups "Clojure" group.
> To post to this group, send email to clo...@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+u...@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+u...@googlegroups.com.

Mark Engelberg

unread,
Aug 23, 2017, 1:30:52 PM8/23/17
to clojure
I usually model sum types as maps with either a :type or :tag key to specify the kind of map it is.  Occasionally, I use vectors with the tag in the first position, especially when I need to favor concision, for example, when the data is serving as a DSL with which I will be manually entering a lot of data.  I almost never use records for sum types.

Using vectors would be somewhat more useful with core.match, because it is easy to combine taking cases on the tag with destructuring the correct number of expected slots after the tag.  I personally don't trust core.match for serious projects, because it is still in alpha after 7 years.  With regular destructuring, vectors are best for scenarios where each vector has the same number of slots -- then you can naturally destructure all the slots of the vector at once in the input to the function.

I avoid records for this kind of use because: 1. They aren't sufficiently flexible when the needs of projects change (i.e., everything you can do with records and protocols you can do with maps and multimethods, but not vice versa).  2. Records and protocols tend to mess up the state of your REPL (e.g., when defrecords are recompiled they cause problems with instance? calls and equality checks to things in memory until your whole program is recompiled and prior objects thrown away).  3. Until a very recent 1.9 alpha, records performed poorly in sets or as keys in a map because they didn't cache their hash values.  4. Use of records and protocols requires much more careful code organization because it is all too easy to run afoul of Clojure's lack of support for cyclical dependencies between namespaces.

Didier

unread,
Aug 25, 2017, 1:59:47 PM8/25/17
to Clojure
Thanks for everyone's great input.

Currently, I see the big distinction being concise vs extension. Maybe for streaming variants would be better as the format would be smaller to transfer over a wire. And for small short lived programs, or things you know won't need extension, variants offer a slightly more convenient structure to work with.

I think both can be specced easily. S/or a few s/tuple to spec a variant. And multi-spec over a set for the map version.

I'd like to explore then the issue of extensibility with variants. How would you extend them, is there really no way? This is some of my brainstorming thoughts.

1) How to design a variant of more then one value?


1.1)

I know this is a standard variant of one value:

[:image/web "www.myimage.com/image.jpg"]

I believe to extend it to more values, you would do:

[:image/file "/images/image" :jpeg]

That is, you'd threat it as a constructor function, which creates an :image/file type and takes an ordered list of arguments. This way, each variant type can even be overloaded on their arguments the way you'd overload a function.

[:image/file "/images/image"]

Can default to format :png for example, when using the one arg constructor.

1.2)

An alternative is to keep variants as vector pairs, and nest them.

[:image/file [:image/file-path "/images/image"] [:image/file-format:jpeg]]

In this form, variants are more like their map counterpart. Each element is named and itself a variant.

1.3)

You could also decide to limit variants to strict pairs, so the second element of any variant is either a variant or a vector of variants.

[:image/file [[:image/file-path "/images/image"] [:image/file-format:jpeg]]]

Now with both these forms, 1.2 and 1.3, if you threat them again as constructor functions, you now have a form of named parameter on your constructor, allowing mixed order.

1.4)

At this point, the variant has become pretty similar to a map, losing in verbosity over it even. There's just one advantage, the type is not a key/value pair, which I find is more intuitive to use, no need to know the special name of key that holds the type.

1.5)

Let's try to make it concise again.

[:image/file {:image/file-path "/images/image" :image/format :jpeg}]

This hybrid avoids needing a type key, while having named parameters, its the best of both worlds, but it mixes vectors and maps.

1.6)

Here it is with the lispcast suggestion:

{:image/file {:image/file-path "/images/image" :image/format :jpeg}}

What I don't like about this, is how do you group variants together? Do you add more to this map? Do you keep each variant a map of one key and group them on a vector?

It does solve the problem of wanting to pass a vector to your variant though, as the lispcast blog talks about.

1.7)

So I'm left with this form, which Clojure macros and options often use:

[:image/file :image/file-path "/images/image" :image/format :jpeg]

This is harder to spec I think, but you could write a predicate that did, there's just not an existing one that can do it I think.

Now a variant is a tuple with first element being the type, and an alternating pair of key/values. This is extensible like map, yet more concise. It isn't ambiguous to pass in a vector either, and lets you have names while not enforcing order.

Now what if I'd want the option to create my variant with named parameters or not? Some languages allow for powerful constructors like that.

1.8)

To do that, you need a way to distinguish if the rest of the vector is an alternating of named pairs, or an ordered list of arguments. I'm stuck here, I'm not sure its possible without restricting the typed a variant can take. If you group the rest in a vector or a map to indicate named pairs, then you can no longer pass a vector or map argument to a variant, since they'll be interpreted as a named pair list. You could use meta maybe, or a reader tag? Not sure I like those ideas though.


1.conclusion)

I like 1.1 and 1.7 the best.

I find 1.7 might actually be a better alternative to using maps. Its more intuitive, looks like a type constructor, but just like maps it allows arbitrary order and has names for readability while being more concise. Best of both worlds. Its not ambiguous either, you can easily pass in vector arguments.

1.1 is also great, if you don't mind losing named parameters and having implicit ordering. Its also non ambiguous, very concise and allows overloading.

Now, that's when you use them as type constructors. But the "type" you construct from them, after parsing the variant might be best represented as a Clojure map or record. It would be annoying to use a variant like that as an actual datastructure to perform logic on. If you need to get the :image/format value in a lot of places, you probably don't want to be passing around the variant and perform linear search lookup for it, and you can't use any of Clojure's function to modify the variant. You could implement some I guess, like an update-variant. So given this fact, using maps have an advantage that they're more homoiconic, you don't need to parse them, when you construct them the result is not a type constructor, but the actual datastructure you'd want to work with.

2) What can you use them for?

2.1) As pseudo type constructor they work well. For cases where the type is constructed by hand, they're a nice DSL. I find they make sense then for hiccup for example. When your types are constrcuted by the computer, I think maps are better. No need to parse them. It would be cool maybe to deftype an actual variant type. In a way, defrecords are almost that.

2.2) As open sum types. When something expects a variant, it means that thing can be one of any variant type. With namespaced keys, they can be restricted to a smaller open set. So :image/... variants are the set of open image variants. Something can spec that it takes a variant whose namespace is image. Then you're free to extend image variants with more of them, like image/web, image/file, etc.

2.3) As closed sum types. I guess you could also spec something to accept a specific set of specific variants, like either a :success or a :failure variant.

2.4) They could be used as product types too. Just allow the type argument to be a vector.

[[:image/file :image/web] "/images/image" :jpeg "www.myimage.com/image"]

This gets harder to oberload arguments though. Unless you use the named pair version.

[[:image/file :image/web] :image.file/path "/images/image" :image.file/format :jpeg :image.web/url "www.myimage.com/image"]

2.conclusion)

I can see now how Jeanine was saying you can use them as the foundation for types of a programming language. Personally, I'll explore they're use when I'm coming up with DSLs, or any time I need to manually create types, I might use variants to construct them, even if what I'm constructing is a map, they're a little nicer to type and read.

Timothy Baldridge

unread,
Aug 25, 2017, 4:30:37 PM8/25/17
to clo...@googlegroups.com
>> they're a little nicer to type and read

And that's where I have to disagree. The problem with most of these options is they complect ordering with meaning. 

[:image/file "/images/image" :jpeg]

Here I have no idea what these values mean. I have to have out-of-band information about what offset in the vector corresponds to what value. Functions have this same problem, look no further than `cons` vs `conj` to see potential confusion on argument ordering. 

So why don't we only use maps for function arguments? Well mostly to make the functions easier to manipulate by humans. But some of the best libraries for web APIs (AWS) that I've used have used almost an exclusively map based interface. 

Once again I have to repeat my mantra about DSLs. Don't start with DSLs. Start with maps. Build everything off of maps. They're extensible, easy to introspect, and can drive a host of metaprogramming algorithms. If maps are to hard to understand, build constructor functions on top of them. Then finally build a DSL on top, if you need it. 

Frankly, I have so many things I have to remember during programming. I'd much rather see a very uniform map-based interface. Than any sort of nested vectors, tagged values, or anything else. 

Surely we can't say that this:

>> [[:image/file :image/web] :image.file/path "/images/image" :image.file/format :jpeg :image.web/url "www.myimage.com/image"]

Is a better interface than:

{:image.file/path "/images/image"
 :image.file/format :jpeg
 :image.web/url "www.myimage.com/image"}

And as I said before, spec is designed from the start to support data in this format. Stating "this is a file", "this is a web image". Is just book keeping that doesn't need to be done. Is a map a :image/web? Well check its members and see if they match the spec. If they match, then you have a :image/web. No need for sum types, tags, wrapping values in vectors. Simply state what a thing should have for it to conform to an interface, and forget whatever else might be in there. It couldn't be simpler. 

--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@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

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+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Reid McKenzie

unread,
Aug 28, 2017, 4:53:33 PM8/28/17
to Clojure
FWIW I wrote a library around defining tagged map types https://github.com/arrdem/guten-tag and used it heavily in the Grimoire implementation almost entirely to my satisfaction. No idea if anyone else ever picked it up, and due to implementation details of Clojure's let map destructuring you can't just treat one of my map wrapper types as a normal Clojure map and :keys it apart but it works great with core.match.

Reid

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+u...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Timothy Baldridge

unread,
Aug 28, 2017, 5:04:50 PM8/28/17
to clo...@googlegroups.com
>>  https://github.com/arrdem/guten-tag

The name alone deserves a +1. :D



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+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Tim Gilbert

unread,
Aug 29, 2017, 10:12:33 AM8/29/17
to Clojure
I've used tagged unions to represent lookup identifiers for variant types and found them to be pleasant to work with. I think part of the reason they don't seem useful in the context of this discussion is that many of the examples given have not actually been variants. For example, in [:image/file "/images/image" :jpeg], every value will have those three data elements, and for {:status :response :result val}, every value will have both of those fields, and their values will be the same type. The identifiers I was working with were in heterogenous collections, so I might have [:image/ref 45] or [:user/name "bob"] or [:project/element "project-slug" 435].

It would certainly be just as easy to use {:collection/type ::person :user/name "bob"} and {:collection/type ::project :project/slug "project-slug" :project/element 435}, but I like the concision of the tagged unions.

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+u...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Didier

unread,
Aug 29, 2017, 10:16:48 PM8/29/17
to Clojure
Guten-tag looks pretty great. I think your readme also explains the differences pretty well.

I definitely feel like variants are really not great as data-structures to manipulate, but much better as declarative data constructors. Your library kinda unifies both into one type, its pretty clever.

James Gatannah

unread,
Sep 29, 2017, 1:32:57 AM9/29/17
to Clojure
I'm late to this party, and maybe this discussion has already gone on too long. If so, I'm sorry for dredging it back up.

But I'm curious about where Tim was going with this, because I feel like I might be missing out on a great idea:


On Tuesday, August 29, 2017 at 9:12:33 AM UTC-5, Tim Gilbert wrote:
I've used tagged unions to represent lookup identifiers for variant types and found them to be pleasant to work with. I think part of the reason they don't seem useful in the context of this discussion is that many of the examples given have not actually been variants. For example, in [:image/file "/images/image" :jpeg], every value will have those three data elements, and for {:status :response :result val}, every value will have both of those fields, and their values will be the same type. The identifiers I was working with were in heterogenous collections, so I might have [:image/ref 45] or [:user/name "bob"] or [:project/element "project-slug" 435].

I'm trying to match this up with my personal experience, and I think that's probably lacking.

Based on Jeanine's talk, I almost think that the real context here is language design. I got the impression that we could probably also gain benefits in network protocol design. (Someone mentioned this earlier in terms of REST and HTTP, but I think there are probably broader implications).

I'm generally in favor of the "just stick it into a map" approach. But I'd love to hear from people with "That didn't work out well" experience. Assuming there are any.

Thanks,

James


Reply all
Reply to author
Forward
0 new messages