Python dataclass equivalent

65 views
Skip to first unread message

Kovas Palunas

unread,
Jan 20, 2022, 10:22:40 PM1/20/22
to Clojure
Hi all,

Coming from python, I use dataclasses a lot to tie complex collections of data together. I like using them in combination with type hints so that it's clearer to me at a glance what kind of data a function is processing.  When my data starts to look like a dict of dicts of lists, I turn to dataclasses to help my simplify.

So far in my Clojure journey, I've stumbled across two features that could help me do something similar.  First are datatypes (deftype, defrecord), and second is using maps with spec.  Based on my reading, using spec to define types seems like a really flexible system that does built in testing for me.  I was surprised to read that specs can't quite be treated like types though: https://ask.clojure.org/index.php/10464/can-i-use-spec-as-type-hint.  Maybe the intention is to use the :pre and :post keys in a function's options as type hints?

I'm curious if I'm thinking along the right lines here, or if there are other language features I could be looking at.  Also curious if anyone has suggestions for books or articles that explore data typing options in Clojure.

Thanks,

 - Kovas

James Reeves

unread,
Jan 21, 2022, 12:39:14 AM1/21/22
to 'EuAndreh' via Clojure
Type hints in Clojure have a different purpose to those in Python. In Clojure, type hints are only a mechanism to avoid reflection; their use is solely to improve performance.

So the question "Can I use spec as a type hint?" is actually asking "Will the compiler use specs to avoid reflection?", to which the answer is no. However Spec, and other equivalent libraries, can be used for runtime type checking in Clojure.

Where you'd use a dataclass in Python, you'd generally use a map in Clojure. This requires a little explanation because of the different ways Clojure and Python support data modelling.

Suppose Python you have a dataclass:

@dataclass
class InventoryItem:
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

Then the minimal equivalent Clojure code is simply:

(defn total-cost [{:keys [unit-price quantity-on-hand]}]
  (* unit-price quantity-on-hand))

The Clojure example lacks explicit type checking, but that may not be necessary. In Clojure, we can incrementally add more specific checks as necessary. For example, we could add a precondition to check the inputs:

(defn total-cost [{:keys [unit-price quantity-on-hand]}]
  {:pre [(float? unit-price) (int? quantity-on-hand)]}
  (* unit-price quantity-on-hand))

Clojure spec allows checks to be taken further, with the caveat that keywords must be namespaced. Spec is more granular than classes, as it's interested in single key/value pairs, rather than grouped properties as in a class or struct. With spec, we might declare:

(s/def :inventory.item/name string?)
(s/def :inventory.item/unit-price float?)
(s/def :inventory.item/quantity-on-hand int?)

(defn total-cost [{:inventory.item/keys [unit-price quantity-on-hand]}]
  (* unit-price quantity-on-hand))

This doesn't automatically perform type checks, as they may not be necessary. We can add type checks to a function with clojure.spec.test.alpha/instrument, or add them as a precondition using clojure.spec.alpha/valid?.

The validation required depends on the origin of the data. Suppose the inventory item comes from a database with its own schema. In which case, we may be reasonably certain that the types are correct and there's no need for additional confirmation.

Or suppose instead that we read the inventory item from an external source we may not trust. In that case, we'd want to validate it as input, and reject it if the data is invalid. But after we've validated it, we can pass it around internally without further checks.

Perhaps we're worried instead about human error. In this case, we might turn on checking during development and testing, but remove it during production when we're more interested in performance. This is the broad use-case for Spec's instrument function.

Clojure's takes a more nuanced, and possibly unique approach to data, compared to other languages. Understanding how Clojure views data is understanding Clojure as a language.
--
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
---
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.

--
James Reeves


Kovas Palunas

unread,
Jan 21, 2022, 1:22:12 PM1/21/22
to clo...@googlegroups.com
Thanks for the detailed response!  I like the idea of using destructuring to be more specific about what data a function expects.  I also think the pre/post conditions with spec would mostly solve my problem. I think I'm still getting used to spec's syntax, which feels fairly verbose to me when defining nested structures.  But maybe it will make more sense with time.

BTW, I posted this question at https://www.reddit.com/r/Clojure/comments/s90udy/python_dataclass_equivalent/ as well, where it has sparked a good deal of discussion. 

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/GcUqCXbh-DI/unsubscribe.
To unsubscribe from this group and all its topics, send an email to clojure+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/clojure/da5de1bf-a466-4b78-898a-84d9b6217a53%40www.fastmail.com.
Reply all
Reply to author
Forward
0 new messages