Skip to content

Instantly share code, notes, and snippets.

@austintaylor
Created April 11, 2012 17:00
Show Gist options
  • Save austintaylor/2360541 to your computer and use it in GitHub Desktop.
Save austintaylor/2360541 to your computer and use it in GitHub Desktop.

(from Haskell and Yesod by Michael Snoyman)

However, there is one issue that newcomers are often bothered by: why are IDs and values completely separate? It seems like it would be very logical to embed the ID inside the value. In other words, instead of having:

data Person = Person { name :: String }

have

data Person = Person { personId :: PersonId, name :: String }

Well, there's one problem with this right off the bat: how do we do an insert? If a Person needs to have an ID, and we get the ID by inserting, and an insert needs a Person, we have an impossible loop. We could solve this with undefined, but that's just asking for trouble.

OK, you say, let's try something a bit safer:

data Person = Person { personId :: Maybe PersonId, name :: String }

I definitely prefer insert $ Person Nothing "Michael" to insert $ Person undefined "Michael". And now our types will be much simpler, right? For example, selectList could return a simple [Person] instead of that ugly [Entity SqlPersist Person].

Entity is a datatype that ties together both the ID and value of an entity. Since IDs can be different based on backend, we also need to provide the Persistent backend we're using. The datatype Entity SqlPersist Person can be read as "the ID and value of a person stored in a SQL database."

The problem is that the "ugliness" is incredibly useful. Having Entity SqlPersist Person makes it obvious, at the type level, that we're dealing with a value that exists in the database. Let's say we want to create a link to another page that requires the PersonId (not an uncommon occurrence as we'll discuss later). The Entity SqlPersist Person form gives us unambiguous access to that information; embedding PersonId within Person with a Maybe wrapper means an extra runtime check for Just, instead of a more error-proof compile time check.

Finally, there's a semantic mismatch with embedding the ID within the value. The Person is the value. Two people are identical (in the context of a database) if all their fields are the same. By embedding the ID in the value, we're no longer talking about a person, but about a row in the database. Equality is no longer really equality, it's identity: is this the same person, as opposed to an equivalent person.

In other words, there are some annoyances with having the ID separated out, but overall, it's the right approach, which in the grand scheme of things leads to better, less buggy code.

@austintaylor
Copy link
Author

I've been dissatisfied for a while by the way the Active Record pattern reduces us to modeling database rows rather than a problem domain. What wasn't clear to me (it isn't spelled out in the Data Mapper pattern) is that to get away from this, we have to keep ids out of our models.

It's been a long time since I used a true Data Mapper ORM, but Yesod's Persistent seems to do a good job of eliminating boilerplate via Template Haskell. It seems to fit Haskell's static approach similarly to the way ActiveRecord fits Ruby's dynamic approach. I won't deny that it still seems a lot less convenient than AR, but I think this passage describes pretty clearly what you're getting in the tradeoff.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment