Navigating and Learning Data Access in FSharp
I wrote a blog post about me getting in to FSharp web application development, but am having issues deciding how I want the data access to look. I'm very much open to feedback!
https://blog.keyboardvagabond.com/programming/navigating-and-learning-data-access-in-fsharpOpen linkView original on piefed.keyboardvagabond.com
Ok, I think that I may have come up with something that mixes some of the OO benefits of typed queries with some of the functional benefits of FSharp. One thing that I ran in to was how to run multiple queries in a transaction, but have them both with independent metrics. My first attempt on my own wound up having two different runners being used and looking like
Needless to say. This is unweildy and who the heck (including future me) is going to remember to do it that way?
So, I asked both claude sonnet 4.6 (not 4.7) with the caveman skill to make it talk like a caveman and save tokens, as well as the smaller gpt-oss 20B model that can run on my GPU, just for fun. Surprisingly, the smaller model came up with a close solution and structure to sonnet, but wasn't quite able to get the composition that I wanted.
What I had proposed to Claude was a solution of running multiple typed queries with transactions and metrics (query name and text getting logged), but with monadic composition similar to what I already have in my validation module.
Monadic composition is where you can essentially combine two functions so that their output is your mapped return of all results. In the validation version, it looks like
To do this on the DbQuery would mean to turn the query and transaction portion into a delegate type.
Sonnet seemed to pick up on this and during its writing, I noticed its thinking catching certain dependencies that I was thinking about. It came up with a solution that I think combines things in a good way.
So, the final output first of what you'd do and how, let's run 2 db queries in a transaction with independent metrics:
This version I think is pretty straight forward, the execution order and mapping to a tuple is done by you.
Now for multiple queries in function composition:
But what about more than 2?
I think that the cleanest approach would be to add another line in the
resultCEversion, which would be the cleanest.Out of cursiosity, I was wondering if it'd be possible to get things to look like the validation applicative model. I wasn't quite successful working with Claude, but got close. Watching its thinking, it does have a better understanding of certain monadic principles than I do. I definitely wouldn't be able to write LanuageExt on my own, that's for sure.
I don't know that this is worth it, but I did get to an output that looks like the below. I'm returning a tuple, but you could combine them into whatever type you want.
The validation module doesn't require the
TxOp.retnto lift the final mapping function into a TxOp.So, while it looks cool, I'll be leaving that portion commented out, most likely.
With some tweaking, I was able to copy portions of the Validation module to get it to look like
At this point, while it looks cool, I don't think it's really worth the effort and complexity over
Do you want them in Parallel? You can try creating new functions or leave it to your later self:
All together, the new parts without the monadic behaviors looks like the below. Claude combined my two different runners into just one that ultimately goes into a
RunOnmethod that uses thewithDbActivityhelper function. I'll do some tweaking later on.For example, explicitly opening the connection in theRemoving this would break parallel queries.Runmethod isn't necessary since Dapper does that itself.The base types changed a bit to match FSharp, but I'm hitting the max comment length. It'll be in the blog.
I think that this is a decent place to be. I'm likely not going to go with the apply, map, and zip functions, or maybe I'll keep zip to make easy tuples, but I think that the most straightforward thing is to KISS it with
db.RunInTransaction(...)and do everything there.What I'm hoping this might allow me to do is to replace the longer Repository implementation with a module of individual queries and, instead of injecting the repository, inject the runner. Or maybe refactor some things for easier testing, though my tests are using Docker containers with Postgres for integration tests, so the end to end portions don't require me to do much mocking.
I'm very much open to everyone's thoughts. I'll be updating the blog post with this comment.
Overall, I think that in this thinking I: