Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Question to people having used Datomic:

Based on experience with Prolog, I always thought using Datalog in a database like Datomic would mean being able to model your data model using stored queries as a very expressive way of creating "classes". And that by modeling your data model using nested such queries, you alleviate the need for an ORM, and all the boilerplate and duplication of defining classes both in SQL and as objects in OO code ... since you already modelled your data model in the database.

Does Datomic live up to that vision?



You can definitely use Datomic in the way you describe, in at least a few different ways. I haven't often seen queries stored in the database itself. It's more common to have the queries as data in the application. Since queries are ordinary Clojure data structures, it's even more common to see functions that build queries from inputs or functions that transform queries (e.g., adding a tenant-id filter clause).

Datomic also support rules, including recursive rules. I wrote a library to do OWL-style inference about classes of entities using rules. You can see an example here (https://github.com/cognitect-labs/onto/blob/master/src/onto/...). This is a rule that infers all the classes that apply to an entity from an attribute that assigns it one class.

I would also say that building an "entity type definition" system as entities in Datomic is almost the first thing every dev tries after the tutorial. It works... but you almost never _need_ it later.


Great, many thanks for this elaboration!


Clojure in general is all about passing around little maps of data and in particular not using OO to model. So Datomic naturally continues that by returning maps of nested structures to represent your query results and does side-step the ORM completely.


Question: What's the difference between nested oop objects (composition over inheiritance) vs maps of nested structures?


Rich Hickey has a great talk about how Objects are data structures with unique interfaces that are unnecessary complexity. He showed and example of a web server with a web request object with request headers etc etc. Doing simple things like collecting information out of that nested object structure is bespoke and harder than it should be for no real gain. If everything is a map or list or set, it becomes completely trivial to extract and manipulate data from a structure. It's a subtle difference at first but when everything is like that it makes your system far simpler. Unfortunately I can't remember exactly where the part of this talk is.



Yes, this hit home so hard. All objects are bespoke mini languages that add little to no value and simultaneously make it hard to remember and hard to use. Just use maps!


An object is a schema, though.

A map is... Whatever you put in it.


This is the benefit. Languages like Clojure have excellent support for manipulating maps and I'd wager that something like 40-60% of Clojure code is similar-looking map manipulation stuff for this very reason.

If you need that validation, you just validate the map? You can use established methods like Malli or Clojure Spec for this. If you need to use a record with a fixed schema, just use a record instead of map. In Clojure, you can use most of the map functions for records too.


This comment is talking specifically in the context of Clojure.

In the Clojure culture (so to speak) maps may also have a schema, as used by various schema checking tools, which are richer than runtime type checks. (Not the same as database schema)

Nit: I would not say that a JVM object is a schema, because there’s more to it. Rich is well known for saying that the idea of an object complects two things: (1) a record-like (struct) data structure with the (2) code to manipulate it.

Sometimes it’s even more complected because in some languages classes can make assumptions about state across all objects and threading.


Constraints always have value. Without constraints programmers are confronted with the naked complexity of mapping between the set of all possible states of N bits which is (2^N)!.


More constraints don’t necessarily have more value, though. Each constraint needs to be thought through, not just have its value assumed.


> Unfortunately I can't remember exactly where the part of this talk is.

If you or anyone else remembers, I'd love to watch


Here is this specific part cut out from the larger talk "Clojure made simple": https://www.youtube.com/watch?v=aSEQfqNYNAc



Most of the time, the class name is used just as a nominal type identifier. That's what we do at least


“No real gain”

I don’t agree with this. iirc Rack ultimately uses and array to represent HTTP responses. It has three members: the status code, the headers, and the response body.

If you’re shipping a new change, is it easier to mistake response[0] or response.headers?

This is a trivial example, but the general class (ha) of trade-off is amplified with more complex objects.

I love clojure and lisp but the blindness behind a statement like “no real gain” has always kneecapped adoption.


> If you’re shipping a new change, is it easier to mistake response[0] or response.headers

False dichotomy. There are many options other than arrays. Clojure in particular is fond of hashmaps. You can have your response.headers without OOP.


In Clojure, response.headers is still data :) You just use the built-in ways of reading named keys, such as (:headers response) or (get headers :response).


