I love Haskell, developed daily with it (side projects & libraries) for like five years, but I think it is way behind other languages when it comes to super basic tasks common to database-backed web applications like mapping a set of relational tables to a tree of data (think Company -> Contacts -> Addresses -> City, only a few layers deeper and more joins in order to find the right data-set) that can be serialized to JSON and sent to an API client. I literally could not find a way to do this without lots of slogging for every single query. And then if I want to take that graph of data back, and validate it and apply changes (like Elixir's Ecto Changesets) then god help me...I just have to write so much code to do something a library should be doing for me. And it seems nearly impossible to write that library in Haskell's type system. Would love to be wrong about this.
Agree, Haskell's works best when working with flat data structures. IHP also only supports one-level nesting depth, like this:
post <- fetch postId
>>= fetchRelated #comments
This is fine for simple form based CRUD apps.
As you mentioned you're consuming the data with a JS frontend: For these cases we added a subsystem called DataSync to IHP. DataSync allows to use the IHP query builder directly from the JS side. This way we basically avoid needing to hand-write these nested graph API responses. Complex forms that depend on a lot of complex data can now be implemented using React components.
Here's an example query call from the JS:
const todos = await query('todos')
.where('id', 'd94173ec-1d91-421e-8fdc-20a3161b7802')
.or(where('id', '173ecd94-911d-1e42-dc8f-1b780320a316'))
.fetch()
This way the frontend can query data that it needs just in time. And also write records to the database without needing to manually writing API actions (thus avoid the data tree problem).
You can find an example of this here: https://ihp.digitallyinduced.com/Guide/realtime-spas.html#bu... For web apps that are fully consumed by a single page app, we're right now building IHP Backend, which provides the IHP tooling + the DataSync API in a way that is way simpler than full stack IHP. If you're curious, you can find a demo video here: https://youtu.be/-jj19fpkd2c
I'm reluctant to say that it is impossible, because there are so many clever things that have been done with Haskell's type system that I can barely understand how to use, much less how to design those systems (like Servant's typed URLs). But from a naive user point of view, I don't see how its remotely possible to develop Ecto-style (or ActiveRecord style) associations with selective eager pre-loading, and typed validations against arbitrary trees of data with such a strictly typed system. And I think I've looked at every SQL library for Haskell and none of them really approach it.
I'm sure its possible to build something that compiles, but it would be so awful to use that you are better off with hand-written SQL where you need it, and N+1 stuff where you can get away with it.
That's awesome, thanks for linking it to me. I don't think I have seen this library before, looks like its fairly new. I have to admit from the signature I can't tell if I can nest these arbitrarily deep, it looks like I can?
Having said that, Postgres supports JSON fine, and Opaleye and Rel8 support Postgres's JSON fine, so you can just use that if you want arbitrarily structured and nested stuff.
If I understand this correctly: you are saying that strict-typing prevents arbitrary depth record requests to be made to SQL because Haskell cannot represent an arbitrary tree of types...
I presume that the accessors in Haskell would be 1-1 with SQL language requests, but the problem is that SQL queries are not typed? Or are dynamically typed?
So, instead of a generic library for SQL queries, one would have to implement a library for a specific SQL DB structure?
There are typed query libraries, like esqueleto which give you well typed and flexible query semantics, my complaint with it is that it just returns a tuple of lists, with a separate list for each type, you cannot return the data structured in a tree.
So if you have a companies table joined to a contacts table, you'd get a two element tuple with a list of companies and a list of contacts, and then you have to iterate to do the joins and turn it into a JSON tree.
And there is no help at all for wanting to take in a tree of JSON data, and turn it into updates to existing records and inserts for new records.
You could have a library that to knows how to return a tree if you define a typeclass for the tree, but the only way I'd know to do it would be a separate tree data type for each result set. In Ecto you do have to define a structure that knows all the possible associations, but which associations you actually load in any given query is flexible, you can have associations that are not loaded eagerly for one query, and loaded eagerly in another. In Haskell I can't imagine doing that without two different tree structs, because there is nothing that would know how to take two differently typed result sets and make the same tree partially loaded.
> So if you have a companies table joined to a contacts table, you'd get a two element tuple with a list of companies and a list of contacts, and then you have to iterate to do the joins and turn it into a JSON tree.
Esqueleto has a function to avoid n+1's and return a map now: