4

I am running into a problem setting up a simple proof of concept servant API. This is my User datatype and my API type:

data User = User { id :: Int, first_name :: String, last_name :: String } deriving (Eq, Show, Generic)
instance FromRow User
instance ToRow User
$(deriveJSON defaultOptions ''User)

type API = "users" :> ReqBody '[JSON] User :> Post '[JSON] User

The handler method for this uses postgresql-simple like this:

create u = liftIO $ head <$> returning connection "insert into users (first_name, last_name) values (?,?) returning id" [(first_name u, last_name u)]

Boilerplate code such as connecting to the db and the routing method has been elided. The problem is that if I make a POST request, what I want to be doing is creating a new user, so I would supply the JSON:

{ "first_name": "jeff", "last_name": "lebowski" }

but then my program fails at runtime with

Error in $: When parsing the record User of type Lib.User the key id was not present. 

This makes sense because the API specified a User which has an id field. But I do not want to have to pass a bogus id in the request (since they are assigned by postgres sequentially) because that is gross. I also can't move the id field out of the User datatype because then postgres-simple fails because of a model-database mismatch when making a GET request to a different endpoint (which does the obvious thing: get by id. Not included above). What do I do here? Write a custom FromJson instance? I already tried setting the Data.Aeson.TH options flag omitNothingFields to True and setting the id field to be a Maybe Int, but that didn't work either. Any advice would be appreciated.

asg0451
  • 493
  • 4
  • 13
  • 3
    Your concepts seem a little muzzy. Consider only pure Haskell. Does `mkUser :: User -> User` make sense? You need to allow clients to pass you a message that has only the info necessary to create a `User`. E.g., `data CreateUser = CreateUser { givenName :: Text, surName :: Text }`, `type API = "users" :> ReqBody '[JSON] CreateUser :> Post '[JSON] User`. – R B Aug 07 '16 at 21:33
  • I have considered that, but having to make an extra datatype for every endpoint like this seems a super non-optimal solution. I was hoping there was some other way to do it. I did implement this actually and found myself using a pattern like User, UserI (UserInput), but this represents just the omission of one field, and it seems like a bad idea to have the input type and the saved type be different. What if refactor User but forget to change UserI? Isn't this part of what servant tries to avoid? – asg0451 Aug 07 '16 at 21:49
  • This POST request represents passing a User to the server, which it then saves. Semantically I would want to keep them the same type, or have UserI be derived from User at compile time, or something. And my concepts are not muzzy, I am not inexperienced at Haskell. I realize that the types mean what they mean, but I was just trying to justify the use of servant by making a proof of concept with the simplest API imaginable (CRUD of one resource - users), something which would e.g. be done in Rails for you automatically. And I was surprised at how non-trivial it is. – asg0451 Aug 07 '16 at 21:52
  • 1
    I don't know Rails from Adam, but I would assume the trick there is that Ruby allows the ID field to be null (`nil`, right?). The impedance mismatch here is that a user without an ID is a different thing from a user with a nullable ID is a different thing than a user with a non-nullable ID. There's not really a way to get the type system to lie. I suppose you could come up with a Template Haskell solution that allows you to effectively knock out fields on an existing datatype to produce a new one. I'm not too quick with the TH though so I hope someone else comes along to snipe the answer. – R B Aug 07 '16 at 22:39
  • 1
    @asg0451 You can use a parametrized datatype holding the id and the saved element. I think you can read this from the persistent library : http://www.yesodweb.com/book/persistent#persistent_insert – Jean-Baptiste Potonnier Aug 07 '16 at 23:15
  • @RowanBlush I actually had a stab at that last night, and while doable definitely seems like a pain to create and (possibly) to use. But yeah, ruby lets any field be nil for better or for worse. – asg0451 Aug 08 '16 at 16:55

1 Answers1

4

First you need to understand that a User and a row in a table corresponding to this User are two different things.

A row has an id, a user does not. For example, you can imagine to compare two users without dealing with ids, or the fact that they are saved or not.

Once convinced, you will have to explain this to the type system, or you will have to deal with Maybe fields, which I think is not the solution here.

Some people talked about Template Haskell, I think it would be overkill here, and you need to solve the problem first.

What you can do is use a data type to represent a saved row in your database. Let's call it an Entity.

newtype PrimaryKey = PrimaryKey Int

data Entity b = Entity PrimaryKey b

Then the function saving a User row in your database could take a user as parameter and return a PrimaryKey (In you database monad, of course). Other functions reading from the database would return something using Entity User

Your field declarations are not duplicated, since you are reusing the User type as a parameter.

You will have to adapt FromRow/ToRow and FromJSON/ToJSON accordingly.

  • Well if a User object has no ID field, the type of the response to an index or show (/users, /users/:id) request would have to return Entities also, because the client will need that ID. So at that point what is a User good for? It seems to me that in every interacting-with-the-world context except for the POST request in my question, I need that ID. – asg0451 Aug 08 '16 at 16:59
  • I've defined a user in my database as having the id only as the primary key. I definitely want to reflect this in the code, so really I should additionally write a custom Eq instance that only checks IDs. You see what I'm saying? I really think that ID is an integral part of a User, not just an artefact of the database system. – asg0451 Aug 08 '16 at 17:02
  • You basically have the choice here to encode the specificity of this id field in the type system, or use a Maybe and deal with the Nothing value. This one is what you typically find in dynamic languages, you then have to manage a convention for this field when saving to your database. My experience is that programming by convention does not work well in Haskell. – Jean-Baptiste Potonnier Aug 08 '16 at 20:19
  • Some other use cases where it could be useful are validation, or form generation. – Jean-Baptiste Potonnier Aug 08 '16 at 20:26
  • Another thing is if later, for example you want to factorise this id generation logic for other things, you will have a very hard time ! Maybe it could be easier if Haskell had polymorphic variants like OCaml, but it is not the case ! – Jean-Baptiste Potonnier Aug 08 '16 at 20:28
  • @Jean-BaptistePotonnier what are OCaml's polymorphic variants like? If it's like polymorphism in haskell, I solve OPs problem with types like `data User a = {id :: a, ...}`, and then the `id` can come through as a Persistent id, or an Int, or a Maybe. Ultimately, I think these are all "ugly" solutions that would be made elegant by perahps row types or proper sub-types. Thoughts? – Josh.F Aug 15 '19 at 19:40