Errata: (get response :headers)


I think there is a need for objects. An active connection, talking to the GPU, etc are not data -- identity is essential for their operation.

OOP is probably the best way to model such,.. well objects, allowing them a private, encapsulated state, and making it only modifiable, or even viewable through a well-defined public interface that enforces all its invariants.


I think OOP (object oriented programming) is abused and is not the optimal paradigm for most software services. It succeeds best at providing inter-operability structure for API design. "Objects", as mentioned here, are an abstraction. Stateful data can be organized and manipulated elegantly without use of the OOP paradigm. Many small systems that employ OOP hamper their maintenance and extendability by unnecessary dependence upon encapsulation and data ownership. Mutability is a villain here as well -- when data structures are immutable, there is little fear of panoptic architectures designed without ownership constraints. Here software is no longer corralled into walled gardens of "objects"; large complex types and their brittle method associations are avoided, greatly simplifying software architectures as a result.


Clojure uses objects for connections and things, but POJOs are harmful IMHO because it makes manipulation and collecting data a bespoke task every time. Every time you change the data, you have to change a class to represent the JSON and the ORM class and .... this is all just data


Well, that’s what Java records are for. As for having to change the type description, that’s more of a static typing discussion, though it can be generated — having a single source of truth and generating others is imo the best approach.


Java records weren't even a gleam in the eye of James Gosling when Clojure was solving these problems at its inception.


In his eyes? It definitely was a thing, records are just nominal product types, these are probably the single most used building block of programming languages.

I really like Clojure, but I really don’t know what some of its fans think (also true of other lisps), like there is a healthy pollination of ideas between languages, lisps are not God’s language.


Standard ML had records since the '70s. Both Clojure and Java would benefit from taking more from what came before, though Java at least had the excuse of being designed for low-powered set-top boxes.


According to the link below, ML records are mostly handled by hash-maps in Clojure, except that there’s no canonical key/val order or strict typing by default.

ML record:

  {first = "John", last = "Doe", age = 150, balance = 0.12}
Clojure hash-map:

  {:first "John", :last "Doe", :age 150, :balance 0.12}
Destructuring a record in an ML function:

  fun full_name{first:string,last:string,age:int,balance:real}:string =
    first ^ " " ^ last
(It’s unclear from the example whether or not all of the destructured values are required in the function signature. I hope they are not, but I left them in since I don’t know. The caret positioning raises further questions.)

Destructuring a map in a Clojure function:

  (defn full-name [{:keys [first last]}]
    (str first “ “ last))
I don’t know if I’m missing something that ML offers with its records aside from more strict typing, which you can also have in Clojure when it’s useful. In both cases, it looks like it’s applied at the function declaration and not the record declaration.

https://www.cs.cornell.edu/courses/cs312/2008sp/recitations/...


Clojure has records too.


Connection pools exist precisely because the code outside of the connection management piece shouldn’t have to care much whether or not there is an active connection. It’s all boilerplate, except for handling the “unable to connect” case.

When you call a connection or connection pool object, you’re querying its current state. This is absolutely data.


"It is better to have 100 functions operate on one data structure than to have 10 functions operate on 10 data structures."

Alan Perlis.


To paraphrase a well known saying, objects are the poor person's nested maps and vis versa.


Thanks for the comment!

I was more thinking of the means to define your data "classes" (or whatever it is called on this context) though, rather than how it is passed around.


It emphatically does. It has pull-syntax just for the nested entities obviating the need for ORM. I am surprised I had to scroll so far down to see this. This is its biggest selling point IMO.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: